入社して4年ほどjavaによる開発ばかりやってきましたが、次のプロジェクトでC#を触ることになりました。
同じオブジェクト指向型言語なので基本的には同じところが多かったのですが、勉強してここはjavaと違うんだなと感じたところを纏めます。
javaはわかるけどこれからC#を勉強したいという人の助けになればと思います。
1.Stringじゃなくてstring
まず初めに感じたのは文字列クラスの型の書き方です。
javaならクラスの型は先頭が大文字の"String"で記述しますが、C#の場合、”string”が正解になります。
- javaの書き方
String name = "hoge"; // StringのSが大文字
- C#の書き方
string name = "hoge"; // stringのsが小文字
C#でもusing System;
を宣言していればString
と書いてもエラーにはなりませんが、
公式の回答としてstring
と書くべきと発表されているので
C#を書く時には大人しく従って小文字から始めたほうがいいでしょう。
StyleCopというマイクロソフトが出しているコーディングスタイルチェックツールではString
は指摘対象だそうです。
2.型推論
これはもしかしたらC#を学んだうえで最も違和感を感じたところかもしれません。
変数を定義したりインスタンスを作成する際にはstringやintなど、型を定義しますが、
C#ではそれらをvarというキーワードで纏めることができます。
例えば下記のようなコードではstringやHuman(独自に定義したクラスです)などの方を明示的に指定する方法で書くこともでき、2行目のようにvarキーワードを使用して暗黙的に型を指定する方法で書くこともできます。
string str = "test"; //明示的な型指定
var str2 = "test"; //暗黙的な型指定
Human human = new Human(); //明示的な型指定
var human2 = new Human(); //暗黙的な型指定
ただし、変数の型としてvarを使用する場合には変数宣言時に初期化をする必要があり、下記のようなコードは
コンパイルエラーになります。
var str;
str = "test"
また、varで定義した変数にはどのような型をいれることもできますが、
下記のように場合によって入れる型が変わるということもできません。
int flg;
Console.WriteLine("1か2を入力してください。");
flg = int.Parse(Console.ReadLine());
var value = flg == 1 ? 0 : "1";
これはユーザに1か2を入力してもらい、varで宣言した変数value
に
- 1の場合はint型の数値
- 2の場合はstring型の文字列
をそれぞれ代入するコードです。
このようなコードを書いた場合は
'int' と 'string' の間に暗黙的な変換がないため、条件式の型がわかりません。
というコンパイルエラーが発生します。
つまりvarで宣言した変数はObject型のようなどんな型でも入れられる万能な型ではなく、
初期化した型に自動で変換してくれるだけのものというわけです。
では明示的な型指定とvarを使った暗黙的な型指定のどちらを使えばいいのかというと[マイクロソフトが出しているプログラミングガイド]
(https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/inside-a-program/coding-conventions#implicitly-typed-local-variables)によると基本的にはvarを使った暗黙的な型が推奨されているそうです。
例外として、下記のコードように呼び出し先のコードを確認しないと型が分からない場合は明示的に型指定するお方がいいとされています。
class Sample10 {
public void doSample() {
var calc = new Calc(); //ここを見ただけでcalcはCalc型だと分かる
var res = calc.getResult(); //getResultの定義を確認しないとresの型が分からない
}
}
class Calc {
int x = 10;
double y = 0.08;
public double getResult() {
return x * y;
}
}
3.nullチェックは?をつけるだけ
javaではオブジェクトがnullでない場合のみ処理を行う場合、下記のように書きます。
(getNameメソッドはnullを返却する可能性がある。)
// java8以降。optionalとラムダ式を使用したやり方
Optional<String> name = getName();
name.ifPresent(n -> System.out.println(n.toLowerCase()));
// java8以前や上記の書き方が使えない場合
String name = getName();
if(name != null) {
System.out.println(name.toLowerCase());
}
C#だとこんな書き方ができます。
// C#6以降。"."の前に"?"をつけることでオブジェクトの参照がnullの場合は処理を行わなくなる。
string name = ghetName();
Console.WriteLine(name?.ToLower());
勿論C#でもifを使った書き方も可能です。
"?"をつけるだけでいいので楽ですね。
4.文字列の比較は"=="で
javaをやっている人なら参照型とプリミティブ型の違いは理解していてintやcharなどの値型なら変数に格納されている値を比較する"=="を使う、StringやDoubleなど、変数にアドレス値が格納される参照型ならそのアドレスが示すメモリに格納されている値を比較する".equals()"メソッドを使用する。これはよく知られている話だと思います。
上記の参照型とプリミティブ型の話はC#でも同じなのですが、文字列の比較に関しては"=="が推奨されています。
javaで==メソッドを使った場合、下記のようになります。
(new String
を使うことで異なる参照先を持つオブジェクトを生成しています。)
String str1 = "test";
String str2 = new String(str1);
System.out.println(str1 == str2); //falseになる
ところが、C#で同様の処理を書くと下記のようになります。
(string.Copr()
メソッドを使うことで異なる参照先を持つオブジェクトを生成しています。)
string str1 = "test";
string str2 = String.Copy(str1);
Console.WriteLine(str1 == str2); //trueになる
なぜ上記のような結果になるのかというと、C#ではstringクラスを"=="で比較した場合、暗黙的に"equals()"メソッドに置き換えられて処理されるそうです。
どっちを使っても結果は一緒ってこと?それなら使い慣れて誤解を産みにくい"equals()"メソッドを使ったほうがいいじゃん!って思いますが、C#ではむしろ"=="が推奨されています。
それはなぜかというと下記のコードで説明します。
public void doSample() {
var var1 = "123";
var var2 = 123;
Console.WriteLine(var1 == var2); //コンパイルエラーになる
Console.WriteLine(var1.Equals(var2)); //falseになる
このように型が異なる変数の比較をしようとした際に、
"=="だとコンパイルエラーが発生するのですが、"equals()"の場合はコンパイルエラーは発生せずに実行までできてしまいます。(型が違うと必ずfalseになりますが)型が異なるものの比較は通常おかしい処理になりますのでバグを実装時に検知できるという観点から"=="が推奨されているそうです。
5. refキーワードとoutキーワード
javaにはなくてC#にはあるものの一つとしてrefキーワードとoutキーワードがある。
- refキーワード
引数を参照渡しで定義するために使う
javaの場合、int型などの値型変数は引数として渡し際には値渡しになり、
呼ぼ出し先で変更されても呼び出し元には影響しない。
C#でも基本的には同じだが、引数にrefキーワードをつけることで値型でも参照渡しすることが可能になる。
まず、下記のコードを実行してみる
public void doSample() {
int num = 1;
Console.WriteLine(num);
Console.WriteLine(add1(num));
Console.WriteLine(num);
}
private int add1(int num) {
return num = num + 1;
}
この実行結果はもちろん下記のようになります。
1
2
1
続行するには何かキーを押してください . . .
次に、refキーワードを追加してみます。
public void doSample() {
int num = 1;
Console.WriteLine(num);
Console.WriteLine(add1(ref num));
Console.WriteLine(num);
}
private int add1(ref int num) {
return num = num + 1;
}
add1という関数の引数にrefキーワードをつけた以外は先ほどのコードと同じです。
これで実行すると下記の結果が得られる。
1
2
2
続行するには何かキーを押してください . . .
3つ目の出力が2になっていることからint型なのに呼び出し先の影響を受けていることが分かる。
これがref関数を使うことによる値型の参照渡しです。
ちなみにrefキーワードは呼ぼ出し先と呼び出し元の両方に記述する必要があります。
これは参照渡しには自分が意図しないところで変数の内容が書き換えられる危険性があるため、使う側にここでは参照渡しをしていると意識させるためにコンパイラがチェックしているそうです。
- outキーワード
次に、outキーワードについて説明します。
まず、下記のコードを見てください。
public void doSample() {
int num;
getNumber(ref num);
Console.WriteLine(num);
}
private void getNumber(ref int num) {
num = 5;
}
変数numを参照渡しでgetNumberメソッドに渡して初期化するコードです。
しかし、このコードでは下記のようなコンパイルエラーが発生します。
未割り当てのローカル変数 'num' が使用されました。
これはrefキーワードをつける場合、その変数は初期化済みである必要があるというエラーです。
呼び出し先のメソッドで初期化したいのに事前に初期化しろと言われるのはちょっと面倒だし余分なコードが増えますよね。
そんな時に使用するのがoutキーワードです。
先ほどのコードのrefキーワード部分をoutキーワードに置き換えてみます。
public void doSample() {
int num;
getNumber(out num);
Console.WriteLine(num);
}
private void getNumber(out int num) {
num = 5;
}
}
これならコンパイルエラーは発生しません。
実行結果も下記のようになります。
5
続行するには何かキーを押してください . . .
ちなみに、outキーワードで指定していてもgetNumberメソッドの中で引数を初期化しないとコンパイルエラーになります。
例えば、下記のコードはコンパイルエラーです。
public void doSample() {
int num;
int num2;
getNumber(out num,out num2);
Console.WriteLine(num);
}
private void getNumber(out int num, out int num2) {
num = 5;
}
'num2' はコントロールが現在のメソッドを抜ける前に割り当てられる必要があります。
6.タプル型
上記のoutキーワードは複数の変数の初期化や最小値と最大値を一緒に取得したいなどのケースでやりたくなることが多いと思います。
しかし、そのようなケースでは参照渡しを使うのではなく、タプル型を使用するほうがいいです。
※ タプル型はC#7(.NET FrameWork4.7)以降に追加された機能です。
タプル型を使用するとこのように複数の変数を戻り値として受け取ることができます。
public void doSample() {
int x = 10;
int y = 20;
(int num1,int num2) t = getNumber();
Console.WriteLine(t.num1);
Console.WriteLine(t.num2);
}
private (int num1,int num2) getNumber(int x, int y) {
return (x*2, y*10);
}
javaで同様のことをしようとするとMapやリスト等に格納して返却する必要がある処理です。
値を返すためだけにインスタンスを作成しなくていいので少しスッキリしますね。
7.プロパティ
javaやC#などのオブジェクト指向型言語ではクラスのカプセル化のためにクラスフィールドを隠蔽し、
アクセサーと呼ばれるgetterやsetterなどのメソッドを通してアクセスします。
こうする事で読み込み専用にしたりクラスフィールドにセットされる値の入力チェックが可能になります。
この書き方がC#では省略した形で書くことができ、それをプロパティと呼びます。
まずはjavaでの書き方。
public class Sample{
public void doSample() {
Language l = new Language();
l.setExperience(5);
l.setName("java");
System.out.println(MessageFormat.format("私は{0}の業務を{1}年経験しました。", l.getName(), l.getExperience()));
}
}
class Language{
private int experience;
private String name;
public void setExperience(int value){
if(value < 0 ){
throw new IllegalArgumentException("経験年数は正の数値を入力してください。");
}
this.experience = value;
}
public int getExperience() {
return this.experience;
}
public void setName(String value){
this.name = value;
}
public String getName(){
return this.name;
}
}
上記のコードのようにLanguageクラスのフィールドであるexperienceとnameに値を設定したり取得したりする際にsetterやgetterを使用します。
これをC#ならこのように焼灼した形で書くことができます。
class Sample9 {
public void doSample() {
var l = new Language();
// 直接値を設定するような書き方ができる
l.Experience = 0;
l.Name = "C#";
Console.WriteLine(string.Format("私は{0}の業務を{1}年経験しました。", l.Name, l.Experience));
}
}
class Language {
private int _experience;
private string _name;
// プロパティ
public int Experience {
set {
if (value < 0) {
throw new ArgumentException("経験年数は正の数値を入力してください。");
}
this._experience = value;
}
get {
return this._experience;
}
}
public string Name {
set {
this._name = value;
}
get {
return this._name;
}
}
}
上記のl.Experience
ようにまるでpublicのフィールドに値を設定するような書き方で記述が可能になります。それを可能にするためにLanguageクラスではpublic int Experience
と public string Name
のようなプロパティコードを記述します。
このプロパティの書き方のほうが呼び出し側、呼び出される側ともに記述する量が少なくて済みます。