Edited at

List<T>使ってたら「CA1002: ジェネリック リストを公開しません」とか言われたけど、じゃあどうしたらいいのさ?

More than 3 years have passed since last update.

CA1002: ジェネリック リストを公開しません というガイドラインがあります。


継承を目的としたジェネリック コレクションを次に示します。公開するときは System.Collections.Generic.List<T> ではなく、これを使用してください。

System.Collections.ObjectModel.Collection<T>

System.Collections.ObjectModel.ReadOnlyCollection<T>

System.Collections.ObjectModel.KeyedCollection<TKey, TItem>


などと書かれていたりしますが、単純に Collection<T> に置き換えればいいというわけでもないと考えています。

ケースごとに見ていきましょう。


引数の場合

もしもあなたが引数に List<T> を受け取ろうとしているのであれば、それは IEnumerable<T>, IReadOnlyCollection<T>, T[] のいずれかにすべきです。なお、Collection<T> を引数に受け取るメソッドは一般的ではなく、メリットもありません。


IEnumerable<T> を受け取る

メソッド内部の処理で列挙が 1 回で済む場合、IEnumerable<T> を受け取るようにしましょう。


良くない

public bool IsHogeHoge<T>(List<T> xs)

{
// Anyで書けますが、例なのでスルーしてください
foreach (var x in xs)
{
if (HogeHoge.Value.Equals(x)) return true;
}
return false;
}


良い

public bool IsHogeHoge<T>(IEnumerable<T> xs)

{
// Anyで書けますが、例なのでスルーしてください
foreach (var x in xs)
{
if (HogeHoge.Value.Equals(x)) return true;
}
return false;
}

引数の原則は可能な限り広い型で受け入れるです。

IEnumerable<T> を受け入れるようにすることで



  • List<T> であっても

  • 配列であっても


  • Collection<T> やその派生型(例えば ControlCollection 型の Controls プロパティとか)でも


  • ObservableCollection<T> であっても

  • LINQ to Object の結果であっても

すべて受け入れることが可能です。

また、.NET Framework 4.0 以降では IEnumerable<T> は共変なので共変性を利用して無駄なキャストを省くこともできます。

// こういうメソッドがあるとき

public bool IsFugaFuga(IEnumerable<object> fugas) {...}

IEnumerable<string> strings = GetStrings();
// 引数の要素型よりも具象的な型の一覧を渡すことが可能
IsFugaFuga(strings);

共変性ってなに?については「ジェネリクスの共変性・反変性 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C」を参照してください。


配列で受け取る

長さによる分岐が必要な場合など、2 回以上の列挙が予想される場合は配列で受け取るのが常套手段です(でした?)。


良くない

public bool IsHogeHoge<T>(List<T> xs)



良い

public bool IsHogeHoge<T>(T[] xs)


なんと、配列はなんと共変性を持っています。すなわち object[] を受け取るメソッドに対して string[] を渡すことなどができます(まぁ半ば無理やりなので細かい問題はありますが、通常の使用で問題になることはありません)。

なお、配列で受け取った場合は要素の書き換えが可能ですが、メソッドの思想・お作法として引数で受け取った配列を書き換えるべきではありません。書き換えようとした場合、上記の共変性の問題もありますので注意してください。


IReadOnlyCollection<T> で受け取る

.NET Framework 4.5 以降であれば列挙可能な読取専用シーケンス(IEnumerable<T>)に対して Count プロパティが追加された IReadOnlyCollection<T> を使用することが可能です。これは遅延実行されない有限リストであることが保証されているので(列挙回数のパフォーマンスの問題を除けば)気にせず複数回の列挙が可能です。

詳細は「neue cc - .NETのコレクション概要とImmutable Collectionsについて」を参照してください。

ただし、IReadOnlyCollection<T> を引数にとるメソッド設計が一般的かといわれると、現状はそう見かけないように思います。IEnumerable<T> では嫌だけど配列はデメリットが…というケースでは有効な手段だと考えていますが、この辺りはどうなのか意見が欲しいところです。


その他

稀に IList<T> で受け取る設計のメソッドもあるように思えます(特に Microsoft 名前空間以下のクラスなどで見かけるような…)。

こうすることで配列(配列は IList<T> を実装しています)だけではなく List<T>Collection<T> などを受け取れるようになる利点がありますが、.NET4.5 以上を前提としていいのであれば今後は IReadOnlyCollection<T>IReadOnlyList<T> を使用したほうが「共変性を持てる」「メソッド内での書き換えがないことが保証される」といった利点があると考えます。


プロパティの場合

List<T> の代わりに Collection<T> を使用することができます。しかし、一般的には Collection<T> の派生クラスを作成し、それを使用します。また、場合に応じて ObservableCollection<T> など別の基底コレクションクラスから派生することも可能です。


