OfType<T>メソッドとCast<T>メソッド

LINQを使えるのはIEnumerableのジェネリック版、IEnumerable<T>のクラスだけ。IEnumerable(<T>なし)でLINQを使うには拡張メソッドのOfType<T>Cast<T>を適用しないといけない。

自分はDataTableやDataGridViewをよく使う。これらでLINQしたければ、OfType<T>とCast<T>(のどちらか)は必須。ということで今日はこの二つについて。

まず、それぞれのソースをReference Sourceで確認してみる。

OfType<T>のソース
public static IEnumerable<TResult> OfType<TResult>(this IEnumerable source) {
    if (source == null) throw Error.ArgumentNull("source");
    return OfTypeIterator<TResult>(source);
}

static IEnumerable<TResult> OfTypeIterator<TResult>(IEnumerable source) {
    foreach (object obj in source) {
        if (obj is TResult) yield return (TResult)obj;
    }
}

渡されたsourceがnullでなければsourceを渡したOfTypeIteratorを返す。OfTypeIteratorはsourceの中の要素をobjectとして取り出し「is TResult」がtrueのときのみTResultにキャストして列挙するようになっている。

ちなみに、「is」が何をしているかというと…

is 式は、指定した式が null 以外であり、指定したオブジェクトを指定した型に例外がスローされることなくキャストできる場合に、true と評価されます。
is (C# リファレンス)

Cast<T>のソース
public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source) {
    IEnumerable<TResult> typedSource = source as IEnumerable<TResult>;
    if (typedSource != null) return typedSource;
    if (source == null) throw Error.ArgumentNull("source");
    return CastIterator<TResult>(source);
}

static IEnumerable<TResult> CastIterator<TResult>(IEnumerable source) {
    foreach (object obj in source) yield return (TResult)obj;
}

まず、sourceを「as」で IEnumerable<TResult>にキャストして成功すればそれをそのまま返す。

「as」の挙動は…

as 演算子はキャスト操作とよく似ています。 ただし、変換可能でない場合、as は、例外は発生せず、null を返します。
as (C# リファレンス)

で、asでのIEnumerable全体のキャストが失敗した場合(ここではまだ例外が発生しない)、sourceがnullでなければsourceを渡したCastIteratorを返す。CastIteratorはsourceの要素をobjectとして取り出し、直接TResultにキャストして列挙する(チェックなしなのでキャスト失敗→例外が発生しうる)。

勘違いしやすいこと
1.例外さえ発生しなければOfType<T>とCast<T>は同じ結果を返す → ×

MSDNのリファレンス(OfType<T>Cast<T>)を見ても二つの違いは

  • OfType<T>はキャストできる要素のみを返す
  • Cast<T>はキャストできなかったとき例外を投げる

ということしかわからない。なので、例外が発生しない場合の二つの結果は同じになると思ってしまう。

しかし、実際はOfType<T>ではキャスト可否の判定を(実際にキャストするのではなく)「is」で行っているので、

たとえば、こんなコード

    var source = new object[] {"ABC", null, "DEF", null, "HIJ"};
    Console.WriteLine(source.OfType().Count());
    Console.WriteLine(source.Cast().Count());

実行結果は
3
5
です。

【C#】OfTypeメソッドとCastメソッドの見落としてはならない違い。 | 創造的プログラミングと粘土細工

…ということが起きる。

この場合だと、OfType<T>ではnullは結果に含まれず、Cast<T>の方ではnullが””(空文字列)となって結果に含まれることになる。

2.キャストできる型ならすべてOK → ×

元の要素の型と結果の型Tが違っていても、元の型からTにキャスト可能なら、OfType<T>もCast<T>もちゃんと対応してくれそうに見える。実際、参照型ならOK。でも値型だと…

LINQのCastでint→longとかint→shortとか、
その他数値間のキャストをしようとすると
InvalidCastExceptionで落ちる。最近知った。
OfType()だとエラーにはならないが、要素が1つも返ってこない。
[C#]IEnumerable.Castメソッドでint→longはできない | OITA: Oika’s Information Technological Activities

OfType<T>もCast<T>も要素をobjectとして扱うので、そのときに変数のボックス化が行われる。

ボックス化では、型情報と値のコピーを保持するオブジェクトが作られる。そして、そのオブジェクトを値型にキャスト(ボックス化解除)するときには、保持していた型情報の型で変数が作られ、そこに値がコピーされる。

つまり

ボックス化解除のときに型が一致しないので失敗する。
[C#]IEnumerable.Castメソッドでint→longはできない | OITA: Oika’s Information Technological Activities

ということ(OfType<T>で要素が返ってこない原因も同じ)。

まとめ
  • OfType<T>
    • is T がtrueとなった要素のみ返す。
  • Cast<T>
    • 要素を直接Tにキャストして返す(キャスト失敗で例外発生)。
  • 要素はobjectとして扱われるので、Tが値型の場合、元の型にしかキャストできない

参考ページ