C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

6.4 泛型接口

使用泛型可以定义接口,在接口中定义的方法可以带泛型参数。在链表的示例中,就实现了IEnumerable<out T>接口,它定义了GetEnumerator()方法,以返回IEnumerator<out T>。.NET为不同的情况提供了许多泛型接口,例如,IComparable<T>、ICollection<T>和IExtensibleObject<T>。同一个接口常常存在比较老的非泛型版本,例如,.NET 1.0有基于对象的IComparable接口。IComparable<in T>基于一个泛型类型:

        public interface IComparable<in T>
        {
          int CompareTo(T other);
        }

注意:不要混淆用于泛型参数的in和out关键字。参见“协变和抗变”一节。

比较老的非泛型接口IComparable需要一个带CompareTo()方法的对象。这需要强制转换为特定的类型,例如,Person类要使用LastName属性,就需要使用CompareTo()方法:

        public class Person: IComparable
        {
          public int CompareTo(object obj)
          {
            Person other = obj as Person;
            return this.lastname.CompareTo(other.LastName);
          }
          //

实现泛型版本时,不再需要将object的类型强制转换为Person:

        public class Person: IComparable<Person>
        {
          public int CompareTo(Person other) => LastName.CompareTo(other.LastName);
          //...

6.4.1 协变和抗变

在.NET 4之前,泛型接口是不变的。.NET 4通过协变和抗变为泛型接口和泛型委托添加了一个重要的扩展。协变和抗变指对参数和返回值的类型进行转换。例如,可以给一个需要Shape参数的方法传送Rectangle参数吗?下面用示例说明这些扩展的优点。

在.NET中,参数类型是协变的。假定有Shape和Rectangle类,Rectangle派生自Shape基类。声明Display()方法是为了接受Shape类型的对象作为其参数:

        public void Display(Shape o) { }

现在可以传递派生自Shape基类的任意对象。因为Rectangle派生自Shape,所以Rectangle满足Shape的所有要求,编译器接受这个方法调用:

        var r = new Rectangle { Width= 5, Height=2.5 };
        Display(r);

方法的返回类型是抗变的。当方法返回一个Shape时,不能把它赋予Rectangle,因为Shape不一定总是Rectangle。反过来是可行的:如果一个方法像GetRectangle()方法那样返回一个Rectangle,

        public Rectangle GetRectangle();

就可以把结果赋予某个Shape:

        Shape s = GetRectangle();

在.NET Framework 4版本之前,这种行为方式不适用于泛型。自C# 4以后,扩展后的语言支持泛型接口和泛型委托的协变和抗变。下面开始定义Shape基类和Rectangle类(代码文件Variance/Shape.cs和Rectangle.cs):

        public class Shape
        {
          public double Width { get; set; }
          public double Height { get; set; }
          public override string ToString() => $"Width: {Width}, Height: {Height}";
        }
        public class Rectangle: Shape
        {
        }

6.4.2 泛型接口的协变

如果泛型类型用out关键字标注,泛型接口就是协变的。这也意味着返回类型只能是T。接口IIndex与类型T是协变的,并从一个只读索引器中返回这个类型(代码文件Variance/IIndex.cs):

          public interface IIndex<out T>
          {
            T this[int index] { get; }
            int Count { get; }
          }

IIndex<T>接口用RectangleCollection类来实现。RectangleCollection类为泛型类型T定义了Rectangle:

注意:如果对接口IIndex使用了读写索引器,就把泛型类型T传递给方法,并从方法中检索这个类型。这不能通过协变来实现——泛型类型必须定义为不变的。不使用out和in标注,就可以把类型定义为不变的(代码文件Variance/RectangleCollection)。

                public class RectangleCollection: IIndex<Rectangle>
                {
                  private Rectangle[] data = new Rectangle[3]
                  {
                    new Rectangle { Height=2, Width=5 },
                    new Rectangle { Height=3, Width=7 },
                    new Rectangle { Height=4.5, Width=2.9 }
                  };
                  private static RectangleCollection _coll;
                  public static RectangleCollection GetRectangles() =>
                    _coll ? ? (coll = new RectangleCollection());
                  public Rectangle this[int index]
                  {
                    get
                    {
                      if (index < 0 || index > data.Length)
                      throw new ArgumentOutOfRangeException("index");
                      return data[index];
                    }
                  }
                  public int Count => data.Length;
                }

注意:RectangleCollection.GetRectangles()方法使用了本章后面将会介绍的合并运算符(coalescing operator)。如果变量col1为null,那么将会调用运算符的右侧,以创建RectangleCollection的一个新实例,并将其赋给变量col1。之后,会从GetRectangles()方法中返回变量col1。这个运算符详见第8章。

RectangleCollection.GetRectangle()方法返回一个实现IIndex<Rectangle>接口的RectangleCollection类,所以可以把返回值赋予IIndex<Rectangle>类型的变量rectangle。因为接口是协变的,所以也可以把返回值赋予IIndex<Shape>类型的变量。Shape不需要Rectangle没有提供的内容。使用shapes变量,就可以在for循环中使用接口中的索引器和Count属性(代码文件Variance/Program.cs):

        public static void Main()
        {
          IIndex<Rectangle> rectangles = RectangleCollection.GetRectangles();
          IIndex<Shape> shapes = rectangles;
          for (int i = 0; i < shapes.Count; i++)
          {
            WriteLine(shapes[i]);
          }
        }

6.4.3 泛型接口的抗变

如果泛型类型用in关键字标注,泛型接口就是抗变的。这样,接口只能把泛型类型T用作其方法的输入(代码文件Variance/IDisplay.cs):

        public interface IDisplay<in T>
        {
          void Show(T item);
        }

ShapeDisplay类实现IDisplay<Shape>,并使用Shape对象作为输入参数(代码文件Variance/ShapeDisplay.cs):

        public class ShapeDisplay: IDisplay<Shape>
        {
          public void Show(Shape s) =>
            WriteLine($"{s.GetType().Name} Width: {s.Width}, Height: {s.Height}");
        }

创建ShapeDisplay的一个新实例,会返回IDisplay<Shape>,并把它赋予shapeDisplay变量。因为IDisplay<T>是抗变的,所以可以把结果赋予IDisplay<Rectangle>,其中Rectangle派生自Shape。这次接口的方法只能把泛型类型定义为输入,而Rectangle满足Shape的所有要求(代码文件Variance/Program.cs):

        public static void Main()
        {
          //...
          IDisplay<Shape> shapeDisplay = new ShapeDisplay();
          IDisplay<Rectangle> rectangleDisplay = shapeDisplay;
          rectangleDisplay.Show(rectangles[0]);
        }