良くない

private readonly List<T> _hoges = new List<T>();

public List<T> Hoges
{
get { return _hoges; }
}



微妙

private readonly Collection<Hoge> _hoges = new Collection<Hoge>();

public Collection<Hoge> Hoges
{
get { return _hoges; }
}



良い

public class HogeCollection : Collection<Hoge>

{

}

private readonly HogeCollection _hoges = new HogeCollection();

public HogeCollection Hoges
{
get { return _hoges; }
}


最初の例が良くない理由は「多態性を保てないから」です。List<T>Add, Remove などの各メソッドは非仮想であり、これらのタイミングで何らかの考慮をしたくなってもメソッドをオーバーライドすることができません。

2つ目の例であれば以下のようにすることで後からでも動作をカスタマイズすることができます。

public class HogeCollection : Collection<Hoge>

{
public override void InsertItem(int index, Hoge item)
{
// 何か処理を追加
Do.Something(index, item);

base.InsertItem(index, item);
}
}

private readonly HogeCollection _hoges = new HogeCollection();

public Collection<T> Hoges
{
get { return _hoges; }
}

これによって要素の追加、変更、削除時の動作はカスタマイズ可能になりますが、例えば HogeCollection に何か機能(メソッド)を追加した場合、それを外部から呼び出すことはできません。プロパティの型を Collection<Hoge> から変更してしまうと後方互換性が崩れるためです。

したがって、3つ目の例が最も後からの拡張性に優れた方法となります。

ただし、コレクションの種類ごとに都度クラスを作成しなければならないというデメリットもあります。今後の拡張がどのくらい予想されるのか?によって独自コレクションを作成すべきかどうかを判断した方がよいでしょう。


コレクション系プロパティの作り方について

前述の例では以下の原則も守っています。


  • 読取専用プロパティとして作成すること(get アクセサのみを定義)

  • フィールドは readonly とすること

  • コンストラクタもしくはフィールド初期化子でフィールドにインスタンスを設定しておくこと

通常、コレクションのインスタンス自体を作り直さないといけない場面というのはありません。

コレクションはあくまでコンテナとしての機能を提供するべきなので、保持される親のインスタンスと同じライフサイクルで 1 回だけインスタンスを生成し、使いまわすことが望ましいです。

これによって「気付いたら違うコレクションインスタンスに入れ替えられていた」「コレクションを列挙しようとしたらプロパティ自体が null になっていて例外が発生した」などの状況を未然に防ぐことができます。Hoges != null && Hoges.Count != 0 なんてコードとはすぐにおさらばしてください。

なお、C#6.0からは以下のように簡略できます。

public HogeCollection Hoges { get; } = new HogeCollection();

この作り方で困ることは以下の 2 点でしょうか。


  • デシリアライズに対応できないケースがある(シリアライザに依ったと思いますが未確認)

  • オブジェクト初期化子で LINQ によって取得した要素を直接設定できない

皆さんがどうされているか興味があるところです。


戻り値の場合

他に情報が必要ない場合は IEnumerable<T> や配列を返すのが一般的です。

public IEnumerable<Hoge> GetHoges();

public Hoge[] GetHoges();

配列を返す場合、必ず新しい配列インスタンスを生成して返します。内部で同じインスタンスを保持してしまった場合、外部での書き換えの影響を受けるためです。

また、併せて他の情報も必要な場合は戻り値用のクラスを作成し、そのプロパティとして持たせます。

// こんなシグネチャにして

public HogeResult GetHoges();

// こう受け取る
var result = GetHoges();
if (result.HasError)
{
foreach (var hoge in result.Hoges) // Hoges の作り方は前述の「プロパティの場合」の通り
{
Console.WriteLine(hoge);
}
}

こうした HogeResult クラスは作り出すとどんどん増えてしまう問題があるため、C#7.0ではタプル(多値戻り値)が検討されているようです。


まとめ

ここまで List<T> を公開メンバに使用しないための代替手段について紹介してきました。

簡単にまとめると以下のようになります。


  • 引数は IEnumerable<T> または配列にする。IReadOnlyCollection<T> などを使うのもアリ?

  • プロパティは Collection<T> の派生クラスを HogeCollection のように作成する

  • 戻り値は IEnumerable<T> または配列にする。ただし、他にも情報を返したい場合は結果用のクラスを作る

実際に私の周りで List<T> を使用していたために困った事例というのは実はそんなに多くはありません。しかし、敢えて List<T> を使うメリットがあるケースもほとんどありません。後から後悔しないためにも、理由がない限りは一般的な作成方法にしたがっておく方がよいでしょう。