泛型类(C# 编程指南)
泛型类封装不特定于特定数据类型的操作。 泛型类最常见用法是用于链接列表、哈希表、堆栈、队列和树等集合。 无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。
对于大多数需要集合类的方案,推荐做法是使用 .NET 类库中提供的集合类。 有关使用这些类的详细信息,请参阅 .NET 中的泛型集合。
通常,创建泛型类是从现有具体类开始,然后每次逐个将类型更改为类型参数,直到泛化和可用性达到最佳平衡。 创建自己的泛型类时,需要考虑以下重要注意事项:
-
要将哪些类型泛化为类型参数。
通常,可参数化的类型越多,代码就越灵活、其可重用性就越高。 但过度泛化会造成其他开发人员难以阅读或理解代码。
-
要将何种约束(如有)应用到类型参数(请参阅类型参数的约束)。
其中一个有用的规则是,应用最大程度的约束,同时仍可处理必须处理的类型。 例如,如果知道泛型类仅用于引用类型,则请应用类约束。 这可防止将类意外用于值类型,并使你可在
T
上使用as
运算符和检查null
值。 -
是否将泛型行为分解为基类和子类。
因为泛型类可用作基类,所以非泛型类的相同设计注意事项在此也适用。 请参阅本主题后文有关从泛型基类继承的规则。
-
实现一个泛型接口还是多个泛型接口。
例如,如果要设计用于在基于泛型的集合中创建项的类,则可能必须实现一个接口,例如
IComparable<T>
,其中T
为类的类型。
有关简单泛型类的示例,请参阅泛型介绍。
类型参数和约束的规则对于泛型类行为具有多种含义,尤其是在继承性和成员可访问性方面。 应当了解一些术语,然后再继续。 对于泛型类 Node<T>
,客户端代码可通过指定类型参数来引用类,创建封闭式构造类型 (Node<int>
)。 或者,可以不指定类型参数(例如指定泛型基类时),创建开放式构造类型 (Node<T>
)。 泛型类可继承自具体的封闭式构造或开放式构造基类:
class BaseNode { }
class BaseNodeGeneric<T> { }
// 具体类型
class NodeConcrete<T> : BaseNode { }
//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }
//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }
非泛型类(即,具体类)可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数,因为运行时客户端代码无法提供实例化基类所需的类型参数。
//No error
class Node1 : BaseNodeGeneric<int> { }
//Generates an error
//class Node2 : BaseNodeGeneric<T> {}
//Generates an error
//class Node3 : T {}
继承自开放式构造类型的泛型类必须对非此继承类共享的任何基类类型参数提供类型参数,如下方代码所示:
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> {}
继承自开放式构造类型的泛型类必须指定作为基类型上约束超集或表示这些约束的约束:
class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }
泛型类型可使用多个类型参数和约束,如下所示:
class SuperKeyType<K, V, U>
where U : System.IComparable<U>
where V : new()
{ }
开放式构造和封闭式构造类型可用作方法参数:
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
类库定义多个泛型接口,以将其用于 System.Collections.Generic
命名空间中的集合类。
接口被指定为类型参数上的约束时,仅可使用实现接口的类型。 如下代码示例演示一个派生自 GenericList<T>
类的 SortedList<T>
类。 有关详细信息,请参阅泛型介绍。 SortedList<T>
添加约束 where T : IComparable<T>
。 这可使 SortedList<T>
中的 BubbleSort
方法在列表元素上使用泛型 CompareTo
方法。 在此示例中,列表元素是一个实现 IComparable<Person>
的简单类 Person
。
//在尖括号中键入参数T
public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{
protected Node head;
protected Node current = null;
//嵌套类在T上也是通用的
protected class Node
{
public Node next;
private T data; //T作为私有成员数据类型
public Node(T t) //T用于非泛型构造函数
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
public T Data //T作为返回类型的属性
{
get { return data; }
set { data = value; }
}
}
public GenericList() //构造函数
{
head = null;
}
public void AddHead(T t) //T作为方法参数类型
{
Node n = new Node(t);
n.Next = head;
head = n;
}
//迭代器的实现
public System.Collections.Generic.IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
//IEnumerable <T>继承自IEnumerable,因此该类必须实现GetEnumerator的泛型和非泛型版本。 在大多数情况下,非泛型方法可以简单地调用泛型方法。
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class SortedList<T> : GenericList<T> where T : System.IComparable<T>
{
//一种简单的,未经优化的排序算法,可以从最低到最高对列表元素进行排序:
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)
{
//因为我们需要调用此方法,所以SortedList类受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);
}
}
//一个简单的类,它使用自身作为类型参数来实现IComparable<T>。这是存储在通用列表中的对象中的常见设计模式。
public class Person : System.IComparable<Person>
{
string name;
int age;
public Person(string s, int i)
{
name = s;
age = i;
}
//这将导致列表元素按年龄值排序
public int CompareTo(Person p)
{
return age - p.age;
}
public override string ToString()
{
return name + ":" + age;
}
// 必须实现Equals
public bool Equals(Person p)
{
return (this.age == p.age);
}
}
class Program
{
static void Main()
{
//声明并实例化一个新的通用SortedList类。
//Person是类型参数。
SortedList<Person> list = new SortedList<Person>();
//创建名称和年龄值以初始化Person对象
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 };
//填充列表
for (int x = 0; x < 10; x++)
{
list.AddHead(new Person(names[x], ages[x]));
}
//打印未排序的列表
foreach (Person p in list)
{
System.Console.WriteLine(p.ToString());
}
System.Console.WriteLine("Done with unsorted list");
//对列表进行排序
list.BubbleSort();
//打印出排序列表
foreach (Person p in list)
{
System.Console.WriteLine(p.ToString());
}
System.Console.WriteLine("Done with sorted list");
}
}
可将多个接口指定为单个类型上的约束,如下所示:
class Stack<T> where T : System.IComparable<T>, IEnumerable<T>
{
}
一个接口可定义多个类型参数,如下所示:
interface IDictionary<K, V>
{
}
适用于类的继承规则也适用于接口:
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
的返回值和 Current
属性 Getter
中仅使用 T
。
具体类可实现封闭式构造接口,如下所示:
interface IBaseInterface<T> { }
class SampleClass : IBaseInterface<string> { }
只要类形参列表提供接口所需的所有实参,泛型类即可实现泛型接口或封闭式构造接口,如下所示:
interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }
class SampleClass1<T> : IBaseInterface1<T> { } //No error
class SampleClass2<T> : IBaseInterface2<T, string> { } //No error
控制方法重载的规则对泛型类、泛型结构或泛型接口内的方法一样。 有关详细信息,请参阅泛型方法。
泛型方法(C# 编程指南)
泛型方法是通过类型参数声明的方法,如下所示:
static void Swap<T>(ref T lhs, ref T rhs)
{
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}
如下示例演示使用类型参数的 int
调用方法的一种方式:
public static void TestSwap()
{
int a = 1;
int b = 2;
Swap<int>(ref a, ref b);
System.Console.WriteLine(a + " " + b);
}
还可省略类型参数,编译器将推断类型参数。 如下 Swap
调用等效于之前的调用:
Swap(ref a, ref b);
类型推理的相同规则适用于静态方法和实例方法。 编译器可基于传入的方法参数推断类型参数;而无法仅根据约束或返回值推断类型参数。 因此,类型推理不适用于不具有参数的方法。 类型推理发生在编译时,之后编译器尝试解析重载的方法签名。 编译器将类型推理逻辑应用于共用同一名称的所有泛型方法。 在重载解决方案步骤中,编译器仅包含在其上类型推理成功的泛型方法。
在泛型类中,非泛型方法可访问类级别类型参数,如下所示:
class SampleClass<T>
{
void Swap(ref T lhs, ref T rhs) { }
}
如果定义一个具有与包含类相同的类型参数的泛型方法,则编译器会生成警告 CS0693
,因为在该方法范围内,向内 T
提供的参数会隐藏向外 T
提供的参数。 如果需要使用类型参数(而不是类实例化时提供的参数)调用泛型类方法所具备的灵活性,请考虑为此方法的类型参数提供另一标识符,如下方示例中 GenericList2<T>
所示。
class GenericList<T>
{
// CS0693
void SampleMethod<T>() { }
}
class GenericList2<T>
{
//No warning
void SampleMethod<U>() { }
}
使用约束在方法中的类型参数上实现更多专用操作。 此版 Swap<T>
现名为 SwapIfGreater<T>
,仅可用于实现 IComparable<T>
的类型参数。
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;
}
}
泛型方法可重载在数个泛型参数上。 例如,以下方法可全部位于同一类中:
void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }