アドカレの時期となり,何を書こうかと考えた結果,ジェネリックコレクションの作り方とか色々習得できるしいいのでは?と思いこの題材にしました。この記事は,C#でインターフェイスとかコレクション知ってるけどコレクションの作り方分からんと言う人向けです。最後にリンク集ジェネリックコレクションとはジェネリックコレクションとは,ジェネリック(任意の型にできるもの)の要素を格納できるコレクションを表します。インデックスで要素を取得・設定できる,所謂ArrayListのジェネリック版。検索機能が豊富で,コレクションだったら基本的にこれを使う。キーと値を結び付ける。HashTableのジェネリック版。連結リストとも呼ばれ,双方向から要素の追加・削除ができる。要素の追加と削除をメインにしたい場合使う。後入れ先出し型のコレクション。Queueのジェネリック版。後入れ先出し型のコレクション。Stackのジェネリック版。キーと値を結び付けるコレクションで,キーをもとに自動的にソートされる。キーや値のコレクションはインデックス検索できない。要素の追加・削除が速め。キーと値を結び付けたコレクションで,キーの値をもとに自動的に並び替えられる。キーまたは値のコレクションはインデックスを使って検索できる。SortedListのジェネリック版。要素の重複を許さないコレクション。要素が自動的にソートされるHashSet<T>。ジェネリックコレクションを作ると学べる事ジェネリックコレクションを作ると,快適なC#プログラミングにおける仕様をいろいろ学ぶ事が出来ます。interface周りインターフェイスの制約や,明示的実装と普通の実装の違いなどIEnumerable周りコレクションに必要な要素であるIEnumerable<T>インターフェイスや,foreach構文についてインデクサ周り[]を使った要素の検索について(使うときと使わない時あり)例外処理コレクションは例外ぶん投げるorぶん投げられることが多いので,例外の投げ方や投げられないような処理の仕方を学べるジェネリックコレクションのメソッド自分で作ってみることで,他のジェネリックコレクションにあるメソッドのやろうとしていることの意味が分かる実際に作ってみるではジェネリックコレクションを一つ作ってみようとなります。インデクサなど色々使うコレクションという事で,List<T>を自作してみましょう。一部で簡単のためにSystem.Collections.Generic.List<T>の実際のソースコードと異なるところがあります。今回実装するのは一部の主要なメソッドですが,興味があればソースコードとにらめっこしながら実装してみてもいいと思います。最初の準備まずは以下のようにC#のクラスを新しく立てます。}}ここでListとクラス定義している右に<T>がありますね。いう宣言をしているわけです。usingステートメントとしてはコレクション作成に重要な3つを記述します。次に実装するインターフェイスを選択してみましょう。コレクションに関わるインターフェイス一覧コレクションの実装に関係するインターフェイスは以下の通り,非ジェネリック・ジェネリックと沢山あります。非ジェネリックな反復処理をサポートする。ジェネリックな反復処理をサポートする。非ジェネリックな列挙可能オブジェクトを表す。foreach構文で必要。ジェネリックな列挙可能オブジェクトを表す。foreach構文やLINQで必要。非ジェネリックなコレクションを表す。全てのコレクションの元となる。ジェネリックなコレクションを表す。ジェネリックコレクションの元となる。インデックスでアクセス可能な非ジェネリックコレクションを表す。インデックスでアクセス可能なジェネリックコレクションを表す。キーと値のペアからなる非ジェネリックコレクションを表す。キーと値のペアからなるジェネリックコレクションを表す。重複不可能な集合としてのジェネリックコレクションを表す。値が読み取り専用なジェネリックコレクションを表す。値が読み取り専用なインデックスアクセス可能なジェネリックコレクションを表す。値が読み取り専用なキーと値のペアのジェネリックコレクションを表す。TValue>この3つのインターフェイスはコレクション自体のためというより内部処理のための物です。破棄可能なオブジェクトを表す。シリアル化・逆シリアル化の設定。逆シリアル化時の実装の設定。TValue>,HashSet<T>,SortedSet<T>どのインターフェイスを実装すればよいかインターフェイス選びで重要なのは,まずどのようなコレクションを作成するかです。TValue>,重複不可能な集合にしたかったらISet<T>といった感じです。上記の三つのうちどのタイプでもない(Stack<T>やQueue<T>など)場合はどれも実装する必要はありません。後は,どのコレクションでも実装されているICollectionインターフェイスもお忘れなく。TValue>で実装することが強制されているので自分で書く必要はありません。今回はListの実装なので,IList<T>とIReadOnlyList<T>,IListの実装をします。(ICollectionはIListにて実装が強制されている)クラスの継承をする感覚でこの3インターフェイスを実装するとこんな感じ。}}この段階だとインターフェイス実装してないんだけど!って怒られています。後々実装していきましょう。フィールドの実装まずは内部処理に必要なフィールド変数を実装していきましょう。ジェネリックコレクションは基本的に内部にジェネリックTの配列を持っていて,メソッドなどで要素を追加するごとにその大きさを自動的に調整しています。必要なフィールド変数を以下に示していきます。}}フィールド変数の解説をしていきましょう。_arrayは,先述の通り要素を格納していく配列です。後々実装するAddメソッドなどで要素数が変更されていきます。versionは,このList<T>のインスタンスのバージョンを表します。要素の追加や削除をするたびに変化していきます。無意味に見えますが,後々に説明していきます。プロパティの実装ここからはプロパティの実装に移っていきます。プロパティのほとんどはインターフェイスにより強制されているものです。順を追って解説していきましょう。}}Count:内部に格納されている要素数を返す。_array.Lengthとは異なるので,別々の値を設定したい。setも付けておきます。Capacity:_arrayの長さを取得または設定する。値はCount以上でなければならないため,Count未満の値が代入されそうなときはArgumentOutOfRangeExceptionを投げるようにする。値の設定時は同じ要素を持つ新たな配列を作成して_arrayに代入する。(Array.Copyメソッドを使ってもよい。)IsSynchronized:マルチスレッド用のプロパティ。マルチスレッドを想定してオブジェクトが作成されているかどうかを表す。SyncRoot:マルチスレッド用のプロパティ。マルチスレッド時に同期させるオブジェクトを返す。マルチスレッド時の動作を補助するSystem.Threading.Interlockedクラスで実装。IsReadOnly:コレクションが読み取り専用かどうかを返す。インデクサやメソッドで値の設定ができるのでここではfalse。IsFixedSize:コレクションのサイズを固定するかどうかを返す。ここでは要素数に応じて自動調整するのでfalse。ここでよくわからない記述が出てきますね。publicをくっつける普通の実装との違いを以下にまとめました。インターフェスごとに実装する必要あり明示的実装はプロパティだけではなくメソッドでも可能です。明示的実装では以下のように通常実装と混ぜて使うこともできます。Number;}この方法では,ExampleClassのインスタンス名.Numberで呼び出されるのはIExample1としてのNumberだけで,IExample2のNumberを呼び出したいときは((IExample2)ExampleClassのインスタンス名).Numberと記述する必要があります。TValue>の値検索の様に,[]内に特定の型の値を代入して,その値に結びつけられた要素を返したり設定できたりするやつのことです。this[インデックスの型]といった形で定義します。}}IList<T>の方の通常実装では,インデックスの範囲がまず配列の管理範囲内かどうかを判定します。配列の管理範囲外だった場合は例外をスローします。値の設定の場合は,配列内の要素が変更されるのでversionを一つ進めます。IListの明示的実装の方では,object型で定義されているので,値の設定時はキャストする必要があります。キャスト時の例外はキャッチして,引数由来の例外である,ArgumentExceptionとして投げ直します。Enumeratorの実装Enumeratorは,ジェネリックコレクションの内部に定義される,IEnumerator<T>を実装する構造体です。本体コレクションのGetEnumeratorメソッドで呼び出され,列挙処理を行います。Enumerable実装の準備まずは下の様にList<T>クラスの内部にEnumeratorを実装します。}}list:呼び出し元のList<T>のインスタンスへの参照。version:コンストラクタ(後で実装)で初期化時現在のList<T>のバージョン。index:列挙操作時の現在インデックス。次にプロパティ,コンストラクタの実装をします。//列挙中の現在要素を返す。//列挙中の現在要素を返す。}}Currentでは列挙作業中のlist中の現在の要素を返します。明示的実装では,インデックスの値がlistの配列の範囲外になったときに例外を返すようにします。コンストラクタでは上記のフィールドとCurrentの初期化を行います。Currentは取り敢えずデフォルトの値を仮に設定しておきます。(列挙時にはちゃんと割り当てられる。コンストラクタのアクセシビリティがinternalなのは,継承先や外部プロジェクトでわざわざそのコンストラクタを使ってインスタンスを生成しなくても,GetEnumeratorメソッドを使えばいいじゃないかという事です。MoveNext,Reset,Disposeの実装ではいよいよIEnumerator<T>のメソッドについて説明します。MoveNext:現在のindexやversionの状態から,コレクションの列挙を次に進めるかどうかを判定します。Reset:フィールドやプロパティの値を初期化します。Dispose:IEnumerator<T>を実装するとなぜかくっついてくるやつ。foreach構文終了時に呼び出される。中身は何も書かなくていい。実際にコードにしてみましょう。//インデックスがlistの範囲内の場合はCurrentを設定してindexを進めtrueを返す。//それ以外の場合は終了操作を行う。}}//変数・プロパティの初期化を行う。0;}ざっくりやってることをまとめるとDispose:何も書かない。MoveNext:インデックスの状態で判定し,それによって次の要素に進めるか操作を中断するかを返す。version変数が変えられたときに例外をスローするのは,要素の状態が返られると列挙中にインデックスがズレたりする可能性が出てくるからです。Resetが明示的実装なのは,外部から勝手に呼ばれると列挙処理が乱されるからです。明示的実装には隠蔽的な意味合いも含まれています。GetEnumeratorの実装Enumerableを実装したので,メインのList<T>に話を戻していきましょう。列挙可能であるオブジェクトを表すIEnumerable<T>は,唯一GetEnumeratorメソッドの実装を強制しています。これは列挙処理に必要なIEnumerator<T>を呼び出すものです。IEnumerator<T>なら先程Enumeratorとして実装したので,これでこのメソッドは実装できそうなので書いてみましょう。}}Enumerableを呼び出すメソッドとしてGetEnumeratorを用意し,IEnumerable<T>及びIEnumerableのGetEnumeatorは明示的実装をしています。GetEnumerator()としないかと言うと,GetEnumeratorを実行時にList<T>.Enumerator()が呼び出されるという事を明示してコードの可読性を図るためです。要素の追加メソッドの作成コレクションに重要な要素を大体実装してきました。これで漸くリストっぽいメソッドの実装が始まります。ここではAdd,Insertメソッドとその派生メソッドを実装します。これらの操作ではコレクションの状態を変化させるのでversion変数を進めます。配列の大きさを変更するメソッドまずは_arrayの大きさを変更するメソッドを作りましょう。_array大きさ変更は今後重複するためメソッドにまとめて共通化した方が良いです。newSize;}_arrayのサイズの変更について,なぜ2倍にするかと言うと,単に等差数列的に足していく場合は増やす値が小さいとAddRangeなどで一括して要素を増やすときに処理が何度も行われる必要があり,増やす値が大きいと小サイズのデータ管理でいい場合にメモリの無駄が増えてしまいます。そのため倍ずつに増やしていけば適宜対応できるのです。int型のオーバーフローを避けるために一旦uintとして設定した後オーバーフローするかどうかを確かめて修正します。AddそれではAddメソッドを実装しましょう。Addメソッドでの流れは,_arrayに新たな要素を格納する場所が無かったら配列を大きくする_arrayのCount番目に新たな要素を代入するversionを進めるIListのものの場合は要素を追加したところのインデックスを返すとなります。これを実装するとこうなります。//上のAddでCountが進んでいるので進む前の値を返す。1;}IListの明示的自走の方ではobject型の引数をキャストする必要があります。AddRange次はAddRangeメソッドを実装します。AddRangeメソッドは,単に引数の列挙可能オブジェクトを全てAddしていけばいいので,Addメソッドを実装した今は余裕ですね。Add(item);}このように,ただforeach構文で片っ端から持ってきてAddすればいいのです。Insert次はInsertメソッドを実装しましょう。Insertメソッドでは指定したインデックスに要素を挿入するものです。要素数を増やすversionを進めるとなります。}}ここでは配列の要素をずらすためにArray.Copyメソッドを使用しています。for文で代用することも可能です。InsertRange次はInsertRangeメソッドの実装です。これもAdd-AddRangeのような感じで実装すればよいのですが,挿入は一つずつずらしていかなければなりません。item)で繰り返すと引数として提供された追加される要素の順序が逆転してしまう)そのため,一工夫が必要です。item);}ここで見てわかる通り,実行するInsertメソッドの引数がindexではなくindex++になっています。これによって挿入する位置を右にずらしながらInsertを実行する事が出来,collectionの順序を守ったまま実行する事が出来ます。要素を追加するメソッドを実装できたのでコンストラクタを実装できます。コンストラクタでは主に_arrayの初期化を行います。AddRange(collection);}真ん中のコンストラクタでは引数で指定された値で_arrayを初期化します。0として_arrayを初期化します。下のコンストラクタではcollectionのnullチェックを行った後,AddRangeで初期要素として追加します。要素の追加メソッドを作ったのだから次は削除メソッドだと行きたいところですが,要素を削除するにはインデックスの検索が必要なことが多いです。そのためまずはインデックスを検索するメソッドを作成していきます。IndexOfまずはIndexOfメソッドの実装です。このメソッドでは引数として指定された要素と一致する要素のうち先頭の要素のインデックスを返すものです。方針としては,for文で要素を最初から回していき,該当要素に当たったらそのインデックスを返すという感じです。このメソッドにはオーバーロードがあり,それぞれで下の表の通りの検索範囲になります。実装して,残り3つはそのメソッドをうまく引数を合わせて呼び出せばいいと考えられます。-1;}ここで気になるのはEqualityComparer<T>だと思います。そのため,EqualityComparer<T>を用いて比較するか,object型にキャストしてから比較するしかないのです。LastIndexOf次はLastIndexOfメソッドを実装します。IndexOfメソッドで返されるインデックスが要素と一致する先頭の物のインデックスなのに対し,LastIndexOfメソッドでは末尾の物のインデックスを返します。実装方法としては,IndexOfで行ったfor文による検索を逆方向に行うという事です。実際に実装してみるとこんな感じ。}}このように,for文の開始地点,終了地点,加算減算を逆転させて場逆方向に検索すれば良いのです。不等号の=有無には気を付けてください。FindIndex,FindLastIndex次はFindIndexメソッドです。FindIndexメソッドではPredicate<T>で指定された条件の要素のうち最初の要素のインデックスを返します。実装においてはEqualityComparer<T>で比較していたものをPredicate<T>.Involke(T)にするだけです。FindLastIndexもLastIndexOfを改造すれば簡単に作れます。実装するとこんな感じになります。}}ここで注意なのは,matchはnullになる可能性があるので,nullチェックをしてArgumentNullExceptionをスローさせるのが大事です。nullチェック⇒例外スローの流れはよくあるので覚えておきましょう。要素削除メソッドの作成インデックス検索メソッドを実装できたので要素削除メソッドを作成できます。RemoveAt最初はRemoveAtメソッドの実装が良いです。RemoveメソッドはRemoveAtを基に実装されるからです。方法としては,indexの範囲チェックをしてから,削除されて空く場所に対して_array内の要素を一つずつ前にずらす感じです。1]に既定値を代入して,末尾の要素の重複を防ぐ必要がある事です。Remove次はRemoveメソッドの実装です。IndexOfでインデックスを検索してそのインデックスでRemoveAtを行うだけです。実装するとこんな感じ。Remove((T)value);}IList<T>.Removeは返り値がboolなので,インデックスが見つかったかどうかで返り値を決定します。RemoveRangeRemoveRangeメソッドでは指定した範囲の要素を一括で削除するメソッドです。実装は引数の範囲チェック要素を削除する分ずらす後ろの余分な要素を削除するCountとversionを調整するといった感じになります。version++;}RemoveAllRemoveAllは引数のPredicate<T>の条件を満たす要素を全て削除するというものです。このメソッドは多少複雑ですが,RemoveAtとFindIndexを組み合わせれば実装可能です。手順は,引数のPredicate<T>のnullチェックFindIndexで条件を満たす要素のインデックスを求めるRemoveAtでその要素を削除するFindIndexの結果が-1になるまで繰り返す削除した要素数を返すといった感じになります。version変数はRemoveAtを実行するたびに変化しているので変える必要はありません。count;}Clear次は全ての要素を削除するClearメソッドです。このメソッドはfor文を回すだけでとても簡単に実装可能です。次は要素を直接検索するメソッドを作成します。Find,FindLastFindメソッドでは引数Predicate<T>を満たす要素のうち先頭の物を返す,FindIndexの要素版です。FindIndexを実装しているので,それを使えばすぐ実装できます。FindLastも同じ要領で実装できます。_array[index];}FindAllFindAllメソッドでは引数のPredicate<T>を満たす要素を全て,新しいList<T>のメソッドに追加して返します。こちらでは新しいList<T>インスタンスを生成して,条件を満たす要素をAddしていく方針で実装します。list;}存在の有無を表すメソッドの作成ここでは指定した条件の有無を返すメソッドを実装していきます。ContainsContainsメソッドでは指定した要素が存在するかどうかを返します。実装はIndexOf(またはLastIndexOf)を使えばすぐできます。IndexOfの結果が-1の時は存在しないのでfalse,それ以外でtrueなので1行ですね。false;}ExistsExistsメソッドでは引数のPredicate<T>を満たす要素がList<T>インスタンス内に存在するかどうかを返します。こちらもFindIndex(またはFindLastIndex)ですぐに実装できます。-1;TrueForAllTrueForAllでは,List<T>インスタンス内の全ての要素が引数のPredicate<T>を満たすかどうかを返します。Existより条件を厳しくした感じです。全て一致という形なのでFindIndexを使った横着はできない感じですね。黙ってfor文を回していきましょう。true;}要素をコピーするメソッドの作成CopyToCopyToメソッドでは指定した配列にList<T>のインスタンスの要素をコピーするものです。オーバーロードによってコピー範囲が以下のように異なります。index実装時は例外チェックを行た後,for文で代入していくか,Array.Copyメソッドでコピーしていきます。}}GetRangeGetRangeメソッドではList<T>の指定範囲の要素を新しいList<T>インスタンスにコピーして返します。実装はCopyToとほぼ同じで,for文を回すかArray.Copyメソッドで写します。TValue>の配列を持っていて,インデックスを検索するメソッドを使用しながら実装しています。Enrtyやらで頭から白煙が上がり悟りを開きそうになります。Stack<T>とQueue<T\>ではかなり似通ったメソッドを持っていますが,Queueの方がフィールドが多くコードが複雑です。ISet<T>ではメソッドの実装にポインタを使っていたりするので闇です。一回実装しようとして挫折した記憶があります...基本的に非ジェネリックコレクションのインターフェイスによって実装が強制されているプロパティやメソッドは,明示的実装によって隠蔽されていることが多いです。明示的実装によってキャスティングしないと使用できないので,objectのキャストミスや返り値の違いなどあるため使いづらいメソッドを隠す事が出来るのです。ジェネリックコレクションを自作するときはこれらのコードのうち自分が実装したいものに近いものを参考にしていくといいと思います。キーが2つのディクショナリなど,複数のジェネリックが関わるコレクションを実装するときは,それらをまとめる構造体やインターフェイスの自作するという事も視野に入れてもいいかもしれません。最後に長々と語ってしまいましたが,これを読めばジェネリックコレクションを自作するノウハウがある程度身に付いたと思います(多分)。C#のプログラミングライフを自分で充実させるために新しいジェネリックコレクションを生み出して使ってみましょう。ジェネリックコレクションを作らないにしても,インデクサやインターフェイスの仕様なども解説していったので他の事項に関しても役に立つんじゃなかなぁと思います。