c#のジェネリック

ジェネリックは便利だ。特に.netの提供するSystem.Collections.Generic名前空間にあるListクラスやDictionaryクラスは課題を解決するために大いに役立つ。しかしその利便性に惑わされ実装時の設計判断を誤るケースを最近になりかなり目にした。以下のコードがそうだ。

public List<string> GetMembers() { ... }

このコードはシステムに登録されている利用者の名前を取得するメソッドである。このコードには問題点がいくつも存在する。

  1. 利用者はこの受け取った配列を自由に増減させることが可能なのか。
  2. Listにアクセスする際に何かルールが追加された場合どうするのか。
  3. このはいったい何を指すのか。

これらの問いに上記コードは一切答えていない。回答を検討しよう。これならばどうだろう。

public string[] GetMembers() { ... }

1つめの問いの答えとしては良いだろう。読み取り側は受け取ったリストを勝手に増減してはならないと言語制約を用いて語っている。しかし依然としてstringが何を指すのかは利用者には秘密のままだ。

2つ目の問いに対する回答を検討する前に少し想像する事にしよう。例えばこのコードを含むライブラリを世界中に出荷したとしよう。そこで世界中のあらゆる場面でコードがリンクされ利用されているとする。そこで、しばらくした後このメソッドの戻り値にListが相応しく無いと考察を得た。そこでこのメソッドの戻り値を以下のように修正したとする。

public ReadOnlyCollection<string> Getmembers() { ... }

そしてバージョン2としてリリースしたとしよう。この変更によりユーザーは自分のプロダクトをリコンパルそして再リリースをさせられる事になる。企業でこのような事になった場合、問題は特に深刻だ。そのような事態を避けるためにも戻り値をこうする。

public IList<string> GetMembers() { ... }

ケーススタディが示すように、大抵の場合Listクラスを利用者に返すことは誤りだ。この場合ReadOnlyCollectionやObservableCollection、LinkedListを実体として返しても何も問題がない。自作のリストでもよい。また次の例はどうだろう。

public IList<Member> GetMembers() {
    return new ReadOnlyCollection<Member>() {
        new Member("Taro"),
        new Member("Jiro"),
    };
}

これはかなり良い。意味は明瞭だ。そしてリストの変更操作はできない。リスト自身にもルール追加の余地が残っている。

この考察はすべてのジェネリックコレクションに当てはまる。DictionaryもIDictionaryとしない場合予期せずコードの柔軟性の低下するため注意が必要だ。また自作のジェネリッククラスの場合も深く考察せず作成すると同様の現象が発生する。非常に便利だが安易な利用をし設計上の判断を誤らないよう注意が必要だ。