ぼやきごと/2017-07-06/C#:ジェネリック版 IEnumerable 実装クラスなのにLINQが使えない…だと…!? の変更点


#blog2navi()
*C#:ジェネリック版 IEnumerable 実装クラスなのにLINQが使えない…だと…!? [#c979e2af]

C#といえばLINQ、LINQといえば @code{IEnumerable<T>}; (以下 @code{IE<T>};)なわけですが、先日仕事でちょっとハマった出来事がありました。

※以降のコードは、元のコードの問題点のみわかるように改変したものです。

とあるアセンブリの @code{IE<T>}; を実装しているはずのクラスオブジェクトに対してLINQを使おうとしたところ…(実際は Visual Studio 2015 でしたが本記事では 2017 で確認)

#code(csharp){{
// 再現コードの一部抜粋
// FooCollection は IE<Foo> を実装している…はず…。
// もちろん using System.Linq; は書いてあります。

/// <summary>
/// FooCollection の各要素の Value プロパティ値を出力する。
/// </summary>
/// <param name="foos">FooCollection 。</param>
static void PrintFooValues(FooCollection foos)
{
    foreach (var v in foos.Select(item => item.Value))
    {
        Console.WriteLine(v);
    }
}
}}

#ref(VS2017_BuildError_CS1061.png,left,nolink,error CS1061: 'FooCollection' に 'Select' の定義が含まれておらず、型 'FooCollection' の最初の引数を受け付ける拡張メソッド 'Select' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。)

> error CS1061: 'FooCollection' に 'Select' の定義が含まれておらず、型 'FooCollection' の最初の引数を受け付ける拡張メソッド 'Select' が見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足していないことを確認してください。

''LINQが使えない…だと…!?''((C#erにとってLINQが使えないことは死刑宣告に等しい。))

ビルドエラー内容を見ると、 @code{IE<T>}; が実装されていないかのような内容です。実際、 @code{int}; 等に対してLINQを使おうとしてもこれと同じエラーになります。

対象クラスが他人の作ったアセンブリ内で定義されていることもあり、アセンブリのCLRバージョンを調べたり、Google先生に「LINQ 特定クラス 使えない」で聞いてみたりとしてみたもののわからず。

ふと、改めて問題のクラスの定義をよくよく見てみると…

#code(csharp,nomenu,nonumber,nooutline,noliteral,nocomment){{
public class FooCollection : BaseCollection, IEnumerable<Foo>
}}

…ん?そういえばこの @code{BaseCollection}; の定義を見てなかったな?

#code(csharp,nomenu,nonumber,nooutline,noliteral,nocomment){{
public class BaseCollection : KeyedCollection<int, Base>
}}

''これだー!!''

@code{KeyedCollection<int, Base>}; は @code{IE<Base>}; を実装しています((@code{Foo}; は @code{Base}; を継承している。))。つまり ''@code{FooCollection}; は @code{IE<Foo>}; と @code{IE<Base>}; の2つの @code{IE<T>}; を実装していた''のです。

''ならそういうエラーメッセージにしてよ…。''

ちなみにLINQを使わず次のようなコードにすると…

#code(csharp){{
static void PrintFooValues(FooCollection foos)
{
    foreach (var foo in foos)
    {
        Console.WriteLine(foo.Value);
    }
}
}}

問題なくビルドが通ります。 @code{foo}; は @code{Foo}; に型推論されます。継承ツリーの末端側で実装されている @code{IE<T>}; が優先されるようですね。

当然ながら、末端で複数の @code{IE<T>}; を実装したクラスの場合は @code{foreach}; でもビルドエラーになります。…が、そのエラーメッセージは次のように大変わかりやすいものとなっています。

> error CS1640: 'IEnumerable<T>' の複数のインスタンスを実装するため、foreach ステートメントは、型 'MyCollection' の変数では操作できません。特定のインターフェイスのインスタンス化にキャストしてください。

''@code{foreach}; パイセンまじカッケーっす!''…いや、C#コンパイラがLINQに対してツンツンすぎるのか?

今回問題となったクラスは @code{IE<Foo>}; を後付けで実装した香りがプンプンしていて、言ってしまえばクラス設計ミス((@code{BaseCollection}; をジェネリックにすべき。))です。このアセンブリの開発者はLINQを使っておらず問題に気付かなかったのでしょう。C#erの面汚しめ…(言い過ぎ)。

とはいえ大人の事情でアセンブリを修正してもらうことはできません。ではどうするかというと、要はどちらの @code{IE<T>}; を使おうとしているのかコンパイラが判断できないのが原因なので、次のようにキャストしてしまえば問題なくLINQが使えるようになります。

#code(csharp){{
// 再現コード修正版

static void PrintFooValues(FooCollection foos)
{
    foreach (var v in ((IEnumerable<Foo>)foos).Select(item => item.Value))
    {
        Console.WriteLine(v);
    }
}
}}

@code{IEnumerable}; (非ジェネリック版)に対するLINQメソッドはそのまま使えるので、 @code{Cast<T>};, @code{OfType<T>}; 等のメソッドで @code{IE<T>}; に変換するという手もありますが、やや無駄な処理ですかね。

インタフェースの仕様上は、複数の @code{IE<T>}; を実装することになんら問題はありません。ですがLINQで使おうとすると不都合があるため、聡明なC#er諸氏はそのような実装を行わないようにしましょう。してくださいお願いします。

RIGHT:Category: &#x5b;[[C#>ぼやきごと/カテゴリ/C#]]&#x5d;&#x5b;[[Visual Studio>ぼやきごと/カテゴリ/Visual Studio]]&#x5d;&#x5b;[[プログラミング>ぼやきごと/カテゴリ/プログラミング]]&#x5d; - 2017-07-06 04:28:13
----
RIGHT:&blog2trackback();
#comment(above)
#blog2navi()