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
です。
…ということが起きる。
この場合だと、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が値型の場合、元の型にしかキャストできない。