定义泛型类

  要创建泛型类,只需在类定义中包含尖括号语法:

  1. class MyGenericClass<T>
  2. {
  3. ...
  4. }

  其中T可以是任意标识符,只要遵循通常的C#命名规则即可,例如,不以数字开头等。但一般只使用T。泛型类可以在其定义中包含任意多个类型参数,它们用逗号分隔开,例如:

  1. class MyGenericClass<T1, T2, T3>
  2. {
  3. ...
  4. }

  定义了这些类型后,就可以在类定义中像使用其他类型那样使用它们。可以把它们用作成员变量的类型、属性或方法等成员的返回类型以及方法的参数类型等。例如:

  1. class MyGenericClass<T1, T2, T3>
  2. {
  3. private T1 innerT1Object;
  4. public MyGenericClass(T1 item)
  5. {
  6. innerT1Object = item;
  7. }
  8. public T1 InnerT1Object
  9. {
  10. get
  11. {
  12. return innerT1Object;
  13. }
  14. }
  15. }

  其中,类型T1的对象可以传递给构造函数,只能通过InnerT1Object属性对这个对象进行只读访问。注意,不能假定为类提供了什么类型。例如,下面的代码就不会编译:

  1. class MyGenericClass<T1, T2, T3>
  2. {
  3. private T1 innerT1Object;
  4. public MyGenericClass()
  5. {
  6. innerT1Object = new T1();
  7. }
  8. public T1 InnerT1Object
  9. {
  10. get
  11. {
  12. return innerT1Object;
  13. }
  14. }
  15. }

  我们不知道T1是什么,也就不能使用它的构造函数,它甚至可能没有构造函数,或者没有可公共访问的默认构造函数。如果不使用涉及本节后面介绍的高级技术的复杂代码,则只能对T1进行如下假设:可以把它看成继承自System.Object的类型或可以封箱到System.Object中的类型。

  显然,这意味着不能对这个类型的实例进行非常有趣的操作,或者对为MyGenericClass泛型类提供的其他类型进行有趣的操作。不使用反射(这是用于在运行期间检查类型的高级技术,本章不介绍它),就只能使用下面的代码:

  1. public string GetAllTypesAsString()
  2. {
  3. return "T1 = " + typeof(T1).ToString()
  4. + ", T2 = " + typeof(T2).ToString()
  5. + ", T3 = " + typeof(T3).ToString();
  6. }

  可以做一些其他工作,尤其是对集合进行操作,因为处理对象组是非常简单的,不需要对对象类型进行任何假设,这是为什么存在本章前面介绍的泛型集合类的一个原因。

  另一个需要注意的限制是,在比较为泛型类型提供的类型值和null,只能使用运算符==!=。例如,下面的代码会正常工作:

  1. public bool Compare(T1 op1, T1 op2)
  2. {
  3. if(op1 != null && op2 != null)
  4. {
  5. return true;
  6. }
  7. else
  8. {
  9. return false;
  10. }
  11. }

  其中,如果T1是一个值类型,则总是假定它是非空的,于是在上面的代码中,Compare总是返回true。但是,下面试图比较两个实参op1和op2的代码将不能编译:

  1. public bool Compare(T1 op1, T1 op2)
  2. {
  3. if(op1 == op2) // ❌
  4. {
  5. return true;
  6. }
  7. else
  8. {
  9. return false;
  10. }
  11. }

  其原因是这段代码假定T1支持==运算符。这说明,要对泛型进行实际的操作,需要更多地了解类中使用的类型。

  1. default关键字

  要确定用于创建泛型类实例的类型,需要了解一个最基本的情况:它们是引用类型还是值类型。若不知道这个情况,就不能用下面的代码赋予null值:

  1. public MyGenericClass()
  2. {
  3. innerT1Object = null;
  4. }

  如果T1是值类型,则innerT1Object不能取null值,所以这段代码不会编译。幸好,开发人员考虑到了这个问题,使用default关键字(本书前面在switch结构中使用过它)的新用法解决了它。该新用法如下:

  1. public MyGenericClass()
  2. {
  3. innerT1Object = default(T1);
  4. }

  其结果是,如果innerT1Object是引用类型,就给它赋予null值;如果它是值类型,就给它赋予默认值。对于数字类型,这个默认值是0;而结构根据其各个成员的类型,以相同的方式初始化为0或null。default关键字允许对必须使用的类型执行更多操作,但为了更进一步,还需要限制所提供的类型。

  2. 约束类型

  前面用于泛型类的类型称为无绑定(unbounded)类型,因为没有对它们进行任何约束。而通过约束(constraining)类型,可以限制可用于实例化泛型类的类型,这有许多方式。例如,可以把类型限制为继承自某个类型。回顾前面使用的Animal、Cow和Chicken类,你可以把一个类型限制为Animal或继承自Animal,则下面的代码是正确的:

  1. MyGenericClass<Cow> = new MyGenericClass<Cow>();

  但下面的代码不能编译:

  1. MyGenericClass<string> = new MyGenericClass<string>();

  在类定义中,这可以使用where关键字来实现:

  1. class MyGenericClass<T> where T: constraint
  2. {
  3. ...
  4. }

  其中constraint定义了约束。可以用这种方式提供许多约束,各个约束之间用逗号分开:

  1. class MyGenericClass<T> where T: constraint1, constraint2
  2. {
  3. ...
  4. }

  还可以使用多个where语句,定义泛型类需要的任意类型或所有类型上的约束:

  1. class MyGenericClass<T1, T2> where T1: constraint1 where T2 : constraint2
  2. {
  3. ...
  4. }

  约束必须出现在继承说明符的后面:

  1. class MyGenericClass<T1, T2> : MyBaseClass, IMyInterface
  2. where T1: constraint1 where T2: constraint2
  3. {
  4. ...
  5. }

  表12-5中列出了一些可用的约束。

