Java やってる人が C# を使うとはまること

ここでは、Java SE 5.0 以降を知っている人が C# 2.0 を使うことになった場合を考える*1 *2
あと、ライブラリについては触れないことにする*3

命名規約

まず、命名規約が全然違う。Java ではメソッド名にキャメル形式*4を使うけど、C# では Pascal 形式*5を使い、Java では定数名に大文字アンダーバー区切り*6を使うけど、C# では Pascal 形式を使う。
C# に関する命名規約としては、ここだとかここだとかにあるので、参考にするといい。
間違っても、オブジェクト倶楽部のは参考にしないこと*7

struct の扱い

Java ではユーザ定義型は全て参照型だけど、C# では値型も作成できる。また、標準ライブラリの中に struct で定義されたものもある。
で、何にはまるかというと、struct は class と違い、「値渡し」される*8、つまりコピーして渡されるところ。
だから、

struct Struct
{
    public int Field;
}
static void Method(Struct s)
{
    s.Field = 100;
}

static void Main()
{
    Struct s = new Struct();
    Method(s);
    Console.WriteLine(s.Field);
}

このプログラムでは 100 じゃなくて 0 が表示される。
更に interface が絡むと話はややこしくなり、

interface IHoge
{
    int Property { get; set; }
}
struct Struct : IHoge
{
    int property;
    public int Property
    {
        get { return property; }
        set { property = value; }
    }
}
static void Method(IHoge h)
{
    h.Property = 100;
}

static void Main()
{
    IHoge h = new Struct();
    Struct s = new Struct();
    Method(h);
    Method(s);
    Console.WriteLine(h.Property);
    Console.WriteLine(s.Property);
}

このプログラムは・・・自信がなければ実行してみるといいよ。


参照渡しがしたい場合は、ref を使用する。

static void Method(ref int i)
{
    i = 100;
}

static void Main()
{
    int i = 10;
    Method(ref i);
    Console.WriteLine(i);   // 100
}

このように、メソッドの仮引数と実引数の両方に ref をつけることで、参照渡しとなる。
この ref のどちらかを忘れた場合、コンパイルエラーとなる。


また、C# では出力引数もサポートしている。例えば、文字列から数値への変換を試みる TryParse というメソッドは、出力引数を使用している。

int result;
if (int.TryParse("", out result))
{
    // 変換に成功した場合の処理
}
else
{
    // 変換に失敗した場合の処理
}

参照渡しの場合、渡す変数には何らかの値が格納されている必要があるが、out では未初期化の変数も渡すことが出来る。
ただし、out として受け取った引数にはメソッド内で必ず値を格納する必要がある。

enum

Javaenum は強力だけど、C#enum は C 言語風なので、メソッドを定義することが出来ない。
もし同じようなことをやりたければ、できないことはないけど、参照型になってしまうので、null が入ってくる可能性が出てくるのが欠点。

定数と読み取り専用変数

Java では フィールドや変数に final と付けるだけだが、C# の場合コンパイル時定数には const を、読み取り専用フィールドには readonly を使用する。
また、Java の final は基本どんな変数にも付けることが出来るが、readonly はフィールドにしか使用できない。
また、const 定数には static を付けることは出来ないが、アクセスする際にはインスタンス経由ではなく、クラス名を修飾して呼び出す。
const は、readonly とは違いローカル変数にも使用することができる。ただし、やはりコンパイル時定数である必要がある。

class Hoge
{
    // 定数
    public const int Piyo = 100;
    // 読み取り専用フィールド
    public readonly int Foo;
    
    // 読み取り専用フィールドはコンストラクタで設定可能
    public Hoge(int foo) { Foo = foo; }
    
    public void Bar()
    {
        const int A = 10;
        Console.WriteLine(A);
    }
}
Console.WriteLine(Hoge.Piyo);

Hoge h = new Hoge(10);
Console.WriteLine(h.Foo);

また、C# の interface は Java と違い、定数や入れ子になったクラスを定義することが出来ない。

virtual や override

