とはいえ、標準のクラスライブラリには、かなりたくさんのバリエーションがあり、多くはほとんど使われていないんじゃないかと思います。(System.Collection名前空間に属するものは使わないほうがよいですが。)
そんな中で、System.Collection.ObjectModel名前空間に属するものに、たまに使いたくなるものがあったりします。
そのうちの一つに、「KeyedCollection」クラスがあります。これは、ListとDictionaryの両方の特徴を持つようなクラスになっていて、
- Dictionaryのようにキーで要素にアクセスできる。
- Listのように追加/挿入時の順番が保持され、Indexでアクセスできる。
- 要素の一部がキーとして扱われる。
- キーの重複は不可。
といった特徴を有します。なので、たとえばEntityの中にIDのような一意な値を持っていて、そのキーでアクセスするようなケースで有用です。さらに順番を保持させたいなら積極的に使うべきクラスかも。
ただ、大変惜しむらくは、KeyedCollectionクラスは抽象仮想クラスになっていて、中に収めるEntityごとにクラスを派生させて使う前提になってしまっています。因みに、派生クラスでオーバーロードするメソッドは基本的に一つのみで、Entityからキーを選びだすセレクタの「GetKeyForItem」メソッド。
使おうとするごとにクラスを一つ作らなければならず、Entityが異なればそれぞれに派生クラスを作らなければならない。今どきこれは大変めんどくさい。
なので、一回汎用的なクラスを作ったら、それを使いまわせるようにしておきたいですね。
戦略としては、「セレクタメソッドをFuncで渡すコンストラクタ」を用意し、先の「GetKeyForItem」メソッドではコンストラクタで渡されたFuncを使う。ことにすれば使いやすくなりそうです。
で、書いてみたのがこれ。
internal class SelectableKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem> { private Func<TItem, TKey> keySelector; internal SelectableKeyedCollection(Func<TItem, TKey> keySelector) { this.keySelector = keySelector; } protected override TKey GetKeyForItem(TItem item) { return keySelector(item); } } public static class KeyedCollection { public static KeyedCollection<TKey, TItem> Create<TKey, TItem>(Func<TItem, TKey> keySelector) { return new SelectableKeyedCollection<TKey, TItem>(keySelector); } }
結局2つのクラスになっていますが、下のKeyedCollectionクラスは、上のSelectableKeyedCollectionを作るだけのクラスです。そしてSelectableKeyedCollectionクラスはinternal指定としています。
なぜにこんな2段階の作りになっているかというと、
なぜにこんな2段階の作りになっているかというと、
var collection = new SelectableKeyedCollection<X, Y>(x => x.y);
と書かせるより、
var collection = KeyedCollection.Create((X x) => x.y);
のほうがタイプ量も少ないし、Intellisenseもある程度効いてくれます。そして、SelectableKeyedCollectionクラスをintenalとすることで、使う人はその存在を知る必要がなくなり、KeyedCollection<TKey, TItem>を使うために、KeyedCollectionクラスさえ知っていれば良くなります。そのほうが親切だと思うのです。
.net FrameworkのTupleが同じような考え方になっています。
このクラスを使ったサンプルを書いてみます。ここでは、「あす以降の一週間のDateTimeをコレクションに突っ込み、曜日でのアクセスとシーケンシャルなアクセスを行う。」コンソールアプリケーションのサンプルです。なんかもう少し意味のあるサンプルにしたいところですが、それをやると簡潔に書けないので。
「System.Collection.ObjectModel」名前空間と、先のSelectableKeyedCollectionとKeyedCollectionが属する名前空間をusingした上で、
static void Main(string[] args) { var week = KeyedCollection.Create((DateTime dt) => dt.DayOfWeek); foreach (var n in Enumerable.Range(1, 7)) { week.Add(DateTime.Today.AddDays(n)); } Console.WriteLine("Today is {0:yyyy/MM/dd (ddd)}", DateTime.Today); Console.WriteLine(); Console.WriteLine("Next 7 days..."); foreach (var d in week) { Console.WriteLine(d.ToShortDateString()); } Console.WriteLine(); Console.WriteLine( "Next Friday is {0:yyyy/MM/dd (ddd)}", week[DayOfWeek.Friday]); }
これをコンパイルして実行すると、以下の結果となります。(ちなみに今日は2014年10月27日)
Today is 2014/10/27 (月) Next 7 days... 2014/10/28 2014/10/29 2014/10/30 2014/10/31 2014/11/01 2014/11/02 2014/11/03 Next Friday is 2014/10/31 (金)
個人的には、これなら使う気になります。
もし本当に汎用的なクラスに仕上げるなら、KeyedCollection<TKey, TItem>クラスには、コンストラクタがもう2つほどあるので、それらに対する備えをしておけば使い勝手の良いクラスができると思います。