定义泛型类
要创建泛型类,只需在类定义中包含尖括号语法:
class MyGenericClass<T>
{
...
}
其中T可以是任意标识符,只要遵循通常的C#命名规则即可,例如,不以数字开头等。但一般只使用T。泛型类可以在其定义中包含任意多个类型参数,它们用逗号分隔开,例如:
class MyGenericClass<T1, T2, T3>
{
...
}
定义了这些类型后,就可以在类定义中像使用其他类型那样使用它们。可以把它们用作成员变量的类型、属性或方法等成员的返回类型以及方法的参数类型等。例如:
class MyGenericClass<T1, T2, T3>
{
private T1 innerT1Object;
public MyGenericClass(T1 item)
{
innerT1Object = item;
}
public T1 InnerT1Object
{
get
{
return innerT1Object;
}
}
}
其中,类型T1的对象可以传递给构造函数,只能通过InnerT1Object属性对这个对象进行只读访问。注意,不能假定为类提供了什么类型。例如,下面的代码就不会编译:
class MyGenericClass<T1, T2, T3>
{
private T1 innerT1Object;
public MyGenericClass()
{
innerT1Object = new T1();
}
public T1 InnerT1Object
{
get
{
return innerT1Object;
}
}
}
我们不知道T1是什么,也就不能使用它的构造函数,它甚至可能没有构造函数,或者没有可公共访问的默认构造函数。如果不使用涉及本节后面介绍的高级技术的复杂代码,则只能对T1进行如下假设:可以把它看成继承自System.Object的类型或可以封箱到System.Object中的类型。
显然,这意味着不能对这个类型的实例进行非常有趣的操作,或者对为MyGenericClass泛型类提供的其他类型进行有趣的操作。不使用反射(这是用于在运行期间检查类型的高级技术,本章不介绍它),就只能使用下面的代码:
public string GetAllTypesAsString()
{
return "T1 = " + typeof(T1).ToString()
+ ", T2 = " + typeof(T2).ToString()
+ ", T3 = " + typeof(T3).ToString();
}
可以做一些其他工作,尤其是对集合进行操作,因为处理对象组是非常简单的,不需要对对象类型进行任何假设,这是为什么存在本章前面介绍的泛型集合类的一个原因。
另一个需要注意的限制是,在比较为泛型类型提供的类型值和null
时,只能使用运算符==
和!=
。例如,下面的代码会正常工作:
public bool Compare(T1 op1, T1 op2)
{
if(op1 != null && op2 != null)
{
return true;
}
else
{
return false;
}
}
其中,如果T1是一个值类型,则总是假定它是非空的,于是在上面的代码中,Compare总是返回true。但是,下面试图比较两个实参op1和op2的代码将不能编译:
public bool Compare(T1 op1, T1 op2)
{
if(op1 == op2) // ❌
{
return true;
}
else
{
return false;
}
}
其原因是这段代码假定T1支持==
运算符。这说明,要对泛型进行实际的操作,需要更多地了解类中使用的类型。
1. default关键字
要确定用于创建泛型类实例的类型,需要了解一个最基本的情况:它们是引用类型还是值类型。若不知道这个情况,就不能用下面的代码赋予null
值:
public MyGenericClass()
{
innerT1Object = null;
}
如果T1是值类型,则innerT1Object不能取null
值,所以这段代码不会编译。幸好,开发人员考虑到了这个问题,使用default关键字(本书前面在switch结构中使用过它)的新用法解决了它。该新用法如下:
public MyGenericClass()
{
innerT1Object = default(T1);
}
其结果是,如果innerT1Object是引用类型,就给它赋予null
值;如果它是值类型,就给它赋予默认值。对于数字类型,这个默认值是0;而结构根据其各个成员的类型,以相同的方式初始化为0或null
。default关键字允许对必须使用的类型执行更多操作,但为了更进一步,还需要限制所提供的类型。
2. 约束类型
前面用于泛型类的类型称为无绑定(unbounded)类型,因为没有对它们进行任何约束。而通过约束(constraining)类型,可以限制可用于实例化泛型类的类型,这有许多方式。例如,可以把类型限制为继承自某个类型。回顾前面使用的Animal、Cow和Chicken类,你可以把一个类型限制为Animal或继承自Animal,则下面的代码是正确的:
MyGenericClass<Cow> = new MyGenericClass<Cow>();
但下面的代码不能编译:
MyGenericClass<string> = new MyGenericClass<string>();
在类定义中,这可以使用where
关键字来实现:
class MyGenericClass<T> where T: constraint
{
...
}
其中constraint定义了约束。可以用这种方式提供许多约束,各个约束之间用逗号分开:
class MyGenericClass<T> where T: constraint1, constraint2
{
...
}
还可以使用多个where语句,定义泛型类需要的任意类型或所有类型上的约束:
class MyGenericClass<T1, T2> where T1: constraint1 where T2 : constraint2
{
...
}
约束必须出现在继承说明符的后面:
class MyGenericClass<T1, T2> : MyBaseClass, IMyInterface
where T1: constraint1 where T2: constraint2
{
...
}
表12-5中列出了一些可用的约束。
表12-5 泛型类型约束
约束 | 定义 | 用法示例 |
---|---|---|
struct | 类型必须是值类型 | 在类中,需要值类型才能起作用,例如,T类型的成员变量是0,表示某种含义 |
class | 类型必须是引用类型 | 在类中,需要引用类型才能起作用,例如,T类型的成员变量是null,表示某种含义 |
base-class | 类型必须是基类或继承自基类。可以给这个约束提供任意类名 | 在类中,需要接口公开的某种基本功能,才能起作用 |
interface | 类型必须是接口或实现了接口 | 在类中,需要接口公开的某种基本功能,才能起作用 |
new() | 类型必须有一个公共的无参构造函数 | 在类中,需要能实例化T类型的变量,例如在构造函数中实例化 |
如果new()用作约束,它就必须是为类型指定的最后一个约束。
可以通过base-class约束,把一个类型参数用作另一个类型参数的约束,如下所示:
class MyGenericClass<T1, T2> where T2 : T1
{
...
}
其中,T2必须与T1的类型相同,或者继承自T1。这称为裸类型约束(naked type constraint),表示一个泛型类型参数用作另一个类型参数的约束。
类型约束不能循环,例如:
class MyGenericClass<T1, T2> where T2 : T1 where T1 : T2
{
...
}
这段代码不能编译。下面的示例将定义和使用一个泛型类,该类使用前面几章介绍的Animal类系列。
public abstract class Animal
{
...
public abstract void MakeANoise();
}
public class Chicken : Animal
{
...
public override void MakeANoise()
{
Console.WriteLine("{0} says says 'cluck!';", name);
}
}
public class Cow : Animal
{
...
public override void MakeANoise()
{
Console.WriteLinie("{0} says 'moo!'", name);
}
}
public class SuperCow : Cow
{
public void Fly()
{
Console.WriteLine("{0} is flying!", name);
}
public SuperCow(string newName) : base(newName)
{
}
public override void MakeANoise()
{
Console.WriteLine(
"{0} says 'here I come to save the day!'", name);
}
}
新添加一个新类Farm,并修改Farm.cs中的代码,如下所示:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Ch12Ex04
{
public class Farm<T> : IEnumerable<T>
where T : Animal
{
private List<T> animals = new List<T>();
public List<T> Animals
{
get
{
return animals;
}
}
public IEnumerator<T> GetEnumerator()
{
return animals.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return animals.GetEnumerator();
}
public void MakeNoises()
{
foreach(T animal in animals)
{
animal.MakeANoise();
}
}
public void FeedTheAnimals()
{
foreach(T animal in animals)
{
animal.Feed();
}
}
public Farm<Cow> GetCows()
{
Farm<Cow> cowFarm = new Farm<Cow>();
foreach(T animal in animals)
{
if(animal is Cow)
{
cowFarm.Animals.Add(animal as Cow)
}
}
return cowFarm;
}
}
}
修改Program.cs,如下所示:
static void Main(string[] args)
{
Farm<Animal> farm = new Farm<Animal>();
farm.Animals.Add(new Cow("Jack"));
farm.Animals.Add(new Chicken("Vera"));
farm.Animals.Add(new Chicken("Sally"));
farm.Animals.Add(new SuperCow("Kevin"));
farm.MakeNoises();
Farm<Cow> dairyFarm = farm.GetCows();
dairyFarm.FeedTheAnimals();
foreach(Cow cow in dairyFarm)
{
if(cow is SuperCow)
{
(cow as SuperCow).Fly();
}
}
Console.ReadKey();
}
示例的说明
在这个示例中,创建了一个泛型类
Farm<T>
,它没有继承泛型List类,而将泛型List类作为公共属性公开,该List的类型由传送给Farm<T>
的类型参数T确定,且被约束为Animals,或者继承自Animal。
public class Farm<T> : IEnumerable<T>
where T : Animal
{
private List<T> animals = new List<T>();
public List<T> Animals
{
get
{
return animals;
}
}
Farm<T>
还实现了IEnumerable<T>
,其中,T传递给这个泛型接口,因此也以相同的方式进行了约束。实现这个接口,就可以迭代包含在Farm<T>
中的项,而不必显式迭代Farm<T>.Animals
。很容易就能做到这一点,只需返回Animals公开的枚举器即可,该枚举器是一个List<T>
类,也实现了IEnumerable<T>
。
public IEnumerator<T> GetEnumerator() { return animals.GetEnumerator(); }
因为
IEnumerable<T>
继承自IEnumerable,所以还需要实现IEnuerable.GetEnumerator()
:
IEnumerator IEnumerable.GetEnumerator() { return animals.GetEnumerator(); }
之后,
Farm<T>
包含的两个方法利用了抽象类Animal的方法:
public void MakeNoises()
{
foreach(T animal in animals)
{
animal.MakeANoise();
}
}
public void FeedTheAnimals()
{
foreach(T animal in animals)
{
animal.Feed();
}
}
T被约束为Animal,所以这段代码会正确编译---无论T是什么,都可以访问MakeNoise()和Feed()方法。
下一个方法GetCows()更加有趣。这个方法提取了集合类型为Cow(或继承自Cow,例如,新的SuperCow类)的所有项:
public Farm<Cow> GetCows() { Farm<Cow> cowFarm = new Farm<Cow>(); foreach(T animal in animals) { if(animal is Cow) { cowFarm.Animals.Add(animal as Cow) } } return cowFarm; }
有趣的是,这个方法似乎有点浪费。如果以后希望有同一系列的其他方法,如GetChickens(),也需要显式实现它们。在使用许多类型的系统中,需要更多方法。一个较好的解决方案是使用泛型方法,详见本章后面的内容。
Program.cs中的客户代码测试了Form的各个方法,它包含的代码大多已在前面列出,所以不需要深入探讨这些代码。
3. 从泛型类中继承
上例中的Farm<T>
类以及本章节前面介绍的其他几个类都继承自一个泛型类型。在Farm<T>
中,这个类型是一个接口IEnumerable<T>
。这里Farm<T>
在T上提供的约束也会在IEnumerable<T>
中使用的T上添加一个额外的约束。这可以用于限制未约束的类型,但需要遵循一些规则。
首先,如果某个类型所继承的基类型中受到了约束,该类型就不能“解除约束”。也就是说,类型T在所继承的基类型中使用时,该类型必须受到至少与基类型相同的约束。例如,下面的代码是正确的:
class SuperFarm<T> : Farm<T>
where T : SuperCow
{
}
因为T在Farm<T>
中被约束为Animal,把它约束为SuperCow,就是把T约束为这些值的一个子集,所以这是可行的。但是,以下代码不会编译:
class SuperFarm<T> : Farm<T>
where T : struct // ❌
{
}
可以肯定地讲,提供给SuperFarm<T>
的类型T不能转换为可由Farm<T>
使用的T,所以代码不会编译。
甚至对于约束为超集的情况,也会出现相同的问题:
class SuperFarm<T> : Farm<T>
where T : class
{
}
即使SuperFarm<T>
允许有像Animal这样的类型,Farm<T>
中也不允许有满足类约束的其他类型。否则编译就会失败。这个规则适用于本章前面介绍的所有约束类型。
另外,如果继承了一个泛型类型,就必须提供所有必须的类型信息,这可以使用其他泛型类型参数的形式上提供,如上所述,也可以显式提供。这也适用于继承了泛型类型的非泛型类。例如:
public class Cards : List<Card>, ICloneable
{
}
这是可行的,但下面的代码会失败:
public class Cards : List<T>, ICloneable
{
}
因为没有提供T的信息,所以无法编译。
如果给泛型类型提供了参数,例如,上面的List
,就可以称该类型是“关闭的”。同样,继承List ,就是继承一个“打开”的泛型类型。
4. 泛型运算符
在C#中,可以像其他方法一样进行运算符的重写,这也可以在泛型类中实现此类重写。例如,可在Farm
public static implicit operator List<Animal>(Farm<T> farm)
{
List<Animal> result = new List<Animal>();
foreach(T animal in farm)
{
result.Add(animal);
}
return result;
}
这样,如有必要,就可以在Farm<T>
中把Animal对象直接作为List<Animal>
来访问。例如,使用下面的运算符添加两个Farm<T>
实例,这是很方便的:
public static Farm<T> operator + (Farm<T> farm1, List<T> farm2)
{
Farm<T> result = new Farm<T>();
foreach(T animal in farm1)
{
result.Animals.Add(animal);
}
foreach(T animal in farm2)
{
if(!result.Animals.Contains(animal))
{
result.Animals.Add(animal);
}
}
return result;
}
public static Farm<T> operator + (List<T> farm1, Farm<T> farm2)
{
return farm2 + farm1;
}
接着可以添加Farm
Farm<Animal> newFarm = farm + dairyFarm;
在这行代码中,dairyFarm(是Farm<Cow>
的实例)隐式转换为List<Animal>
,List<Animal>
可以在Farm<T>
中由重载运算符+
使用。
读者可能认为,使用下面的代码也可以做到这一点:
public static Farm<T> operator + (Farm<T> farm1, Farm<T> farm2)
{
...
}
但是,Farm<Cow>
不能转换为Farm<Animal>
,所以汇总会失败。为了更进一步,可以使用下面的转换运算符来解决这个问题:
public static implicit operator Farm<Animal>(Farm<T> farm)
{
Farm<Animal> result = new Farm<Animal>();
foreach(T animal in farm)
{
result.Animals.Add(animal);
}
return result;
}
使用这个运算符,Farm<T>
的实例(如Farm<Cow>
)就可以转换为Farm<Animal>
的实例,这解决了上面的问题。所以,可以使用上面列出的两种方法中的一种,但是后者更适合,因为它比较简单。
5. 泛型结构
前几章说过,结构实际上与类相同,只有一些微小的区别,而且结构是值类型,不是引用类型。所以,可以用与泛型类相同的方式来创建泛型结构。例如:
public struct MyStruct<T1, T2>
{
public T1 item1;
public T2 item2;
}
🔚