こんにちは。
Javaを今まで書いていてここ2ヶ月でC#を書き始めたものです。
それにしてもJavaとC#って似てますよね?
「JavaとC#ってほぼ同じだし、1日で文法さらえたわ」
こんなふうに思っていたら、要注意です(自戒)。
似てるが故にハマる落とし穴というものもあるのです。
背景
聞くところによると、Javaが1995年に最初に登場して、C#が2000年に最初に登場したようです。
Javaの方が5年上の先輩ですね。
よって、C#にできるけどJavaではできない、みたいな言語機能もあるわけです。
まあでもお互いに影響し合って今まで成長してきたのでしょうね。
ここからは、実際にJavaとC#の相違点について考え、Javaエンジニアが特に陥りやすい言語仕様などを紹介していきたいと思います。
1. "protected
" の意味が違う
JavaをやっていてC#を始めて触る人は大概、「internalってなんだ?でもprotectedは共通してる!たぶん同じ意味だろうな」なんて思ったりするものです。
違います。
JavaとC#では、アクセス修飾子の意味合いはかなり違います。
(private
とpublic
しか同じじゃないです)
Java
protected: 同じパッケージ内 or 継承クラス
(修飾子なし): 同じパッケージ内
C#
protected internal: 同じアセンブリ内 or 継承クラス
internal: 同じアセンブリ内
protected: 継承クラス
private protected: 同じアセンブリ内 and 継承クラス
このように…
違うんですよね、意外と。
表にまとめてみるとこんな感じです。
アクセスしやすさ | Java | C# | 詳細 |
---|---|---|---|
1 | public |
public |
どこでも |
2 | protected |
protected internal |
同じパッケージ/アセンブリ内 or 継承クラス |
3 | (修飾子なし) | internal |
同じパッケージ/アセンブリ内 |
3 | - | protected |
継承クラス |
5 | - | private protected |
同じアセンブリ内 and 継承クラス |
6 | private |
private |
同じクラス内のみ |
こうみると、C#はJavaの悪い点をちゃんと改善していて良いなって思いますよね。
C#での「修飾子なし」のアクセスレベル
またもう1つC#を学ぶ際に罠になるのが、C#での「修飾子なし」の場合です。
この場合は、アクセスレベルには「規定のアクセシビリティ」が付与されます。
しかしこの「規定のアクセシビリティ」、つまり「デフォルトのやつ」がものによって異なっているんですよね〜。
アクセスされるもの | メンバのアクセス修飾子 |
---|---|
class |
private |
struct |
private |
interface |
public |
enum |
public |
つまりこれらは一緒です。
public class TestClass
{
private int _privateInt1;
int _privateInt2;
}
public struct TestStruct
{
private string _privateString1;
string _privateString2;
}
public interface ITest
{
public void PublicFunction1();
void PublicFunction2();
}
覚え方としては、「考えられるアクセス修飾子の中で、一番厳しいもの」と覚えれば大丈夫です。interface
やenum
はそもそも普通は実装を持つものではないし、private
にする意味もないのでpublic
になってる、と考えれば納得がいきます。
2. getHoge
, setHoge
などを大量に書きがち
Java上がりのエンジニアは、getter関数やsetter関数を大量に書く習性があります。
なぜなら、Javaで大事なメンバをpublicで宣言して…なんて事したら上司にこっ酷く怒られたからです。
なので普通は外部に公開したいメンバには基本的にgetterとsetterを書いて値の正当性を保持したりするのですが…
C#erはあんまりSetHoge
やGetHoge
なんて書きません。代わりに**「プロパティ」**を使用します。
プロパティとは?
getterとsetterが備わっているメンバみたいなもののことをいいます。
例えば以下の例。
public class Person
{
private int _age;
public int Age
{
internal get
{
return _age;
}
set
{
if (value < 0 || 150 < value) throw new System.ArgumentOutOfRangeException();
_age = value;
}
}
}
_age
というメンバは外からは見えません。しかしAge
というプロパティを通してそのメンバにアクセスすることができるようになります。
また、getterとsetterにはそれぞれ個別のアクセシビリティを指定することができます。上記の例では、getterはinternal
, setterはpublic
となります。これによって歳を気にしだしたアラフォーのおばさんも、外部のアセンブリには自分の歳を非公開にすることができます(まあReflection使ったら見れるんですけどね)。
これによって、下手に冗長なコードを書かずにすみますし、綺麗ですよね。
でもsetHoge
, getHoge
を書きたくなるのはわかります…僕も抜け出せないでいました…
3. Javaにはない参照渡しがある…!!
そうです。Javaにはない参照渡しが存在してしまっているのです。
Javaでは?
Javaでは、関数の引数はいわゆる「値渡し」というものでした。正確に言えば、全てが「クラスのインスタンスへのポインタ」でした。なので、新しいnewしたインスタンスを返してあげたい時は絶対に返り値で戻す必要があったのです。
Java初心者じゃなければ以下の意味がわかるはずです。
public class ListRefModify {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("original");
modify(list);
System.out.println(list);
}
private static void modify(ArrayList<String> list) {
list = new ArrayList<String>();
list.add("modified");
}
}
出力: ["original"]
※なぜならlist
はポインタの値であり、ポインタの指し示す値を変えているわけではないから
C#では?
C#ではJavaでは紛らわしかった、ポインタ渡しではなく、純粋な「参照渡し」ができます!
C#では参照渡しのためにref
, out
, in
という3つのキーワードを、実引数と仮引数の両方に使います。
ref
ただの参照渡し。
public static void modify(ref int a)
{
a = 2;
}
public static void test()
{
int a = 1;
modify(ref a);
Console.WriteLine(a); // 2
}
in
呼ばれた関数内で、readonlyとして使われる参照渡し。「見るだけ、お触りはだめ」的なノリ。
public static void modify(in int a)
{
// a = 2; // compile error
int b = a;
Console.WriteLine(b); // 0
}
public static void test()
{
int a = 0;
modify(in a);
Console.WriteLine(a); // 0
}
out
呼ばれた関数内のみで、絶対に初期化される参照渡し。「これになんかもの入れといて〜」的なノリ。
public static void modify(out int a)
{
a = 2;
}
public static void test()
{
int a;
modify(out a);
Console.WriteLine(a); // 2
}
問題解決…!
これにより、先ほどのJavaの問題(つまり、呼び出し先でnewしたやつを入れてそれを反映させたいという欲求)が解消されます。ListRefModify.javaをC#で書き直してみます。
public class ListRefModify
{
public static void Main(String[] args)
{
List<string> list = new List<string>();
list.Add("original");
Modify(ref list);
Console.WriteLine(list);
}
private static void Modify(ref List<string> list)
{
list = new List<string>();
list.Add("modified");
}
}
出力: ["modified"]
C#はもともとクラスに関しては参照渡しですが、refをつけることで「参照の参照渡し」ができるようになります。これで、上記のように呼び出し先で違うインスタンスの参照を代入しても値が変更されないことがありません。
最初は多いように感じますが、使っていくたびにC#の参照渡しの多様性のありがたみに気づき、C#無しじゃ生きていけないようになり、そしてJavaが霞んで見えてくるでしょう
おわり
今回はJavaに慣れている人が驚くであろう、そしてつまずくであろうポイントを3つ紹介しました。なぜかC#を称賛してJavaを卑下しそうになりましたが、Javaにだって良いところはあります。どっちも愛してあげようね。