C# Tips
−キャストすべきかasするべきか−


[トップ] [目次]

キャストとas演算子

C#ではキャストを使う機会が多いです。 というのも、ArrayListなどのコンテナがobjectを入れるようになってますし、イベントハンドラなんかもobjectが渡されるようになってることが多いからです。
ただ、C#にはC風のキャストの他にas演算子があります。 これらについてまとめてみます。


キャスト

キャストはCの構文と変わりません。


TestClass c = (TestClass)obj;

ただ、構文自体はCのキャストと同じですが、機能は違います。
Cのキャストは「何でもあり」でおかしなキャストをしないようにするのはコードを書く人の責任です。 C#のキャストは、単純型(int、doubleなど)に対してはCのキャストと同じような感じで、その他の型(値型、参照型すべて)に対しては実行時型チェック付きのキャストになります。 なので、Cのキャスト + C++のdynamic_castって感じです。

まず、Cのキャストと同じようなところから。
int、doubleなどといった単純型の型変換については仕様が決められています。 「C#言語仕様 6. 変換」に書かれています。
「6.1 暗黙の変換」は、自動的にやってくれるキャストです。 たとえば、intからdoubleへは暗黙の変換をしてくれるので、わざわざキャストする必要ありません。


int i = 1;
double d = i;     // キャストは不要

逆にdoubleからintへは明示的な変換が必要とされているので、自分でキャストする必要があります。


double d = 1;
int i = (double)d;   // 明示的なキャストが必要

キャストしないと「error CS0029: 型 'double' を型 'int' に暗黙的に変換できません。」というコンパイルエラーになります。
まぁ、「桁数の小さいほうから大きいほうへは代入可能、大きいほうから小さいほうへは桁落ちするから明示的なキャストが必要(桁落ちして構わないという意思表示が必要)」ということですね。 この仕様は、Cとほとんど同じです。

続いて、その他の型に対するキャスト。
継承関係があれば子供から親へは暗黙の変換をしてくれます。


using System;

public class B { }
public class D : B { }

public class Test {
    public static void Main(string[] args) {
        D d = new D();
        B b = d;       // キャストは不要
    }
}

インターフェースも親だと思ってもらって構いません。 だから、インターフェースへも暗黙の変換をしてくれます。


using System;

public interface I1 { }
public interface I2 { }
public class D : I1, I2 { }

public class Test {
    public static void Main(string[] args) {
        D d = new D();
        I1 i1 = d;       // キャストは不要
        I2 i2 = d;       // キャストは不要
    }
}

また、すべての型はobjectから派生してるので、objectには何でも放り込めることになります。
(C++では親を持たないクラスを作れますが、CLSではできません。 親を指定しない場合は、自動的にobjectから継承します。 また、intとInt32はどう違う?でも書いたようにintなどの単純型も実体はSystem.Int32などで、親をたどればobjectに行き着きます。)

親から子孫へは明示的な変換が必要です。


using System;

public class B { }
public class D : B { }

public class Test {
    public static void Main(string[] args) {
        B b = new D();
        D d = (D)b;      // 明示的なキャストが必要
    }
}

インターフェースからそれを実装しているクラスへや、インターフェースから別のインターフェースへの変換も明示的な変換が必要です。


using System;

public interface I1 { }
public interface I2 { }
public class D : I1, I2 { }

public class Test {
    public static void Main(string[] args) {
        I1 i1 = new D();
        I2 i2 = (I2)i1;       // 明示的なキャストが必要
        D d = (D)i2;          // 明示的なキャストが必要
    }
}

派生関係のない、赤の他人にはキャストできません。


using System;

public class CA { }
public class CB { }

public class Test {
    public static void Main(string[] args) {
        CA a = new CA();
        f(a);
    }
    
    public static void f(object a) {
        CB b = (CB)a;       // キャストできない(例外発生)
	}
}

こういうキャストをするとSystem.InvalidCastException例外が発生します。
この辺がCのキャストではなくって、C++のdynamic_castだってところです。


as演算子

as演算子の構文はこんな感じです。


TestClass c = obj as TestClass;

キャストとは逆に「変換先の型」が"as"の右側にきます。
また、as演算子は、型を変換するという意味ではキャストと同じです。 ただ、キャストとはいろいろと違うところがあります。
まず、as演算子は参照型にしか使えません。


double d = 1;
int i = d as int;   // これはエラー

たとえ、intをSystem.Int32と書いてもダメです。値型(struct)には使えません。
また、変換できないときは単にnullが返ります(例外は出ません)。


using System;

public class CA { }
public class CB { }

