C#といえばLINQ、LINQといえば IEnumerable<T> (以下 IE<T>)なわけですが、先日仕事でちょっとハマった出来事がありました。
※以降のコードは、元のコードの問題点のみわかるように改変したものです。
とあるアセンブリの IE<T> を実装しているはずのクラスオブジェクトに対してLINQを使おうとしたところ…(実際は Visual Studio 2015 でしたが本記事では 2017 で確認)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | - | | ! - | | | ! - | - | ! ! | |

error CS1061: 'FooCollection' に 'Select' の定義が含まれておらず、型 'FooCollection' の最初の引数を受け付ける拡張メソッド 'Select' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。
LINQが使えない…だと…!?*1
ビルドエラー内容を見ると、 IE<T> が実装されていないかのような内容です。実際、 int 等に対してLINQを使おうとしてもこれと同じエラーになります。
対象クラスが他人の作ったアセンブリ内で定義されていることもあり、アセンブリのCLRバージョンを調べたり、Google先生に「LINQ 特定クラス 使えない」で聞いてみたりとしてみたもののわからず。
ふと、改めて問題のクラスの定義をよくよく見てみると…
|
…ん?そういえばこの BaseCollection の定義を見てなかったな?
|
これだー!!
KeyedCollection<int, Base> は IE<Base> を実装しています*2。つまり FooCollection は IE<Foo> と IE<Base> の2つの IE<T> を実装していたのです。
ならそういうエラーメッセージにしてよ…。
ちなみにLINQを使わず次のようなコードにすると…
問題なくビルドが通ります。 foo は Foo に型推論されます。継承ツリーの末端側で実装されている IE<T> が優先されるようですね。
当然ながら、末端で複数の IE<T> を実装したクラスの場合は foreach でもビルドエラーになります。…が、そのエラーメッセージは次のように大変わかりやすいものとなっています。
error CS1640: 'IEnumerable<T>' の複数のインスタンスを実装するため、foreach ステートメントは、型 'MyCollection' の変数では操作できません。特定のインターフェイスのインスタンス化にキャストしてください。
foreach パイセンまじカッケーっす!…いや、C#コンパイラがLINQに対してツンツンすぎるのか?
今回問題となったクラスは IE<Foo> を後付けで実装した香りがプンプンしていて、言ってしまえばクラス設計ミス*3です。このアセンブリの開発者はLINQを使っておらず問題に気付かなかったのでしょう。C#erの面汚しめ…(言い過ぎ)。
とはいえ大人の事情でアセンブリを修正してもらうことはできません。ではどうするかというと、要はどちらの IE<T> を使おうとしているのかコンパイラが判断できないのが原因なので、次のようにキャストしてしまえば問題なくLINQが使えるようになります。
IEnumerable (非ジェネリック版)に対するLINQメソッドはそのまま使えるので、 Cast<T>, OfType<T> 等のメソッドで IE<T> に変換するという手もありますが、やや無駄な処理ですかね。
インタフェースの仕様上は、複数の IE<T> を実装することになんら問題はありません。ですがLINQで使おうとすると不都合があるため、聡明なC#er諸氏はそのような実装を行わないようにしましょう。してくださいお願いします。