C# では Java と違い、オーバーライドするには基底クラスのメソッドが virtual になっていなければならない。
更に、オーバーライドする側のメソッドには override を付ける必要がある。Java では @Override アノテーションがあるが、これは付けても付けなくてもいいが、C# の override は付けないとコンパイルできない。
ただし、C# では interface からのメソッドには override は不要 (付けるとエラー)。

ジェネリクス

JavaジェネリクスC#ジェネリクスは、実装方法が違うので比較的はまることが多いかもしれない。
どう違うかについては、ジェネリック: Java vs C#にとても分かりやすくまとめられている*9

比較

Java には演算子のオーバーロードがないため、等値性と同一性が一致する場合以外は equals メソッドや compareTo メソッドなど、何らかのメソッドを使用するが、C# には演算子のオーバーロードがあるため、比較に演算子を使用することが多い。
たとえば、文字列 (string) の比較に == や != 演算子を使用したり、日付・時間 (DateTime) の比較に == や !=、< や > などの比較演算子が使用できる。

string str = "hoge";
Console.WriteLine(str == "hoge");

DateTime a = new DateTime(2000, 1, 1);
DateTime b = DateTime.Now;
Console.WriteLine(a < b);

例外処理

C# の例外処理は、Java よりも書き方が多い。
Java では例外オブジェクトを受け取るための変数は必須だが、C# では catch 内で使用しない場合、省略できる。
更に、C# では全ての例外をキャッチし、キャッチした例外オブジェクトを使用しない場合、例外クラスの名前と小括弧すら省略できる。

// 通常パターン
try { ... }
catch (HogeException e) { ... }

// 例外オブジェクトを使用しない
try { ... }
catch (HogeException) { ... }

// 全ての例外をキャッチし、例外オブジェクトも使用しない
try { ... }
catch { ... }

さらに、例外の再スローにも 2 つの方法が用意されている。
ひとつは Java と同じように、throw e; とする方法*10。そしてもう一つは、ただ throw; とだけする方法がある。
後者は、上で紹介した例外オブジェクトを使用しない場合にでも例外を再スローすることができる。


また、C# には Java とは違い検査例外は存在しない*11

名前の扱い

Java では フィールド名と メソッド名に同じ名前を使用できるが、C# では使用できない。
ただし、C# ではメソッド名を大文字からはじめるため、基本的に小文字からはじまるフィールド名と衝突することはない。

名前空間の扱い

C# には同じ名前空間は単純に名前の衝突を避けるためだけに存在するため、Java のパッケージプライベートと同じようなものはない。
アセンブリをパッケージプライベートの代わりに使うことも出来るが、ちょっと面倒だし、ILMerge を使わないと dll が増えすぎて微妙。

入れ子になったクラスと静的クラス関連

Java には 4 つの入れ子になったクラス*12が存在するが、C# には 1 つしかない*13
更に、C# には static クラスが存在するため、static が付いたネストクラスと付いていないネストクラスが記述できるが、Java の場合とは全く違う意味を持つため注意が必要。

class Outer
{
    // C#ではJavaでのstaticなネストクラスに相当するものしか存在しない
    class Inner1 { }

    // ただ、C#にはstaticクラスというものが存在するため、Javaに慣れていると勘違いしやすい
    static class Inner2 { }
}

static クラスとはいわゆるユーティリティクラスの作成の補助のために用意された構文であり、クラスに static を付けると、

  • sealed になる
  • インスタンスが生成できなくなる
  • static メンバ以外を持てなくなる

という性質を持つようになる。


もし C#Java のインナークラスのようなものを実現したい場合は、明示的にコンストラクタを用意すればいい。

class Outer
{
    class Inner
    {
        Outer outer;
        Inner(Outer outer) { this.outer = outer; }
        // ...
    }
    // ...
}

逐語的文字列リテラルの存在

C# には逐語的文字列リテラルが存在するが、Java には存在しないため、Java をやっていた人は使わない (そもそも知らない) ことがある。
逐語的文字列リテラルSQL や HTML、正規表現などを記述するために便利なので、是非とも使用するべき。

string str @"逐語的文字列リテラルでは\rや\nなどがそのままの文字として認識される。
また、改行を含むことも出来る。
逐語的文字列リテラル中にダブルクォーテーションを記述したい場合、""のように重ねる。";