public class Test {
    public static void Main(string[] args) {
        CA a = new CA();
        f(a);
    }
    
    public static void f(object a) {
        CB b = a as CB;
        if (b == null) {
            // 変換できないときはnullが返る
        }
    }
}

インターフェースからそれを実装しているクラスへや、インターフェースから別のインターフェースへの変換はキャストと同様にできます。


using System;

public interface I1 { }
public interface I2 { }
public class D : I1, I2 { }

public class Test {
    public static void Main(string[] args) {
        I1 i1 = new D();
        I2 i2 = i1 as I2;       // as演算子でインターフェースからインターフェースへの変換
        D d = i2 as D;          // as演算子でインターフェースから実装クラスへの変換
    }
}

ユーザ定義の変換演算子も呼ばれません。


using System;

public class CA { }
public class CB {
    // CA から CB への明示的な変換演算子
    public static explicit operator CB(CA a) {
        return new CB();
   }
}

public class Test {
    public static void Main(string[] args) {
        CA a = new CA();
        CB b1 = (CB)a;      // explicit operator CBが呼ばれて変換される
        CB b2 = a as CB;    // explicit operator CBは呼ばれないため変換できない(b2はnull)
    }
}

(上の例だと、実際にはコンパイルエラーになっちゃいますが)


キャストとas演算子の使い分け

基本的には、

という感じでいいと思ってます。
もうちょっと具体的に言うと、


ArrayList ary = new ArrayList();

// ...aryにオブジェクトを突っ込む

foreach (object o in ary) {
    try {
        TestClass t = (TestClass)o;
        // ...tを使っていろいろする
    }
    catch (InvalidCastException e) {
        // 例外処理
    }
}

こんなコードがあったとします。
ここで、aryには「仕様上、TestClassにキャストできるオブジェクト(TestClass自身かTestClassを継承したもの)しか入らない」という前提があるのなら、これでいいと思います。 逆に、aryには「仕様上、何が入るかわからないのでTestClassにキャストできるとは限らない」のであれば、あまりよろしくないと思います。 もちろん、後者の場合でも例外をきちんと処理すれば、プログラム上は問題ありません。 けど、例外はやっぱり例外として扱いたいじゃないですか。 例外処理は比較的重い部類の処理っていう理由もありますが、それより、例外は「本来なら発生しないはずのもの」だと思うんです。 前者なら「ここでInvalidCastExceptionが出るってことは、他のどっかがバグってる」ってことなので、まさに例外ですが、後者だと「当然、例外は出るけどかまやしねぇ」って感じだと思うんです。 「例外をif文代わりに使っちゃやだ」と言ってもいいですね。
だから、Windows.Formsのイベントハンドラなんかでも、


private void Button_Click(object sender, EventArgs e) {
    Button btn = (Button)sender;    // senderは必ずButtonのはず
    // ...
}

で構わないと思うんです。

じゃあ、後者の場合はどうするかって言うと、


ArrayList ary = new ArrayList();

// ...aryにオブジェクトを突っ込む

foreach (object o in ary) {
    TestClass t = o as TestClass;
    if (t != null) {
        // ...tを使っていろいろする
    }
}

とas演算子を使うのが自然だと思うんです。
as演算子とよく似たis演算子(as演算子の「キャストできるかチェックするだけ版」です)を使えば、


ArrayList ary = new ArrayList();

// ...aryにオブジェクトを突っ込む

foreach (object o in ary) {
    if (o is TestClass) {
        TestClass t = (TestClass)o;
        // ...tを使っていろいろする
    }
}

と書けますが、これはちょっともったいないです。
これだと、is演算子のときに「キャストできるかチェック」して、キャストのときにもういっぺん「キャストできるかチェック」することになっちゃうからです。

基本的にはこれでいいと思ってるんですが、ちょっと悩ましいのがユーザ定義の変換演算子です。
変換演算子は、as演算子だと呼ばれないのでキャストにしなきゃいけません。 おまけに、変換演算子の存在理由は「知らないうちにうまいことつじつまをあわせてくれる」ってとこだと思うんです。 クラスを使う立場のときに「えっと、このクラスは変換演算子を呼ばなきゃダメだから、as演算子を使っちゃダメで...」 なんてこと考えなきゃいけないんだったら、最初から変換演算子なんて使わずにToHoge()みたいな変換用のメソッドを用意したほうがよっぽどましです。 けど、それだと「常にキャストを使う」ってことになっちゃいますし。 このへんは、まだ考えがまとまってません(まぁ、よほどのことがない限り変換演算子なんて使うなってことになるとは思いますが)。


[トップ] [目次]

株式会社ディーバ 青柳 臣一
2002/04/13