表12-5 泛型类型约束

约束定义用法示例
struct类型必须是值类型在类中,需要值类型才能起作用,例如,T类型的成员变量是0,表示某种含义
class类型必须是引用类型在类中,需要引用类型才能起作用,例如,T类型的成员变量是null,表示某种含义
base-class类型必须是基类或继承自基类。可以给这个约束提供任意类名在类中,需要接口公开的某种基本功能,才能起作用
interface类型必须是接口或实现了接口在类中,需要接口公开的某种基本功能,才能起作用
new()类型必须有一个公共的无参构造函数在类中,需要能实例化T类型的变量,例如在构造函数中实例化
  如果new()用作约束,它就必须是为类型指定的最后一个约束。

  可以通过base-class约束,把一个类型参数用作另一个类型参数的约束,如下所示:

  1. class MyGenericClass<T1, T2> where T2 : T1
  2. {
  3. ...
  4. }

  其中,T2必须与T1的类型相同,或者继承自T1。这称为裸类型约束(naked type constraint),表示一个泛型类型参数用作另一个类型参数的约束。

  类型约束不能循环,例如:

  1. class MyGenericClass<T1, T2> where T2 : T1 where T1 : T2
  2. {
  3. ...
  4. }

  这段代码不能编译。下面的示例将定义和使用一个泛型类,该类使用前面几章介绍的Animal类系列。

  1. public abstract class Animal
  2. {
  3. ...
  4. public abstract void MakeANoise();
  5. }
  6. public class Chicken : Animal
  7. {
  8. ...
  9. public override void MakeANoise()
  10. {
  11. Console.WriteLine("{0} says says 'cluck!';", name);
  12. }
  13. }
  14. public class Cow : Animal
  15. {
  16. ...
  17. public override void MakeANoise()
  18. {
  19. Console.WriteLinie("{0} says 'moo!'", name);
  20. }
  21. }
  22. public class SuperCow : Cow
  23. {
  24. public void Fly()
  25. {
  26. Console.WriteLine("{0} is flying!", name);
  27. }
  28. public SuperCow(string newName) : base(newName)
  29. {
  30. }
  31. public override void MakeANoise()
  32. {
  33. Console.WriteLine(
  34. "{0} says 'here I come to save the day!'", name);
  35. }
  36. }

  新添加一个新类Farm,并修改Farm.cs中的代码,如下所示:

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading.Tasks;
  7. namespace Ch12Ex04
  8. {
  9. public class Farm<T> : IEnumerable<T>
  10. where T : Animal
  11. {
  12. private List<T> animals = new List<T>();
  13. public List<T> Animals
  14. {
  15. get
  16. {
  17. return animals;
  18. }
  19. }
  20. public IEnumerator<T> GetEnumerator()
  21. {
  22. return animals.GetEnumerator();
  23. }
  24. IEnumerator IEnumerable.GetEnumerator()
  25. {
  26. return animals.GetEnumerator();
  27. }
  28. public void MakeNoises()
  29. {
  30. foreach(T animal in animals)
  31. {
  32. animal.MakeANoise();
  33. }
  34. }
  35. public void FeedTheAnimals()
  36. {
  37. foreach(T animal in animals)
  38. {
  39. animal.Feed();
  40. }
  41. }
  42. public Farm<Cow> GetCows()
  43. {
  44. Farm<Cow> cowFarm = new Farm<Cow>();
  45. foreach(T animal in animals)
  46. {
  47. if(animal is Cow)
  48. {
  49. cowFarm.Animals.Add(animal as Cow)
  50. }
  51. }
  52. return cowFarm;
  53. }
  54. }
  55. }

  修改Program.cs,如下所示:

  1. static void Main(string[] args)
  2. {
  3. Farm<Animal> farm = new Farm<Animal>();
  4. farm.Animals.Add(new Cow("Jack"));
  5. farm.Animals.Add(new Chicken("Vera"));
  6. farm.Animals.Add(new Chicken("Sally"));
  7. farm.Animals.Add(new SuperCow("Kevin"));
  8. farm.MakeNoises();
  9. Farm<Cow> dairyFarm = farm.GetCows();
  10. dairyFarm.FeedTheAnimals();
  11. foreach(Cow cow in dairyFarm)
  12. {
  13. if(cow is SuperCow)
  14. {
  15. (cow as SuperCow).Fly();
  16. }
  17. }
  18. Console.ReadKey();
  19. }
  示例的说明  在这个示例中,创建了一个泛型类Farm<T>,它没有继承泛型List类,而将泛型List类作为公共属性公开,该List的类型由传送给Farm<T>的类型参数T确定,且被约束为Animals,或者继承自Animal。```csharp public class Farm : IEnumerable where T : Animal { private List animals = new List();
  1. public List<T> Animals
  2. {
  3. get
  4. {
  5. return animals;
  6. }
  7. }
  1. >&emsp;&emsp;`Farm<T>`还实现了`IEnumerable<T>`,其中,T传递给这个泛型接口,因此也以相同的方式进行了约束。实现这个接口,就可以迭代包含在`Farm<T>`中的项,而不必显式迭代`Farm<T>.Animals`。很容易就能做到这一点,只需返回Animals公开的枚举器即可,该枚举器是一个`List<T>`类,也实现了`IEnumerable<T>`
  2. >```csharp
  3. public IEnumerator<T> GetEnumerator()
  4. {
  5. return animals.GetEnumerator();
  6. }
  因为IEnumerable<T>继承自IEnumerable,所以还需要实现IEnuerable.GetEnumerator()

  1. IEnumerator IEnumerable.GetEnumerator()
    {
    return animals.GetEnumerator();
    }


  之后,Farm<T>包含的两个方法利用了抽象类Animal的方法:```csharp public void MakeNoises() { foreach(T animal in animals) { animal.MakeANoise(); } }
  1. public void FeedTheAnimals()
  2. {
  3. foreach(T animal in animals)
  4. {
  5. animal.Feed();
  6. }
  7. }
  1. >&emsp;&emsp;T被约束为Animal,所以这段代码会正确编译---无论T是什么,都可以访问MakeNoise()和Feed()方法。
  2. >&emsp;&emsp;下一个方法GetCows()更加有趣。这个方法提取了集合类型为Cow(或继承自Cow,例如,新的SuperCow类)的所有项:
  3. >```csharp
  4. public Farm<Cow> GetCows()
  5. {
  6. Farm<Cow> cowFarm = new Farm<Cow>();
  7. foreach(T animal in animals)
  8. {
  9. if(animal is Cow)
  10. {
  11. cowFarm.Animals.Add(animal as Cow)
  12. }
  13. }
  14. return cowFarm;
  15. }
  有趣的是,这个方法似乎有点浪费。如果以后希望有同一系列的其他方法,如GetChickens(),也需要显式实现它们。在使用许多类型的系统中,需要更多方法。一个较好的解决方案是使用泛型方法,详见本章后面的内容。  Program.cs中的客户代码测试了Form的各个方法,它包含的代码大多已在前面列出,所以不需要深入探讨这些代码。

  3. 从泛型类中继承

  上例中的Farm<T>类以及本章节前面介绍的其他几个类都继承自一个泛型类型。在Farm<T>中,这个类型是一个接口IEnumerable<T>。这里Farm<T>在T上提供的约束也会在IEnumerable<T>中使用的T上添加一个额外的约束。这可以用于限制未约束的类型,但需要遵循一些规则。

  首先,如果某个类型所继承的基类型中受到了约束,该类型就不能“解除约束”。也就是说,类型T在所继承的基类型中使用时,该类型必须受到至少与基类型相同的约束。例如,下面的代码是正确的:

  1. class SuperFarm<T> : Farm<T>
  2. where T : SuperCow
  3. {
  4. }

  因为T在Farm<T>中被约束为Animal,把它约束为SuperCow,就是把T约束为这些值的一个子集,所以这是可行的。但是,以下代码不会编译:

  1. class SuperFarm<T> : Farm<T>
  2. where T : struct // ❌
  3. {
  4. }

  可以肯定地讲,提供给SuperFarm<T>的类型T不能转换为可由Farm<T>使用的T,所以代码不会编译。

  甚至对于约束为超集的情况,也会出现相同的问题:

  1. class SuperFarm<T> : Farm<T>
  2. where T : class
  3. {
  4. }

  即使SuperFarm<T>允许有像Animal这样的类型,Farm<T>中也不允许有满足类约束的其他类型。否则编译就会失败。这个规则适用于本章前面介绍的所有约束类型。

  另外,如果继承了一个泛型类型,就必须提供所有必须的类型信息,这可以使用其他泛型类型参数的形式上提供,如上所述,也可以显式提供。这也适用于继承了泛型类型的非泛型类。例如:

  1. public class Cards : List<Card>, ICloneable
  2. {
  3. }

  这是可行的,但下面的代码会失败:

  1. public class Cards : List<T>, ICloneable
  2. {
  3. }

  因为没有提供T的信息,所以无法编译。

  如果给泛型类型提供了参数,例如,上面的List,就可以称该类型是“关闭的”。同样,继承List,就是继承一个“打开”的泛型类型。

  4. 泛型运算符

  在C#中,可以像其他方法一样进行运算符的重写,这也可以在泛型类中实现此类重写。例如,可在Farm中定义如下隐式的转换运算符:

  1. public static implicit operator List<Animal>(Farm<T> farm)
  2. {
  3. List<Animal> result = new List<Animal>();
  4. foreach(T animal in farm)
  5. {
  6. result.Add(animal);
  7. }
  8. return result;
  9. }

  这样,如有必要,就可以在Farm<T>中把Animal对象直接作为List<Animal>来访问。例如,使用下面的运算符添加两个Farm<T>实例,这是很方便的:

  1. public static Farm<T> operator + (Farm<T> farm1, List<T> farm2)
  2. {
  3. Farm<T> result = new Farm<T>();
  4. foreach(T animal in farm1)
  5. {
  6. result.Animals.Add(animal);
  7. }
  8. foreach(T animal in farm2)
  9. {
  10. if(!result.Animals.Contains(animal))
  11. {
  12. result.Animals.Add(animal);
  13. }
  14. }
  15. return result;
  16. }
  17. public static Farm<T> operator + (List<T> farm1, Farm<T> farm2)
  18. {
  19. return farm2 + farm1;
  20. }

  接着可以添加Farm和Farm的实例,如下所示:

  1. Farm<Animal> newFarm = farm + dairyFarm;

  在这行代码中,dairyFarm(是Farm<Cow>的实例)隐式转换为List<Animal>List<Animal>可以在Farm<T>中由重载运算符+使用。

  读者可能认为,使用下面的代码也可以做到这一点:

  1. public static Farm<T> operator + (Farm<T> farm1, Farm<T> farm2)
  2. {
  3. ...
  4. }

  但是,Farm<Cow>不能转换为Farm<Animal>,所以汇总会失败。为了更进一步,可以使用下面的转换运算符来解决这个问题:

  1. public static implicit operator Farm<Animal>(Farm<T> farm)
  2. {
  3. Farm<Animal> result = new Farm<Animal>();
  4. foreach(T animal in farm)
  5. {
  6. result.Animals.Add(animal);
  7. }
  8. return result;
  9. }

  使用这个运算符,Farm<T>的实例(如Farm<Cow>)就可以转换为Farm<Animal>的实例,这解决了上面的问题。所以,可以使用上面列出的两种方法中的一种,但是后者更适合,因为它比较简单。


  5. 泛型结构

  前几章说过,结构实际上与类相同,只有一些微小的区别,而且结构是值类型,不是引用类型。所以,可以用与泛型类相同的方式来创建泛型结构。例如:

  1. public struct MyStruct<T1, T2>
  2. {
  3. public T1 item1;
  4. public T2 item2;
  5. }