Java で \ を含む正規表現を書くと悲惨なことになるが、C# では逐語的文字列リテラルを使用することで分かりやすく記述できる。

using ステートメントの存在

これも Java には存在しないため、使用されないことがある (代わりに try-finally を使用する)。
が、try-finally よりも記述が容易な上、記述間違いも起こりにくいため、使える部分では積極的に使用するべき。

StreamReader reader = null;
try
{
    reader = new StreamReader(filePath);
    // ...
}
finally
{
    if (reader != null) reader.Dispose();
}

このプログラムは、以下の using を使用したプログラムと等価である、

using (StreamReader reader = new StreamReader(filePath))
{
    // ...
}

using は入れ子にして使用することが出来る。例えば、

using (SqlConnection con = new SqlConnection(ConStr))
using (SqlCommand cmd = new SqlCommand(sql, con))
using (SqlDataReader reader = cmd.ExecuteReader())
{
    // ...
}

と記述できる。これは特殊な構文ではなく、using ステートメントの本体が using ステートメントのみであるだけで、for ステートメントなどの場合、

for (int i = 0; i < row; i++)
    for (int j = 0; j < col; j++)
        Console.WriteLine(table[i, j]);

このようにインデントするのが普通 (VS もインデントを行う) だが、using ステートメントの場合はインデントを行わないのが普通である (VS もインデントを行わない)。

null 許容型と ?? 演算子

Java のプリミティブ型には null を入れることは出来ない*14が、C# では null 許容型を使用することで null を入れることできる。

void Method(int? num)
{
    if (num == null) Console.WriteLine("<null>");
    else Console.WriteLine(num);
}

void Method2(int? num)
{
    // ??演算子を使うと、左項がnullだった場合の値を指定できる
    int n = num ?? -1;
    // ...
}

?? 演算子は null 許容型以外にも使える。例えば、Java なら

// Java
public static String defaultValue(String...values) {
    for (String value : values) {
        if (value != null) return value;
    }
    return null;
}

のようなメソッドを用意するような部分で、

string str = hoge ?? piyo ?? foo ?? "";

のように記述できる。更に、?? 演算子は null 以外の値が見つかった時点で評価が中断されるため、

string str = Hoge() ?? Piyo() ?? Foo() ?? "";

としたときに、Hoge() が非 null を返せば、それ以降のメソッドは呼び出されないという利点も持つ。

考える粒度の違い

Java では インターフェイスやクラスといった単位で設計等を考えるが、C# にはデリゲートという関数ポインタをカプセル化したようなものがあるため、考える粒度が Java よりも細かくなる。
デリゲートを使用すると共通のインターフェイスや基底クラスを持っていなくても、シグニチャさえあっていれば同じものとして扱うことが出来るため、「クラスに縛られない」記述が可能となる*15

番外編 : Set がない

ライブラリのことについては触れないといったけど、個人的には大きかったことなので特別に。
C# には、Java にあるような Set が (2.0では) 存在しない。
そのため、(外部のライブラリに頼れない場合は) 自分で作るか、IDictionary を使って擬似的に実現するしかない。

*1:C# 3.0 は自分自身が全然知らないし、C# 3.0 を使えるような現場ってまだ少ないんじゃ?

*2:一応、自分とか職場の人たちがはまった/知らなかったことからチョイスしている

*3:ライブラリに触れていたらキリがない!

*4:likeThis

*5:LikeThis

*6:LIKE_THIS

*7:あまりに Java に影響されすぎている上、いろいろおかしな部分が目立つ

*8:追記:class は参照が値渡しされる

*9:なんと 3 年前の記事!

*10:ただし、こちらを使用するとスタックトレースがクリアされる。コメントや[http://d.hatena.ne.jp/bleis-tift/20080903:title=ここ]を参照

*11:Java でも積極的に使うべきではない

*12:インナークラス、ローカルクラス、匿名クラス、static なネストクラス

*13:Java の static なネストクラスに相当

*14:やるとしたら、ラッパークラスを使用する

*15:これがいいことかどうかの判断は各自に任せます:-P