ジェネリクス
最終更新:
atachi
ジェネリクス
ジェネリクスとは特定の型をタイプセーフに受け渡しができる関数やクラスの定義方法です。
似た書式と目的にC++言語のテンプレートがありますが、C#のジェネリクスは実装方法と概念が根本的に異なります。
ジェネリクス関数
型引数に与えられる実際の型は、コンパイル時に決定する。
public static T Max<T>(T a, T b)
where T : IComparable //型引数はIComparable型であることを制約している
{
return a.CompareTo(b) > 0 ? a : b;
}
//呼び出しは次の通り
int n1 = Max<int>(5,10); // 型を明示的に指定(Javaと同じ呼び出し方法。安全。)
int n2 = Max(5,10); // 引数の型から、int型のジェネリクスを暗黙に推移。
double d1 = Max(5.0,10.0); // 引数の型から、double型
string str = Max("abc","ABC"); //
ジェネリクスクラス
Javaのジェネリクスと同じ。
// generics 版スタッククラス
class Stack<T>
{
T[] buf;
int top;
public Stack(int max) {
this.buf = new T[max];
this.top = 0;
}
public void Push(T val) {
this.buf[this.top++] = val;
}
public Type Pop() {
return this.buf[--this.top];
}
public int Size {
get
{
return this.top;
}
}
public int MaxSize{
get
{
return this.buf.Length;
}
}
}
// ジェネリクスクラスのインスタンス作成
Stack<int> si = new Stack<int>(10);
Stack<string> ss = new Stack<string>(5);
型の制約
public static T Max<T>(T a, T b)
where T : IComparable //型引数はIComparable型であることを制約している
{
return a.CompareTo(b) > 0 ? a : b;
}
この場合、型引数T はIComparableを実装する型であるという制約を設けています。
T は IComparableであることが保証されているため、IComparable.CompareTo()が呼び出される。
共変性・反変性のサポート
C#4からはジェネリクスに共変性・反変性が実装された。
「配列→ジェネリックコレクション」への変換
string[]やMyClass[]のような配列を、ジェネリックなコレクションへ変換することはできません。
代わりに、ジェネリックなコレクションを作成し、そこに要素を追加していく処理となります。
ICollection<T>は配列をコレクションに追加するようなメソッドは用意されていません。よって、foreach構文を使って配列を逐次コレクションに代入する処理を行う必要があります。
下記のサンプルでは、foreach構文を使う代わりにArrayが持つサポートメソッドを使っています。
Array.ConvertAllとラムダ式を使うことで、「配列→ジェネリックコレクション」への変換をワンライナーで記述できます。
MyClass[] myclasses = ...; // MyClassの配列
ICollection<MyClass> c = Array.ConvertAll(myclasses, prop => (MyClass)prop);
IDisposableの問題
ジェネリックなクラスを実装する際に、パラメータTのインスタンスを作成する必要があるクラスを実装する場合に、IDisposableインターフェースが実装されている場合に問題が発生します。
public class MyClass<T> where T : new() {
public void SomeMethod() {
// Tのインスタンスを作成する
T obj = new T();
obj.Method();
}
}
上記はジェネリクスクラスであること以外は一般的なコードです。
しかし、ジェネリクスクラスであるが故の問題がはらんでいます。上記のクラスはパラメータTに任意のクラスを指定することができますが、パラメータTがIDisposableを実装している場合にこのコードはリソースリークを起こします。IDisposableインターフェースを実装したクラスは必ずDispose()を呼び出さなければならないためです。
ジェネリッククラス内で、パラメータTのインスタンスを作成する必要がある場合には下記のようにusing構文を使用すべきです。
public class MyClass<T> where T : new() {
public void SomeMethod() {
// Tのインスタンスを作成する
T obj = new T();
using(obj as IDisposable){
obj.Method();
}
}
}
ラムダ式
C#ではラムダ式を使用することで、ロジックの表現を簡略化できる。
(引数リスト) => {式}; または、式が単文の場合はパーレンを省略できる。 (引数リスト) => 式;
delegate bool Pred(int n);
Pred p = delegate (int n) {
return n > 0;
}
// 上記のような匿名メソッドは、ラムダ式で次のように表現できる。
p += (int n) => { return n > 0; }
p += (int n) => n > 0;
// p の型がわかっているので、
// エンティティの引数のシグネチャから型の指定を省略できる
p += n => n > 0;
オブジェクトのシリアライズ
XMLへのシリアライズ
C#2以降ではジェネリクスを駆使してXMLへのシリアライズ・デシリアライズというパターンをタイプセーフに実装できます。
このテクニックの利点は、シリアライズ対象のオブジェクトにシリアライズのためのコードが不必要な点です。
public static XmlPersistenceManager<T> {
public static T LoadFromFile(string filePath) {
if( File.Exists(filePath) ) {
using( XmlReader is = XmlReader.Create(filePath) ) {
return ReadFromStream(is);
}
}
return default(r);
}
public static void SaveToFile(string filePath,T data) {
using( XmlWriter writer = XmlWriter.Create(filePath) ) {
AddToStream(writer, data);
}
}
public static T ReadFromStream(XmlReader is) {
if( factory == null ) {
factory = new XmlSerializer( typeof(T) );
}
T r = (T)factory.Deserialize(is); // 「as T」は使えない
return r;
}
public static void AddToStream(XmlWriter os,T data) {
if( factory == null ) {
factory = new XmlSerializer( typeof(T) );
}
factory.Serialize(os, data);
}
#region Fields
private static XmlSerializer factory;
#endregion Fields
}