1.
泛型介绍
泛型类和泛型方法同时具备可重用性、类型安全和效率,这是非泛型类和非泛型方法无法具备的。泛型通常用在集合和在集合上运行的方法中。
.NET Framework 2.0
版类库提供一个新的命名空间
System.Collections.Generic
,其中包含几个新的基于泛型的集合类。建议面向
2.0
版的所有应用程序都使用新的泛型集合类,而不要使用旧的非泛型集合类,如
ArrayList
。有关更多信息,请参见
.NET Framework 类库中的泛型(C# 编程指南)
。
当然,也可以创建自定义泛型类型和方法,以提供自己的通用解决方案,设计类型安全的高效模式。下面的代码示例演示一个用于演示用途的简单泛型链接列表类。(大多数情况下,建议使用
.NET Framework
类库提供的
List<T>
类,而不要自行创建类。)在通常使用具体类型来指示列表中所存储项的类型时,可使用类型参数
T
。其使用方法如下:
·
在
AddHead
方法中作为方法参数的类型。
·
在
Node
嵌套类中作为公共方法
GetNext
和
Data
属性的返回类型。
·
在嵌套类中作为私有成员数据的类型。
注意,
T
可用于
Node
嵌套类。如果使用具体类型实例化
GenericList<T>
(例如,作为
GenericList<int>
),则所有的
T
都将被替换为
int
。
// type parameter T in angle brackets
public class GenericList<T>
{
// The nested class is also generic on T
private class Node
{
// T used in non-generic constructor
public Node(T t)
{
next = null;
data = t;
}
private Node next;
public Node Next
{
get { return next; }
set { next = value; }
}
// T as private member data type
private T data;
// T as return type of property
public T Data
{
get { return data; }
set { data = value; }
}
}
private Node head;
// constructor
public GenericList()
{
head = null;
}
// T as method parameter type:
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = head;
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
}
下面的代码示例演示客户端代码如何使用泛型
GenericList<T>
类来创建整数列表。只需更改类型参数,即可方便地修改下面的代码示例,创建字符串或任何其他自定义类型的列表:
class TestGenericList
{
static void Main()
{
// int is the type argument
GenericList<int> list = new GenericList<int>();
for (int x = 0; x < 10; x++)
{
list.AddHead(x);
}
foreach (int i in list)
{
System.Console.Write(i + " ");
}
System.Console.WriteLine("/nDone");
}
}
2.
泛型的优点(
C#
编程指南)
在公共语言运行库和
C#
语言的早期版本中,通用化是通过在类型与通用基类型
Object
之间进行强制转换来实现的,泛型提供了针对这种限制的解决方案。通过创建泛型类,您可以创建一个在编译时类型安全的集合。
使用非泛型集合类的限制可以通过编写一小段程序来演示,该程序利用
.NET Framework
基类库中的
ArrayList
集合类。
ArrayList
是一个使用起来非常方便的集合类,无需进行修改即可用来存储任何引用或值类型。
// The .NET Framework 1.1 way to create a list:
System.Collections.ArrayList list1 = new System.Collections.ArrayList();
list1.Add(3);
list1.Add(105);
System.Collections.ArrayList list2 = new System.Collections.ArrayList();
list2.Add("It is raining in Redmond.");
list2.Add("It is snowing in the mountains.");
但这种方便是需要付出代价的。添加到
ArrayList
中的任何引用或值类型都将隐式地向上强制转换为
Object
。如果项是值类型,则必须在将其添加到列表中时进行装箱操作,在检索时进行取消装箱操作。强制转换以及装箱和取消装箱操作都会降低性能;在必须对大型集合进行循环访问的情况下,装箱和取消装箱的影响非常明显。
另一个限制是缺少编译时类型检查;因为
ArrayList
将把所有项都强制转换为
Object
,所以在编译时无法防止客户端代码执行以下操作:
System.Collections.ArrayList list = new System.Collections.ArrayList();
// Add an integer to the list.
list.Add(3);
// Add a string to the list. This will compile, but may cause an error later.
list.Add("It is raining in Redmond.");
int t = 0;
// This causes an InvalidCastException to be returned.
foreach (int x in list)
{
t += x;
}
尽管将字符串和
ints
组合在一个
ArrayList
中的做法在创建异类集合时是完全合法的,有时是有意图的,但这种做法更可能产生编程错误,并且直到运行时才能检测到此错误。
在
C#
语言的
1.0
和
1.1
版本中,只能通过编写自己的特定于类型的集合来避免
.NET Framework
基类库集合类中的通用代码的危险。当然,由于此类不可对多个数据类型重用,因此将丧失通用化的优点,并且您必须对要存储的每个类型重新编写该类。
ArrayList
和其他相似类真正需要的是:客户端代码基于每个实例指定这些类要使用的具体数据类型的方式。这样将不再需要向上强制转换为
T:System.Object
,同时,也使得编译器可以进行类型检查。换句话说,
ArrayList
需要一个
type parameter
。这正是泛型所能提供的。在
N:System.Collections.Generic
命名空间的泛型
List<T>
集合中,向该集合添加项的操作类似于以下形式:
// The .NET Framework 2.0 way to create a list
List<int> list1 = new List<int>();
// No boxing, no casting:
list1.Add(3);
// Compile-time error:
// list1.Add("It is raining in Redmond.");
对于客户端代码,与
ArrayList
相比,使用
List<T>
时添加的唯一语法是声明和实例化中的类型参数。虽然这稍微增加了些编码的复杂性,但好处是您可以创建一个比
ArrayList
更安全并且速度更快的列表,特别适用于列表项是值类型的情况。
3.
泛型类型参数(
C#
编程指南)
在泛型类型或方法定义中,类型参数是客户端在实例化泛型类型的变量时指定的特定类型的占位符。泛型类(如
泛型介绍(C# 编程指南)
中列出的
GenericList<T>
)不可以像这样使用,因为它实际上并不是一个类型,而更像是一个类型的蓝图。若要使用
GenericList<T>
,客户端代码必须通过指定尖括号中的类型参数来声明和实例化构造类型。此特定类的类型参数可以是编译器识别的任何类型。可以创建任意数目的构造类型实例,每个实例使用不同的类型参数,如下所示:
C#
复制代码
GenericList<float> list1 = new GenericList<float>(); GenericList<ExampleClass> list2 = new GenericList<ExampleClass>(); GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();
在每个
GenericList<T>
实例中,类中出现的每个
T
都会在运行时替换为相应的类型参数。通过这种替换方式,我们使用一个类定义创建了三个独立的类型安全的有效对象。有关
CLR
如何执行此替换的更多信息,请参见
运行库中的泛型(C# 编程指南)
。
· 务必
使用描述性名称命名泛型类型参数,除非单个字母名称完全可以让人了解它表示的含义,而描述性名称不会有更多的意义。
public
interface ISessionChannel<TSession> { /*...*/ }
public
delegate TOutput Converter<TInput, TOutput>(TInput from);
public
class List<T> { /*...*/ }
·
考虑
使用
T
作为具有单个字母类型参数的类型的类型参数名。
public
int IComparer<T>() { return 0; }
public
delegate bool Predicate<T>(T item);
public
struct Nullable<T> where T : struct { /*...*/ }
·
务必
将
“T”
作为描述性类型参数名的前缀。
public
interface ISessionChannel<TSession> { TSession Session { get; } }
·
考虑
在参数名中指示对此类型参数的约束。例如,可以将带有
ISession
约束的参数命名为
TSession
。
4.
类型参数的约束(
C#
编程指南)
在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束。约束是使用
where
上下文关键字指定的。下表列出了六种类型的约束:
约束
|
说明
|
T
:结构
|
类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。有关更多信息,请参见使用可空类型(C# 编程指南)。
|
T
:类
|
类型参数必须是引用类型,包括任何类、接口、委托或数组类型。
|
T
:new()
|
类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。
|
T
:<基类名>
|
类型参数必须是指定的基类或派生自指定的基类。
|
T
:<接口名称>
|
类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。
|
T
:U
|
为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束。
|
如果要检查泛型列表中的某个项以确定它是否有效,或者将它与其他某个项进行比较,则编译器必须在一定程度上保证它需要调用的运算符或方法将受到客户端代码可能指定的任何类型参数的支持。这种保证是通过对泛型类定义应用一个或多个约束获得的。例如,基类约束告诉编译器:仅此类型的对象或从此类型派生的对象才可用作类型参数。一旦编译器有了这个保证,它就能够允许在泛型类中调用该类型的方法。约束是使用上下文关键字
where
应用的。下面的代码示例演示可通过应用基类约束添加到
GenericList<T>
类(在
泛型介绍(C# 编程指南)
中)的功能。
public class Employee
{
private string name;
private int id;
public Employee(string s, int i)
{
name = s;
id = i;
}
public string Name
{
get { return name; }
set { name = value; }
}
public int ID
{
get { return id; }
set { id = value; }
}
}
public class GenericList<T> where T : Employee
{
private class Node
{
private Node next;
private T data;
public Node(T t)
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
public T Data
{
get { return data; }
set { data = value; }
}
}
private Node head;
public GenericList() //constructor
{
head = null;
}
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = head;
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
约束使得泛型类能够使用
Employee.Name
属性,因为类型为
T
的所有项都保证是
Employee
对象或从
Employee
继承的对象。
可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型,如下所示:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
通过约束类型参数,可以增加约束类型及其继承层次结构中的所有类型所支持的允许操作和方法调用的数量。因此,在设计泛型类或方法时,如果要对泛型成员执行除简单赋值之外的任何操作或调用
System.Object
不支持的任何方法,您将需要对该类型参数应用约束。
在应用
where T : class
约束时,建议不要对类型参数使用
==
和
!=
运算符,因为这些运算符仅测试引用同一性而不测试值相等性。即使在用作参数的类型中重载这些运算符也是如此。下面的代码说明了这一点;即使
String
类重载
==
运算符,输出也为
false
。
public static void OpTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s == t);
}
static void Main()
{
string s1 = "foo";
System.Text.StringBuilder sb = new System.Text.StringBuilder("foo");
string s2 = sb.ToString();
OpTest<string>(s1, s2);
}
这种情况的原因在于,编译器在编译时仅知道
T
是引用类型,因此必须使用对所有引用类型都有效的默认运算符。如果需要测试值相等性,建议的方法是同时应用
where T : IComparable<T>
约束,并在将用于构造泛型类的任何类中实现该接口。
· 不能使用
!=
和
==
运算符,因为无法保证具体类型参数能支持这些运算符。
· 可以在它们与
System.Object
之间来回转换,或将它们显式转换为任何接口类型。
C#
复制代码
class
List<T> { void Add<U>(List<U> items) where U : T {/*...*/} }
在上面的示例中,
T
在
Add
方法的上下文中是一个裸类型约束,而在
List
类的上下文中是一个未绑定的类型参数。
裸类型约束还可以在泛型类定义中使用。注意,还必须已经和其他任何类型参数一起在尖括号中声明了裸类型约束:
C#
复制代码
//naked type constraint
public class SampleClass<T, U, V> where T : V { }
泛型类的裸类型约束的作用非常有限,因为编译器除了假设某个裸类型约束派生自
System.Object
以外,不会做其他任何假设。在希望强制两个类型参数之间的继承关系的情况下,可对泛型类使用裸类型约束。
5.
泛型类(
C#
编程指南)
泛型类封装不是特定于具体数据类型的操作。泛型类最常用于集合,如链接列表、哈希表、堆栈、队列、树等,其中,像从集合中添加和移除项这样的操作都以大体上相同的方式执行,与所存储数据的类型无关。
对于大多数需要集合类的方案,推荐的方法是使用
.NET Framework 2.0
类库中所提供的类。有关使用这些类的更多信息,请参见
.NET Framework 类库中的泛型(C# 编程指南)
。
一般情况下,创建泛型类的过程为:从一个现有的具体类开始,逐一将每个类型更改为类型参数,直至达到通用化和可用性的最佳平衡。创建您自己的泛型类时,需要特别注意以下事项:
·
将哪些类型通用化为类型参数。
一般规则是,能够参数化的类型越多,代码就会变得越灵活,重用性就越好。但是,太多的通用化会使其他开发人员难以阅读或理解代码。
一个有用的规则是,应用尽可能最多的约束,但仍使您能够处理需要处理的类型。例如,如果您知道您的泛型类仅用于引用类型,则应用类约束。这可以防止您的类被意外地用于值类型,并允许您对
T
使用
as
运算符以及检查空值。
·
是否将泛型行为分解为基类和子类。
由于泛型类可以作为基类使用,此处适用的设计注意事项与非泛型类相同。有关从泛型基类继承的规则,请参见下面的内容。
·
是否实现一个或多个泛型接口。
例如,如果您设计一个类,该类将用于创建基于泛型的集合中的项,则可能需要实现一个接口,如
IComparable<T>
,其中
T
是您的类的类型。
有关简单泛型类的示例,请参见
泛型介绍(C# 编程指南)
类型参数和约束的规则对于泛型类行为有几方面的含义,特别是关于继承和成员可访问性。请务必先理解一些术语,然后再继续进行。对于泛型类
Node<T>,
,客户端代码可以通过指定类型参数引用该类,以创建封闭式构造类型
(
Node<int>
)
,或者可以让类型参数处于未指定状态(例如在指定泛型基类时)以创建开放式构造类型
(
Node<T>
)
。泛型类可以从具体的、封闭式构造或开放式构造基类继承:
C#
复制代码
class
BaseNode { } class BaseNodeGeneric<T> { } // concrete type class NodeConcrete<T> : BaseNode { } //closed constructed type class NodeClosed<T> : BaseNodeGeneric<int> { } //open constructed type class NodeOpen<T> : BaseNodeGeneric<T> { }
非泛型(具体)类可以从封闭式构造基类继承,但无法从开放式构造类或裸类型参数继承,因为在运行时客户端代码无法提供实例化基类所需的类型变量。
C#
复制代码
//No error
class
Node1 : BaseNodeGeneric<int> { }
//Generates an error
//class Node2 : BaseNodeGeneric<T> {}
//Generates an error
//class Node3 : T {}
从开放式构造类型继承的泛型类必须为任何未被继承类共享的基类类型参数提供类型变量,如以下代码所示:
C#
复制代码
class
BaseNodeMultiple<T, U> { } //No error
class
Node4<T> : BaseNodeMultiple<T, int> { } //No error
class
Node5<T, U> : BaseNodeMultiple<T, U> { } //Generates an error //class Node6<T> : BaseNodeMultiple<T, U> {}
从开放式构造类型继承的泛型类必须指定约束,这些约束是基类型约束的超集或暗示基类型约束:
C#
复制代码
class
NodeItem<T> where T : System.IComparable<T>, new() { }
class
SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }
泛型类型可以使用多个类型参数和约束,如下所示:
C#
复制代码
class
SuperKeyType<K, V, U> where U : System.IComparable<U> where V : new() { }
开放式构造类型和封闭式构造类型可以用作方法参数:
C#
复制代码
void
Swap<T>(List<T> list1, List<T> list2) { //code to swap items } void Swap(List<int> list1, List<int> list2) { //code to swap items }
泛型类是不变的。也就是说,如果输入参数指定
List<BaseClass>
,则当您试图提供
List<DerivedClass>
时,将会发生编译时错误。
泛型接口(
C#
编程指南)
为泛型集合类或表示集合中项的泛型类定义接口通常很有用。对于泛型类,使用泛型接口十分可取,例如使用
IComparable<T>
而不使用
IComparable
,这样可以避免值类型的装箱和取消装箱操作。
.NET Framework 2.0
类库定义了若干新的泛型接口,以用于
System.Collections.Generic
命名空间中新的集合类。
将接口指定为类型参数的约束时,只能使用实现此接口的类型。下面的代码示例显示从
GenericList<T>
类派生的
SortedList<T>
类。有关更多信息,请参见
泛型介绍(C# 编程指南)
。
SortedList<T>
添加了约束
where T : IComparable<T>
。这将使
SortedList<T>
中的
BubbleSort
方法能够对列表元素使用泛型
CompareTo
方法。在此示例中,列表元素为简单类,即实现
IComparable<Person>
的
Person
。
//Type parameter T in angle brackets.
public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{
protected Node head;
protected Node current = null;
// Nested class is also generic on T
protected class Node
{
public Node next;
private T data; //T as private member datatype
public Node(T t) //T used in non-generic constructor
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
public T Data //T as return type of property
{
get { return data; }
set { data = value; }
}
}
public GenericList() //constructor
{
head = null;
}
public void AddHead(T t) //T as method parameter type
{
Node n = new Node(t);
n.Next = head;
head = n;
}
// Implementation of the iterator
public System.Collections.Generic.IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
// IEnumerable<T> inherits from IEnumerable, therefore this class
// must implement both the generic and non-generic versions of
// GetEnumerator. In most cases, the non-generic method can
// simply call the generic method.
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class SortedList<T> : GenericList<T> where T : System.IComparable<T>
{
// A simple, unoptimized sort algorithm that
// orders list elements from lowest to highest:
public void BubbleSort()
{
if (null == head || null == head.Next)
{
return;
}
bool swapped;
do
{
Node previous = null;
Node current = head;
swapped = false;
while (current.next != null)
{
// Because we need to call this method, the SortedList
// class is constrained on IEnumerable<T>
if (current.Data.CompareTo(current.next.Data) > 0)
{
Node tmp = current.next;
current.next = current.next.next;
tmp.next = current;
if (previous == null)
{
head = tmp;
}
else
{
previous.next = tmp;
}
previous = tmp;
swapped = true;
}
else
{
previous = current;
current = current.next;
}
}
} while (swapped);
}
}
// A simple class that implements IComparable<T> using itself as the
// type argument. This is a common design pattern in objects that
// are stored in generic lists.
public class Person : System.IComparable<Person>
{
string name;
int age;
public Person(string s, int i)
{
name = s;
age = i;
}
// This will cause list elements to be sorted on age values.
public int CompareTo(Person p)
{
return age - p.age;
}
public override string ToString()
{
return name + ":" + age;
}
// Must implement Equals.
public bool Equals(Person p)
{
return (this.age == p.age);
}
}
class Program
{
static void Main()
{
//Declare and instantiate a new generic SortedList class.
//Person is the type argument.
SortedList<Person> list = new SortedList<Person>();
//Create name and age values to initialize Person objects.
string[] names = new string[]
{
"Franscoise",
"Bill",
"Li",
"Sandra",
"Gunnar",
"Alok",
"Hiroyuki",
"Maria",
"Alessandro",
"Raul"
};
int[] ages = new int[] { 45, 19, 28, 23, 18, 9, 108, 72, 30, 35 };
//Populate the list.
for (int x = 0; x < 10; x++)
{
list.AddHead(new Person(names[x], ages[x]));
}
//Print out unsorted list.
foreach (Person p in list)
{
System.Console.WriteLine(p.ToString());
}
System.Console.WriteLine("Done with unsorted list");
//Sort the list.
list.BubbleSort();
//Print out sorted list.
foreach (Person p in list)
{
System.Console.WriteLine(p.ToString());
}
System.Console.WriteLine("Done with sorted list");
}
}
可将多重接口指定为单个类型上的约束,如下所示:
C#
复制代码
class
Stack<T> where T : System.IComparable<T>, IEnumerable<T> { }
一个接口可定义多个类型参数,如下所示:
C#
复制代码
interface IDictionary<K, V> { }
类之间的继承规则同样适用于接口:
C#
复制代码
interface IMonth<T> { } interface IJanuary : IMonth<int> { } //No error interface IFebruary<T> : IMonth<int> { } //No error interface IMarch<T> : IMonth<T> { } //No error //interface IApril<T> : IMonth<T, U> {} //Error
如果泛型接口为逆变的,即仅使用其类型参数作为返回值,则此泛型接口可以从非泛型接口继承。在
.NET Framework
类库中,
IEnumerable<T>
从
IEnumerable
继承,因为
IEnumerable<T>
仅在
GetEnumerator
的返回值和当前属性
getter
中使用
T
。
具体类可以实现已关闭的构造接口,如下所示:
C#
复制代码
interface IBaseInterface<T> { }
class
SampleClass : IBaseInterface<string> { }
只要类参数列表提供了接口必需的所有参数,泛型类便可以实现泛型接口或已关闭的构造接口,如下所示:
C#
复制代码
interface IBaseInterface1<T> { } interface IBaseInterface2<T, U> { }
class
SampleClass1<T> : IBaseInterface1<T> { } //No error class SampleClass2<T> : IBaseInterface2<T, string> { } //No error
对于泛型类、泛型结构或泛型接口中的方法,控制方法重载的规则相同。
泛型方法(
C#
编程指南)
泛型方法是使用类型参数声明的方法,如下所示:
C#
复制代码
static
void Swap<T>(ref T lhs, ref T rhs) { T temp; temp = lhs; lhs = rhs; rhs = temp; }
下面的代码示例演示一种使用
int
作为类型参数的方法调用方式:
C#
复制代码
public
static void TestSwap() { int a = 1; int b = 2; Swap<int>(ref a, ref b); System.Console.WriteLine(a + " " + b); }
也可以省略类型参数,编译器将推断出该参数。下面对
Swap
的调用等效于前面的调用:
C#
复制代码
Swap(ref a, ref b);
相同的类型推断规则也适用于静态方法以及实例方法。编译器能够根据传入的方法参数推断类型参数;它无法仅从约束或返回值推断类型参数。因此,类型推断不适用于没有参数的方法。类型推断在编译时、编译器尝试解析任何重载方法签名之前进行。编译器向共享相同名称的所有泛型方法应用类型推断逻辑。在重载解析步骤中,编译器仅包括类型推断取得成功的那些泛型方法。
在泛型类中,非泛型方法可以访问类级别类型参数,如下所示:
C#
复制代码
class
SampleClass<T> { void Swap(ref T lhs, ref T rhs) { } }
如果定义的泛型方法接受与包含类相同的类型参数,编译器将生成警告
CS0693
,因为在方法范围内,为内部
T
提供的参数将隐藏为外部
T
提供的参数。除了类初始化时提供的类型参数之外,如果需要灵活调用具有类型参数的泛型类方法,请考虑为方法的类型参数提供其他标识符,如下面示例中的
GenericList2<T>
所示。
C#
复制代码
class
GenericList<T> { // CS0693 void SampleMethod<T>() { } } class GenericList2<T> { //No warning void SampleMethod<U>() { } }
使用约束对方法中的类型参数启用更专门的操作。此版本的
Swap<T>
现在称为
SwapIfGreater<T>
,它只能与实现
IComparable<T>
的类型参数一起使用。
C#
复制代码
void
SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T> { T temp; if (lhs.CompareTo(rhs) > 0) { temp = lhs; lhs = rhs; rhs = temp; } }
泛型方法可以使用许多类型参数进行重载。例如,下列方法可以全部存在于同一个类中:
C#
复制代码
void
DoWork() { } void DoWork<T>() { } void DoWork<T, U>() { }
泛型和数组(
C#
编程指南)
在
C# 2.0
中,下限为零的一维数组自动实现
IList<T>
。这使您可以创建能够使用相同代码循环访问数组和其他集合类型的泛型方法。此技术主要对读取集合中的数据很有用。
IList<T>
接口不能用于在数组中添加或移除元素;如果试图在此上下文中调用
IList<T>
方法(如数组的
RemoveAt
),将引发异常。
下面的代码示例演示带有
IList<T>
输入参数的单个泛型方法如何同时循环访问列表和数组,本例中为整数数组。
class Program
{
static void Main()
{
int[] arr = { 0, 1, 2, 3, 4 };
List<int> list = new List<int>();
for (int x = 5; x < 10; x++)
{
list.Add(x);
}
ProcessItems<int>(arr);
ProcessItems<int>(list);
}
static void ProcessItems<T>(IList<T> coll)
{
foreach (T item in coll)
{
System.Console.Write(item.ToString() + " ");
}
System.Console.WriteLine();
}
}
|
泛型委托(
C#
编程指南)
委托
可以定义自己的类型参数。引用泛型委托的代码可以指定类型参数以创建已关闭的构造类型,就像实例化泛型类或调用泛型方法一样,如下例所示:
C#
复制代码
public
delegate void Del<T>(T item); public static void Notify(int i) { } Del<int> m1 = new Del<int>(Notify);
C# 2.0
版具有称为方法组转换的新功能,此功能适用于具体委托类型和泛型委托类型,并使您可以使用如下简化的语法写入上一行:
C#
复制代码
Del
<int> m2 = Notify;
在泛型类内部定义的委托使用泛型类类型参数的方式可以与类方法所使用的方式相同。
C#
复制代码
class
Stack<T> { T[] items; int index; public delegate void StackDelegate(T[] items); }
引用委托的代码必须指定包含类的类型变量,如下所示:
C#
复制代码
private
static void DoWork(float[] items) { } public static void TestStack() { Stack<float> s = new Stack<float>(); Stack<float>.StackDelegate d = DoWork; }
delegate void StackEventHandler<T, U>(T sender, U eventArgs);
class Stack<T>
{
public class StackEventArgs : System.EventArgs { }
public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;
protected virtual void OnStackChanged(StackEventArgs a)
{
stackEvent(this, a);
}
}
class SampleClass
{
public void HandleStackChange<T>(Stack<T> stack, Stack<T>.StackEventArgs args) { }
}
public static void Test()
{
Stack<double> s = new Stack<double>();
SampleClass o = new SampleClass();
s.stackEvent += o.HandleStackChange;
}
泛型代码中的默认关键字(
C#
编程
指南)
在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型
T
:
·
T
是引用类型还是值类型。
·
如果
T
为值类型,则它是数值还是结构。
给定参数化类型
T
的一个变量
t
,只有当
T
为引用类型时,语句
t = null
才有效;只有当
T
为数值类型而不是结构时,语句
t = 0
才能正常使用。解决方案是使用
default
关键字,此关键字对于引用类型会返回空,对于数值类型会返回零。对于结构,此关键字将返回初始化为零或空的每个结构成员,具体取决于这些结构是值类型还是引用类型。以下来自
GenericList<T>
类的示例显示了如何使用
default
关键字。有关更多信息,请参见
泛型概述
。
public class GenericList<T>
{
private class Node
{
//...
public Node Next;
public T Data;
}
private Node head;
//...
public T GetNext()
{
T temp = default(T);
Node current = head;
if (current != null)
{
temp = current.Data;
current = current.Next;
}
return temp;
}
}
C++
模板和
C#
泛型之间的区别(
C#
编程指南)
C#
泛型和
C++
模板都是用于提供参数化类型支持的语言功能。然而,这两者之间存在许多差异。在语法层面上,
C#
泛型是实现参数化类型的更简单方法,不具有
C++
模板的复杂性。此外,
C#
并不尝试提供
C++
模板所提供的所有功能。在实现层面,主要区别在于,
C#
泛型类型替换是在运行时执行的,从而为实例化的对象保留了泛型类型信息。有关更多信息,请参见
运行库中的泛型(C# 编程指南)
。
以下是
C#
泛型和
C++
模板之间的主要差异:
·
C#
泛型未提供与
C++
模板相同程度的灵活性。例如,尽管在
C#
泛型类中可以调用用户定义的运算符,但不能调用算术运算符。
·
C#
不允许非类型模板参数,如
template C<int i> {}
。
·
C#
不支持显式专用化,即特定类型的模板的自定义实现。
·
C#
不支持部分专用化:类型参数子集的自定义实现。
·
C#
不允许将类型参数用作泛型类型的基类。
·
C#
不允许类型参数具有默认类型。
·
在
C#
中,尽管构造类型可用作泛型,但泛型类型参数自身不能是泛型。
C++
确实允许模板参数。
·
C++
允许那些可能并非对模板中的所有类型参数都有效的代码,然后将检查该代码中是否有用作类型参数的特定类型。
C#
要求相应地编写类中的代码,使之能够使用任何满足约束的类型。例如,可以在
C++
中编写对类型参数的对象使用算术运算符
+
和
-
的函数,这会在使用不支持这些运算符的类型来实例化模板时产生错误。
C#
不允许这样;唯一允许的语言构造是那些可从约束推导出来的构造。
运行库中的泛型(
C#
编程指南)
将泛型类型或方法编译为
Microsoft
中间语言
(MSIL)
时,它包含将其标识为具有类型参数的元数据。泛型类型的
MSIL
的使用因所提供的类型参数是值类型还是引用类型而不同。
第一次用值类型作为参数来构造泛型类型时,运行库会创建专用泛型类型,将提供的参数代入到
MSIL
中的适当位置。对于每个用作参数的唯一值类型,都会创建一次专用泛型类型。
例如,假设您的程序代码声明了一个由整数构造的堆栈,如下所示:
C#
复制代码
Stack<int> stack;
在此位置,运行库生成
Stack <T>
类的专用版本,并相应地用整数替换其参数。现在,只要程序代码使用整数堆栈,运行库就会重用生成的专用
Stack<T>
类。在下面的示例中,创建了整数堆栈的两个实例,它们共享
Stack<int>
代码的单个实例:
C#
复制代码
Stack<int> stackOne = new Stack<int>(); Stack<int> stackTwo = new Stack<int>();
但是,如果在程序代码中的其他位置创建了另一个
Stack<T>
类,这次使用不同的值类型(如
long
或用户定义的结构)作为其参数,则运行库会生成泛型类型的另一版本(这次将在
MSIL
中的适当位置代入
long
)。由于每个专用泛型类本身就包含值类型,因此不再需要转换。
对于引用类型,泛型的工作方式略有不同。第一次使用任何引用类型构造泛型类型时,运行库会创建专用泛型类型,用对象引用替换
MSIL
中的参数。然后,每次使用引用类型作为参数来实例化构造类型时,无论引用类型的具体类型是什么,运行库都会重用以前创建的泛型类型的专用版本。之所以可以这样,是因为所有引用的大小相同。
例如,假设您有两个引用类型:一个
Customer
类和一个
Order
类,并且进一步假设您创建了一个
Customer
类型的堆栈:
C#
复制代码
class
Customer { } class Order { }
C#
复制代码
Stack<Customer> customers;
在此情况下,运行库生成
Stack<T>
类的一个专用版本,该版本不是存储数据,而是存储稍后将填写的对象引用。假设下一行代码创建另一个引用类型的堆栈,称为
Order
:
C#
复制代码
Stack<Order> orders = new Stack<Order>();
不同于值类型,对于
Order
类型不创建
Stack<T>
类的另一个专用版本。而是创建
Stack<T>
类的一个专用版本实例,并将
orders
变量设置为引用它。假设接下来您遇到一行创建
Customer
类型堆栈的代码:
C#
复制代码
customers = new Stack<Customer>();
与前面使用
Order
类型创建的
Stack<T>
类一样,创建了专用
Stack<T>
类的另一个实例,并且其中所包含的指针被设置为引用
Customer
类型大小的内存区域。因为引用类型的数量会随程序的不同而大幅变化,
C#
泛型实现将编译器为引用类型的泛型类创建的专用类的数量减小到一个,从而大幅减小代码量的增加。
此外,使用类型参数实例化泛型
C#
类时,无论它是值类型还是引用类型,可以在运行时使用反射查询它,并且可以确定它的实际类型和类型参数。
.NET Framework
类库中的泛型(
C#
编程指南)
.NET Framework 2.0
版类库提供一个新的命名空间
System.Collections.Generic
,其中包括几个随时可用的泛型集合类和关联接口。其他命名空间(如
System
)也提供新的泛型接口,如
IComparable<T>
。与早期版本的
.NET Framework
所提供的非泛型集合类相比,这些类和接口更为高效和类型安全。在设计和实现自己的自定义集合类之前,请考虑是否能够使用基类库所提供的类,或是否能从基类库所提供的类派生。
泛型和反射(
C#
编程指南)
在
.NET Framework 2.0
中,
Type
类增添了几个新成员以启用泛型类型的运行时信息。有关如何使用这些方法和属性的更多信息,请参见关于这些类的文档。
System.Reflection.Emit
命名空间还包含支持泛型的新成员。请参见
如何:定义具有反射发出的泛型类型
。
System.Type 成员名称
|
说明
|
IsGenericType
|
如果类型为泛型,则返回 true。
|
返回 Type 对象数组,这些对象表示为构造类型提供的类型变量,或泛型类型定义的类型参数。
| |
返回当前构造类型的基础泛型类型定义。
| |
返回表示当前泛型类型参数约束的 Type 对象的数组。
| |
如果类型或其任意封闭类型或方法包含没有被提供特定类型的类型参数,则返回 true。
| |
获取 GenericParameterAttributes 标志的组合,这些标志描述当前泛型类型参数的特殊约束。
| |
对于表示类型参数的 Type 对象,获取类型参数在声明该类型参数的泛型类型定义或泛型方法定义的类型参数列表中的位置。
| |
获取一个值,该值指示当前 Type 是表示泛型类型定义的类型参数,还是泛型方法定义的类型参数。
| |
获取一个值,该值指示当前 Type 是否表示可以用来构造其他泛型类型的泛型类型定义。如果类型表示泛型类型的定义,则返回 true。
| |
返回定义当前泛型类型参数的泛型方法;如果类型参数不是由泛型方法定义的,则返回空值。
| |
用类型数组的元素替代当前泛型类型定义的类型参数,并返回表示结果构造类型的 Type 对象。
|
System.Reflection.MemberInfo 成员名称
|
说明
|
IsGenericMethod
|
如果方法为泛型,则返回 true。
|
返回 Type 对象数组,这些对象表示构造泛型方法的类型变量,或泛型方法定义的类型参数。
| |
返回当前构造方法的基础泛型方法定义。
| |
如果方法或其任意封闭类型包含没有被提供特定类型的任何类型参数,则返回 true。
| |
如果当前 MethodInfo 表示泛型方法的定义,则返回 true。
| |
用类型数组的元素替代当前泛型方法定义的类型参数,并返回表示结果构造方法的 MethodInfo 对象。
|
泛型和属性(
C#
编程指南)
自定义属性只允许引用开放泛型类型(未提供类型参数的泛型类型)和封闭构造泛型类型(为所有类型参数提供参数)。
下面的示例使用此自定义属性:
C#
复制代码
class
CustomAttribute : System.Attribute { public System.Object info; }
属性可以引用开放式泛型类型:
C#
复制代码
public
class GenericClass1<T> { } [CustomAttribute(info = typeof(GenericClass1<>))] class ClassA { }
使用数目适当的若干个逗号指定多个类型参数。在此示例中,
GenericClass2
有两个类型参数:
C#
复制代码
public
class GenericClass2<T, U> { } [CustomAttribute(info = typeof(GenericClass2<,>))] class ClassB { }
属性可以引用封闭式构造泛型类型:
C#
复制代码
public
class GenericClass3<T, U, V> { } [CustomAttribute(info = typeof(GenericClass3<int, double, string>))] class ClassC { }
引用泛型类型参数的属性将导致编译时错误:
C#
复制代码
//[CustomAttribute(info = typeof(GenericClass3<int, T, string>))] //Error
class ClassD<T> { }
C#
复制代码
//public class CustomAtt<T> : System.Attribute {} //Error
泛型类型中的变化(
C#
编程指南)
在
C#
中添加泛型的一个主要好处是能够使用
System.Collections.Generic
命名空间中的类型,轻松地创建强类型集合。例如,您可以创建一个类型为
List<int>
的变量,编译器将检查对该变量的所有访问,确保只有
ints
添加到该集合中。与
C# 1.0
版中的非类型化集合相比,这是可用性方面的一个很大改进。
遗憾的是强类型集合有自身的缺陷。例如,假设您有一个强类型
List<object>
,您想将
List<int>
中的所有元素追加到
List<object>
中。您可能希望编写如下代码:
C#
List<int> ints = new List<int>();
ints.Add(1);
ints.Add(10);
ints.Add(42);
List<object> objects = new List<object>();
// doesnt compile ints is not a IEnumerable<object>
//objects.AddRange(ints);
在这种情况下,您希望能够将
List<int>
(它同时也是
IEnumerable<int>
)当作
IEnumerable<object>
。这样做看起来似乎很合理,因为
int
可以转换为对象。这与能够将
string[]
当作
object[]
(现在你就可以这样做)非常相似。如果您正面临这种情况,那么您需要一种称为泛型变化的功能,它将泛型类型的一种实例化(在本例中为
IEnumerable<int>
)当成该类型的另一种实例化(在本例中为
IEnumerable<object>
)。
C#
不支持泛型类型的变化,所以当遇到这种情况时,您需要尝试可以用来解决此问题的几种技术,找到一种适合的技术。对于最简单的情况,例如上例中的单个方法
AddRange
,您可以声明一个简单的帮助器方法来为您执行转换。例如,您可以编写如下方法:
// Simple workaround for single method
// Variance in one direction only
public static void Add<S, D>(List<S> source, List<D> destination)
where S : D
{
foreach (S sourceElement in source)
{
destination.Add(sourceElement);
}
}
它使您能够完成以下操作:
C#
复制代码
// does compile
VarianceWorkaround.Add<int, object>(ints, objects);
此示例演示了一种简单的变化解决方法的一些特征。帮助器方法带两个类型参数,分别对应于源和目标,源类型参数
S
有一个约束,即目标类型参数
D
。这意味着读取的
List<>
所包含的元素必须可以转换为插入的
List<>
类型的元素。这使编译器可以强制
int
必须可转换为对象。将类型参数约束为从另一类型参数派生被称为裸类型参数约束。
定义一个方法来解决变化问题不算是一种过于拙劣的方法。遗憾的是变化问题很快就会变得非常复杂。下一级别的复杂性产生在当您想要将一个实例化的接口当作另一个实例化的接口时。例如,您有一个
IEnumerable<int>
,您想将它传递给一个只以
IEnumerable<object>
为参数的方法。同样,这样做也是有一定意义的,因为您可以将
IEnumerable<object>
看作对象的序列,将
IEnumerable<int>
看作
ints
的序列。由于
ints
是对象,因此
ints
的序列应当可以被当作对象序列。例如:
C#
复制代码
static
void PrintObjects(IEnumerable<object> objects)
{
foreach
(object o in objects)
{
Console.WriteLine(o);
}
}
您要调用的方法如下所示:
C#
复制代码
// would like to do this, but cant ...
// ... ints is not an IEnumerable<object>
//PrintObjects(ints);
接口
case
的解决方法是:创建为接口的每个成员执行转换的包装对象。它看起来应如下所示:
C#
// Workaround for interface
// Variance in one direction only so type expressinos are natural
public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source)
where S : D
{
return new EnumerableWrapper<S, D>(source);
}
private class EnumerableWrapper<S, D> : IEnumerable<D>
where S : D
{
它使您能够完成以下操作:
C#
复制代码
PrintObjects(VarianceWorkaround.Convert<int, object>(ints));
同样,请注意包装类和帮助器方法的裸类型参数约束。此系统已经变得相当复杂,但是包装类中的代码非常简单;它只委托给所包装接口的成员,除了简单的类型转换外,不执行其他任何操作。为什么不让编译器允许从
IEnumerable<int>
直接转换为
IEnumerable<object>
呢?
尽管在查看集合的只读视图的情况下,变化是类型安全的,然而在同时涉及读写操作的情况下,变化不是类型安全的。例如,不能用此自动方法处理
IList<>
接口。您仍然可以编写一个帮助器,用类型安全的方式包装
IList<>
上的所有读操作,但是写操作的包装就不能如此简单了。
下面是处理
IList<T>
接口的变化的包装的一部分,它显示在读和写两个方向上的变化所引起的问题。
C#
private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D>
where S : D
{
public ListWrapper(IList<S> source) : base(source)
{
this.source = source;
}
public int IndexOf(D item)
{
if (item is S)
{
return this.source.IndexOf((S) item);
}
else
{
return -1;
}
}
// variance the wrong way ...
// ... can throw exceptions at runtime
public void Insert(int index, D item)
{
if (item is S)
{
this.source.Insert(index, (S)item);
}
else
{
throw new Exception("Invalid type exception");
}
}
包装中的
Insert
方法有一个问题。它将
D
当作参数,但是它必须将
D
插入到
IList<S>
中。由于
D
是
S
的基类型,不是所有的
D
都是
S
,因此
Insert
操作可能会失败。此示例与数组的变化有相似之处。当将对象插入
object[]
时,将执行动态类型检查,因为
object[]
在运行时可能实际为
string[]
。例如:
C#
object[] objects = new string[10];
// no problem, adding a string to a string[]
objects[0] = "hello";
// runtime exception, adding an object to a string[]
objects[1] = new object();
在
IList<>
示例中,当实际类型在运行时与需要的类型不匹配时,可以仅仅引发
Insert
方法的包装。所以,您同样可以想象得到编译器将为程序员自动生成此包装。然而,有时候并不应该执行此策略。
IndexOf
方法在集合中搜索所提供的项,如果找到该项,则返回该项在集合中的索引。然而,如果没有找到该项,
IndexOf
方法将仅仅返回
-1
,而并不引发。这种类型的包装不能由自动生成的包装提供。
到目前为止,我们描述了泛型变化问题的两种最简单的解决方法。然而,变化问题可能变得要多复杂就有多复杂。例如,当您将
List<IEnumerable<int>>
当作
List<IEnumerable<object>>
,或将
List<IEnumerable<IEnumerable<int>>>
当作
List<IEnumerable<IEnumerable<object>>>
时。
生成这些包装以解决代码中的变化问题可能给代码带来巨大的系统开销。同时,它还会带来引用标识问题,因为每个包装的标识都与原始集合的标识不一样,从而会导致很微妙的
Bug
。当使用泛型时,应选择类型实例化,以减少紧密关联的组件之间的不匹配问题。这可能要求在设计代码时做出一些妥协。与往常一样,设计程序时必须权衡相互冲突的要求,在设计过程中应当考虑语言中类型系统具有的约束。
有的类型系统将泛型变化作为语言的首要任务。
Eiffel
是其中一个主要示例。然而,将泛型变化作为类型系统的首要任务会急剧增加
C#
的类型系统的复杂性,即使在不涉及变化的相对简单方案中也是如此。因此,
C#
的设计人员觉得不包括变化才是
C#
的正确选择。
下面是上述示例的完整源代码。
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
static class VarianceWorkaround
{
// Simple workaround for single method
// Variance in one direction only
public static void Add<S, D>(List<S> source, List<D> destination)
where S : D
{
foreach (S sourceElement in source)
{
destination.Add(sourceElement);
}
}
// Workaround for interface
// Variance in one direction only so type expressinos are natural
public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source)
where S : D
{
return new EnumerableWrapper<S, D>(source);
}
private class EnumerableWrapper<S, D> : IEnumerable<D>
where S : D
{
public EnumerableWrapper(IEnumerable<S> source)
{
this.source = source;
}
public IEnumerator<D> GetEnumerator()
{
return new EnumeratorWrapper(this.source.GetEnumerator());
}
IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class EnumeratorWrapper : IEnumerator<D>
{
public EnumeratorWrapper(IEnumerator<S> source)
{
this.source = source;
}
private IEnumerator<S> source;
public D Current
{
get { return this.source.Current; }
}
public void Dispose()
{
this.source.Dispose();
}
object IEnumerator.Current
{
get { return this.source.Current; }
}
public bool MoveNext()
{
return this.source.MoveNext();
}
public void Reset()
{
this.source.Reset();
}
}
private IEnumerable<S> source;
}
// Workaround for interface
// Variance in both directions, causes issues
// similar to existing array variance
public static ICollection<D> Convert<S, D>(ICollection<S> source)
where S : D
{
return new CollectionWrapper<S, D>(source);
}
private class CollectionWrapper<S, D>
: EnumerableWrapper<S, D>, ICollection<D>
where S : D
{
public CollectionWrapper(ICollection<S> source)
: base(source)
{
}
// variance going the wrong way ...
// ... can yield exceptions at runtime
public void Add(D item)
{
if (item is S)
{
this.source.Add((S)item);
}
else
{
throw new Exception(@"Type mismatch exception, due to type hole introduced by variance.");
}
}
public void Clear()
{
this.source.Clear();
}
// variance going the wrong way ...
// ... but the semantics of the method yields reasonable semantics
public bool Contains(D item)
{
if (item is S)
{
return this.source.Contains((S)item);
}
else
{
return false;
}
}
// variance going the right way ...
public void CopyTo(D[] array, int arrayIndex)
{
foreach (S src in this.source)
{
array[arrayIndex++] = src;
}
}
public int Count
{
get { return this.source.Count; }
}
public bool IsReadOnly
{
get { return this.source.IsReadOnly; }
}
// variance going the wrong way ...
// ... but the semantics of the method yields reasonable semantics
public bool Remove(D item)
{
if (item is S)
{
return this.source.Remove((S)item);
}
else
{
return false;
}
}
private ICollection<S> source;
}
// Workaround for interface
// Variance in both directions, causes issues similar to existing array variance
public static IList<D> Convert<S, D>(IList<S> source)
where S : D
{
return new ListWrapper<S, D>(source);
}
private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D>
where S : D
{
public ListWrapper(IList<S> source) : base(source)
{
this.source = source;
}
public int IndexOf(D item)
{
if (item is S)
{
return this.source.IndexOf((S) item);
}
else
{
return -1;
}
}
// variance the wrong way ...
// ... can throw exceptions at runtime
public void Insert(int index, D item)
{
if (item is S)
{
this.source.Insert(index, (S)item);
}
else
{
throw new Exception("Invalid type exception");
}
}
public void RemoveAt(int index)
{
this.source.RemoveAt(index);
}
public D this[int index]
{
get
{
return this.source[index];
}
set
{
if (value is S)
this.source[index] = (S)value;
else
throw new Exception("Invalid type exception.");
}
}
private IList<S> source;
}
}
namespace GenericVariance
{
class Program
{
static void PrintObjects(IEnumerable<object> objects)
{
foreach (object o in objects)
{
Console.WriteLine(o);
}
}
static void AddToObjects(IList<object> objects)
{
// this will fail if the collection provided is a wrapped collection
objects.Add(new object());
}
static void Main(string[] args)
{
List<int> ints = new List<int>();
ints.Add(1);
ints.Add(10);
ints.Add(42);
List<object> objects = new List<object>();
// doesnt compile ints is not a IEnumerable<object>
//objects.AddRange(ints);
// does compile
VarianceWorkaround.Add<int, object>(ints, objects);
// would like to do this, but cant ...
// ... ints is not an IEnumerable<object>
//PrintObjects(ints);
PrintObjects(VarianceWorkaround.Convert<int, object>(ints));
AddToObjects(objects); // this works fine
AddToObjects(VarianceWorkaround.Convert<int, object>(ints));
}
static void ArrayExample()
{
object[] objects = new string[10];
// no problem, adding a string to a string[]
objects[0] = "hello";
// runtime exception, adding an object to a string[]
objects[1] = new object();
}
}
}