『究極のC#プログラミング』 Part2 Chapter2 ジェネリック

ジェネリックC# 3.0らしいプログラミングを支える基礎
ジェネリックを使えば、コレクションの要素のアクセス時にキャストが要らなくなる

キャストの難点

1.面倒

var list = new ArrayList();
// ...
Console.WriteLine( (string)list[0] ); // ArrayListだといちいちキャストが要る


2. 重い
キャストは重いという難点もある。
ちなみにasを使うと高速になるが、変換できない時にそのまま例外を投げてくれるキャストと比べて使いにくい。


3. コンパイル時にチェックされず、実行時にチェックされる
これの何が問題かというと、
A. テスト実行時にすべての行が実行されるとは限らず、実行されなかった行のキャストの妥当性チェックはされない
B. 実行時にありうる全ての値に対してキャストが妥当かがチェックされない
の2点である。Aはテスト支援ツールでカバレッジ率(網羅率)100%を目指せば解決できるかもしれないが、Bは難しい。
大きなプロジェクトほどこれは大問題となる。


ちなみに、C++ではtemplateを使って解決できた。このtemplateに似たものがC#3.0のジェネリックである
ジェネリックのおかげで、コレクションクラスのキャストが要らなくなった

var list = new List<string>;

かっこで指定した型は、その型だけでなく、その型を継承した型も受け入れる
インターフェースを指定することもでき、その場合、そのインターフェースを実装した型を受け入れる

コレクションの種類

従来のコレクション ジェネリックコレクション
ArrayList List
HashTable Dictionary
Queue Queue
SortedList SortedList
Stack Stack

Queue, SortedList, Stackは同名だが、名前空間が異なる、まったく異なるクラスである。

LinkedList

インデックスによるアクセスができず、先頭または末尾から順次たどる必要がある
要素の挿入と削除が高速(桁違いの速度)
Listに比べてLinkedListは数倍のメモリを消費する

※データの入れ物となるLinkedListNodeクラスも意識する必要があり、ややコーディングが面倒にはなる
その代わり、LinkedListNodeインスタンスはLinkedListへの脱着が容易であり、
リストをばらして再構築するような処理の効率が上がる

SortedDictionary

SortedListをLinkedListみたいにしたものと考えるとよい


ソートされてないデータへの挿入と削除が高速
SortedListに比べてSortedDictionaryはメモリを消費する
※ソートされたデータを一度に取り出す時はSortedListのほうが高速

ジェネリックメソッドと型推論


ジェネリックはクラス以外にも、デリゲートやメソッドに使用できる
メソッドの典型例が、Array.Sortである


int[]な配列をソートする時、

Array.Sort<int>(array)

Array.Sort(array)

と略せる。型推論が働くからである。
これは一見すると従来の非ジェネリックなSortメソッドのように見えるが、
ildasm(VS付属の逆アセンブラ)や.NET Reflectorで調べると、ジェネリック版のArray.Sortが呼ばれているのが分かる。


型推論がうまく働かないかもしれない場合
IComparableなクラスAを継承したクラスBがあって、CompareToメソッドの戻り値がAとBで逆符号である時に、
B[]な配列arrayを

Array.Sort<A>

でソートするのと、

Array.Sort<B>

でソートするのとでは、逆の結果になる。
そして、

Array.Sort(array)

のようにカッコ無しにすると、

Array.Sort<B>(array)

の方が呼ばれる。

HashtableとDictionaryの非互換性

前者は存在しないキーにアクセスするとnullを返すが、後者はSystem.Collections.Generic.KeyNotFoundException例外を投げる
nullが意味を持つ場合(nullable型とか)もあるので、後者のほうが合理的と言える


キーがコレクションに含まれてるかは、ContainsKeyメソッドを、
それと同時に値の取得も行うには、TryGetValueメソッドを使う

if( dictionary1.TryGetValue("hoge", out value) )
{
  // valueを使った処理
}

例外を使ってキーの存在判定する手もあるが、例外は重いので、上記の2つのメソッドを使おう。

ジェネリッククラスの自作

コレクションにはジェネリックが有効だが、それ以外のクラスやメソッドに使うことはあまりない
ジェネリックは「どのような型でも受け入れる」というパワフルなものなので、設計や実装が一筋縄でいかないからだ

制約つきのジェネリッククラス

任意の型を受け入れるクラスはパワフル過ぎるので、制約をつける

public class MyClass<T>
  where T : IDisposable, new()  // 制約リスト。Tは、IDisposabelを実装していて、引数無しのコンストラクタを持つ型。
{
  // new T()やDisposeメソッドを使ったコード
}

C++のtemplateとの違い

C#ジェネリックは、コンパイル時には1つのジェネリックに対して1つのクラスしか実行ファイルに書かれない。
実行時に、ジェネリック型のインスタンス化と言って、具体的な型に合った実行コードが生成される。
(ただし、参照型に対してはたった1つの実体が生成されるだけ)


よって、C++のtemplateと比べて
・実行ファイルのサイズが小さくなる
・実行時に必要なメモリが小さくなる
というメリットがある。