C#とCLR学習ノート(7)——汎用と汎用制約
文書ディレクトリ 1汎用概要 1.1意味 1.2汎用型の継承 1.2.1汎用型の継承 1.2.2汎用型の型パラメータの継承 2汎用拘束 2.1コンパイラによる汎用パラメータの検証 2.2汎用制約のタイプ 2.3その他の検証問題 参考文献 1汎用性の概要
1.1意味
汎用型を使用する主な目的は三つあります.(1)コード多重化を実現すること;(2)Object類を使用することを避けること.汎用型クラスをインスタンス化する時、Tの実際のタイプ(タイプ実参)を指定する必要があり、このようにタイプの安全を保証すること;(3)Objectによる箱詰め解体を減らし、性能を高める(原理は以下参照).コンパイラにとって、汎用Tは本質的にタイプパラメータ(Type parameter)であり、パラメータとは特殊なプレースホルダである.汎用型はプログラムセットに定義されているため、コードがILにコンパイルされてプログラムセットに置かれると、Tは依然として存在する.例えば、以下の例である.
ここで、後一重引用符(`)後の数字はタイプパラメータの個数を表します.コード実行時、すなわちJIT段階でILがネイティブ言語に翻訳されると、汎用タイプパラメータTが具体的なタイプに置き換えられます.たとえば、
タイプインスタンスが指定されていない汎用タイプは、インタフェースをインスタンス化できないのと同じオープンタイプであるため、インスタンス化できません.1つのタイプの実パラメータを渡すと、閉じたタイプになり、インスタンス化できます.ケース:
汎用型で最もよく使われる場所は集合クラスです.マイクロソフトは汎用集合を使用することを提案し、非汎用集合を使用することを提案しない.上述したタイプの安全で、性能が高いほか、汎用集合クラスの虚の方法が少なく、実行性能をさらに向上させる.また,汎用集合は一般的により多くの拡張方法を有し,より使いやすい.
1.2汎用性の継承
1.2.1汎用型の継承
汎用クラスは、1つの汎用ベースクラスから派生することができますが、汎用サブクラスは、汎用ベースクラスの汎用タイプを繰り返すか、ベースクラスの汎用タイプを指定する必要があります.たとえば、次のコードについて考察します.
1.2.2汎用型のタイプパラメータの継承
2つの汎用タイプ
タイプパラメータの継承関係は、汎用タイプの継承関係を変更しないため、あるいは、汎用タイプは汎用タイプパラメータの継承関係を破壊する.
より具体的には、指定したタイプの実パラメータは階層に影響しません.
ここからもう一つの話題、すなわちインバータとコヒーレンスを引き出し、詳しくは私のもう一つの文章「C#とCLR学習ノート(6)--コヒーレンスとインバータを簡単に理解する」を参照してください.
2汎用拘束
2.1コンパイラによる汎用パラメータの検証
汎用パラメータTは定義時に指定されていないため、コンパイラはセキュリティのためにコンパイル時に分析し、コードが将来指定可能な任意の汎用タイプの実パラメータに適用されることを保証します.たとえば、次のコードについて考察します.
上記のコードのコンパイルに失敗したのは、すべてのタイプが
だから表面的には、汎用型を使うことはあまりできないようで、
上記のケースを改善すると、スムーズにコンパイルできます.
コンストレイントは、汎用タイプまたは汎用メソッドに適用できます.ベースクラスまたは書き換え/実装されたメソッドが汎用制約を持っている場合、サブクラスまたはそのメソッドは同じ汎用制約を適用する必要があります.例:
2.2汎用制約のタイプ
ぶんかつ
形式
意味
プライマリコンストレイント(最大1つ指定)
Tは値タイプでなければならない(
Tは参照タイプでなければなりません
TはFooクラスから派生しなければならない
マイナーコンストレイント(複数指定可能)
TはIooインタフェースを実現しなければならない
T 1は、汎用型T 2から派生する
コンストラクタコンストレイント
Tは共通の無パラメトリック構造関数を持つ非抽象クラスである.
2.3その他の検証問題
(1)タイプ変換の問題
汎用パラメータは、コンストレイントに合致することを保証するためにタイプ変換されます.できるだけ
(2)汎用タイプをデフォルトにする
(3)2つの汎用パラメータの比較
汎用コンストレイントを指定する必要があります.
(4)汎用型を特定の値タイプに拘束することはできない.1つは、値タイプが暗黙的に密封され、継承されないためである.2つは、このような場合に汎用型を使用すると意味がないためである.
参考文献
[1]『CLR via C#』第4版
1.1意味
汎用型を使用する主な目的は三つあります.(1)コード多重化を実現すること;(2)Object類を使用することを避けること.汎用型クラスをインスタンス化する時、Tの実際のタイプ(タイプ実参)を指定する必要があり、このようにタイプの安全を保証すること;(3)Objectによる箱詰め解体を減らし、性能を高める(原理は以下参照).コンパイラにとって、汎用Tは本質的にタイプパラメータ(Type parameter)であり、パラメータとは特殊なプレースホルダである.汎用型はプログラムセットに定義されているため、コードがILにコンパイルされてプログラムセットに置かれると、Tは依然として存在する.例えば、以下の例である.
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
//
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]
ここで、後一重引用符(`)後の数字はタイプパラメータの個数を表します.コード実行時、すなわちJIT段階でILがネイティブ言語に翻訳されると、汎用タイプパラメータTが具体的なタイプに置き換えられます.たとえば、
List
クラスを使用すると、コード中のTはJIT翻訳後にintタイプになります.そのため、タイプ変換、箱詰め解体は存在しません.パフォーマンスが向上した理由です.タイプインスタンスが指定されていない汎用タイプは、インタフェースをインスタンス化できないのと同じオープンタイプであるため、インスタンス化できません.1つのタイプの実パラメータを渡すと、閉じたタイプになり、インスタンス化できます.ケース:
Object o;
o = Activator.CreateInstance(typeof(List<int>));//OK
o = Activator.CreateInstance(typeof(List<>));// ArgumentException,
汎用型で最もよく使われる場所は集合クラスです.マイクロソフトは汎用集合を使用することを提案し、非汎用集合を使用することを提案しない.上述したタイプの安全で、性能が高いほか、汎用集合クラスの虚の方法が少なく、実行性能をさらに向上させる.また,汎用集合は一般的により多くの拡張方法を有し,より使いやすい.
1.2汎用性の継承
1.2.1汎用型の継承
汎用クラスは、1つの汎用ベースクラスから派生することができますが、汎用サブクラスは、汎用ベースクラスの汎用タイプを繰り返すか、ベースクラスの汎用タイプを指定する必要があります.たとえば、次のコードについて考察します.
public class ChildGeneric : Generic<T>{} //
public class ChildGeneric<T> : Generic<T>{} //
public class ChildGeneric : Generic<int>{} //
1.2.2汎用型のタイプパラメータの継承
2つの汎用タイプ
List
とList
を考察し、そのうちMyChildClass
はMyBaseClass
から派生しているが、List
とList
の間には何の関係があるのだろうか.答えは関係ない.タイプパラメータの継承関係は、汎用タイプの継承関係を変更しないため、あるいは、汎用タイプは汎用タイプパラメータの継承関係を破壊する.
より具体的には、指定したタイプの実パラメータは階層に影響しません.
List
はObject
から派生し、List
とList
はいずれもObject
から派生し、両者は「同輩」である.指定タイプ実パラメータは、JIT時に指定したタイプをTに置き換えるだけであり、この2つのリストは2つの異なるクラスである.ここからもう一つの話題、すなわちインバータとコヒーレンスを引き出し、詳しくは私のもう一つの文章「C#とCLR学習ノート(6)--コヒーレンスとインバータを簡単に理解する」を参照してください.
2汎用拘束
2.1コンパイラによる汎用パラメータの検証
汎用パラメータTは定義時に指定されていないため、コンパイラはセキュリティのためにコンパイル時に分析し、コードが将来指定可能な任意の汎用タイプの実パラメータに適用されることを保証します.たとえば、次のコードについて考察します.
private static T Min<T>(T o1, T o2)
{
if (o1.CompareTo(o2) < 0)
return o1;
return o2;
}
上記のコードのコンパイルに失敗したのは、すべてのタイプが
IComparable
インタフェースを実現しているわけではないため、CompareTo()
メソッドが実行できないリスクがある.だから表面的には、汎用型を使うことはあまりできないようで、
Object
で定義された方法しか使えません.明らかに、実際の状況はそうではありません.汎用制約メカニズムがあるため、汎用を有用にします.上記のケースを改善すると、スムーズにコンパイルできます.
private static T Min<T>(T o1, T o2) where T : IComparable<T>
{
if (o1.CompareTo(o2) < 0)
return o1;
return o2;
}
コンストレイントは、汎用タイプまたは汎用メソッドに適用できます.ベースクラスまたは書き換え/実装されたメソッドが汎用制約を持っている場合、サブクラスまたはそのメソッドは同じ汎用制約を適用する必要があります.例:
public class Generic<T> where T : struct
{}
public class ChildGeneric<T> : Generic<T> //
{}
public class ChildGeneric<T> : Generic<T> where T : struct //
{}
public interface IGeneric2
{
string GetTInfo<T>() where T : struct;
}
class ChildGeneric : IGeneric2
{
public string GetTInfo<T>() where T : class //
{
return "";
}
}
class ChildGeneric : IGeneric2
{
public string GetTInfo<T>() where T : struct//
{
return "";
}
}
2.2汎用制約のタイプ
ぶんかつ
形式
意味
プライマリコンストレイント(最大1つ指定)
where T : struct
Tは値タイプでなければならない(
Nullable
を除き、詳細は参考文献参照)where T : class
Tは参照タイプでなければなりません
where T : Foo
TはFooクラスから派生しなければならない
マイナーコンストレイント(複数指定可能)
where T : IFoo
TはIooインタフェースを実現しなければならない
where T1 : T2
T 1は、汎用型T 2から派生する
コンストラクタコンストレイント
where T : new()
Tは共通の無パラメトリック構造関数を持つ非抽象クラスである.
2.3その他の検証問題
(1)タイプ変換の問題
汎用パラメータは、コンストレイントに合致することを保証するためにタイプ変換されます.できるだけ
as
を使用して変換します.public void DoSomething<T>(T obj)
{
int x = (int)obj; //
int x = (int)(Object)obj; // , InvalidCastException
stirng x = obj as string; //
}
(2)汎用タイプをデフォルトにする
default
キーワードの使用を推奨します.public void DoSomething<T>()
{
T temp = null; // , T : class
T temp = default(T); //
}
(3)2つの汎用パラメータの比較
汎用コンストレイントを指定する必要があります.
public void DoSomething<T>(T o1, T o2)
{
if (o1 == o2){} // , T , == 。
}
(4)汎用型を特定の値タイプに拘束することはできない.1つは、値タイプが暗黙的に密封され、継承されないためである.2つは、このような場合に汎用型を使用すると意味がないためである.
参考文献
[1]『CLR via C#』第4版