C#图解教程 第十七章 泛型

泛型

什么是泛型


到如今为止,全部在类声明中用到的类型都是特定的类型–或是程序员定义的,或是语言或BCL定义的。然而,不少时候,咱们须要把类的行为提取或重构出来,使之不只能用到它们编码的数据类型上,还能应用到其余类型上。
泛型能够作到这一点。咱们重构代码并额外增长一个抽象层,对于这样的代码来讲,数据类型就不用硬编码了。这是专门为多段代码在不一样的数据类型上执行相同指令的状况专门设计的。程序员

听起来比较抽象,下面看一个示例安全

一个栈的示例

假设咱们声明一个MyIntStack类,该类实现一个int类型的栈。它容许int值的压入弹出。函数

class MyIntStack
{
    int StackPointer=0;
    int[] StackArray;
    public void Push(int x)
    {
        ...
    }
    public int Pop()
    {
        ...
    }
}

假设如今但愿将相同的功能应用与float类型的值,能够有几种方式来实现。不用泛型,按照咱们之前的思路产生的代码以下。this

class MyFloatStack
{
    int StackPointer=0;
    float[] StackArray;
    public void Push(float x)
    {
        ...
    }
    public float Pop()
    {
        ...
    }
}

这个方法固然可行,但容易出错且有以下缺点:编码

  • 咱们须要仔细检查类的每部分来看哪些类型的声明须要修改,哪些须要保留
  • 每次须要新类型的栈类时,咱们须要重复该过程
  • 代码冗余
  • 不宜调试和维护

C#中的泛型


泛型(generic)特性提供了一种更优雅的方式,可让多个类型共享一组代码。泛型容许咱们声明类型参数化(type-parameterized)的代码,能够用不一样的类型进行实例化。即咱们能够用“类型占位符”来写代码,而后在建立类的实例时指明真实的类型。
本书读到这里,咱们应该很清楚类型不是对象而是对象的模板这个概念了。一样地,泛型类型也不是类型,而是类型的模板

C#提供了5种泛型:类、结构、接口、委托和方法。
注意,前4个是类型,而方法是成员。 spa


继续栈示例

将MyIntStack和MyFloatStack两个类改成MyStack泛型类。设计

class MyStack<T>
{
    int StackPointer=0;
    T[] StackArray;
    public void Push(T x){...}
    public T Pop(){...}
}

泛型类


建立和使用常规的、非泛型的类有两个步骤:声明和建立类的实例。可是泛型类不是实际的类,而是类的模板,因此咱们必须从它们构建实际的类类型,而后建立实例。
下图从一个较高的层面上演示了该过程。3d

  • 在某些类型上使用占位符来声明一个类
  • 为占位符提供真实类型。这样就有了真实类的定义,填补了全部的“空缺”。该类型称为构造类型(constructed type)
  • 建立构造类型的实例

声明泛型类


声明一个简单的泛型类和声明普通类差很少,区别以下。调试

  • 在类名后放置一组尖括号
  • 在尖括号中用逗号分隔的占位符字符串来表示但愿提供的类型。这叫作类型参数(type parameter)
  • 在泛型类声明的主体中使用类型参数来表示应该替代的类型
class SomeClass<T1,T2>
{
    public T1 SomeVar=new T1();
    public T2 OtherVar=new T2();
}

泛型类型声明中没有特殊的关键字,取而代之的是尖括号中的类型参数列表。code

建立构造类型


一旦建立了泛型类型,咱们就须要告诉编译器能使用哪些真实类型来替代占位符(类型参数)。
建立构造类型的语法以下,包括列出类名并在尖括号中提供真实类型来替代类型参数。要替代类型参数的真实类型叫作类型实参(type argument)。

SomeClass<short,int>

编译器接受类型实参而且替换泛型类主体中的相应类型参数,产生构造类型–从它建立真实类型的实例。


下图演示了类型参数和类型实参的区别。

  • 泛型类声明上的类型参数用作类型的占位符
  • 在建立构造类型时提供的真实类型是类型实参

建立变量和实例


在建立引用和实例方面,构造类类型的使用和常规类型类似。

MyNonGenClass myNGC=new MyNonGenClass();
SomeClass
<short,int> mySc1=new SomeClass<short,int>(); var mySc2=new SomeClass<short,int>();

和非泛型同样,引用和实例能够分开建立。

SomeClass<short,int> myInst;
myInst=new SomeClass<short,int>();

能够从同一泛型类型构建不一样类类型。每一个独立的类类型,就好像它们都有独立的非泛型类声明同样。

class SomeClass<T1,T2>
{
...
}
class Program
{
    static void Main()
    {
        var first=new SomeClass<short,int>();
        var second=new SomeClass<int,long>();
    }
}

使用泛型的栈的示例
class MyStack<T>
{
    T[] StackArray;
    int StackPointer=0;
    public void Push<T x>
    {
        if(!IsStackFull)
        {
            StackArray[StackPointer++]=x;
        }
    }
    public T Pop()
    {
        return (!IsStackEmpty)
            ?StackArray[--StackPointer]
            :StackArray[0];
    }
    const int MaxStack=10;
    bool IsStackFull{get{return StackPointer>=MaxStack;}}
    bool IsStackEmpty{get{return StackPointer<=0;}}
    public MyStack()
    {
        StackArray=new T[MaxStack];
    }
    public void Print()
    {
        for(int i=StackPointer-1;i>=0;i--)
        {
            Console.WriteLine("  Value:{0}",StackArray[i]);
        }
    }
}
class Program
{
    static void Main()
    {
        var StackInt=new MyStack<int>();
        var StackString=new MyStack<string>();
        StackInt.Push(3);
        StackInt.Push(5);
        StackInt.Push(7);
        StackInt.Push(9);
        StackInt.Print();
        StackString.Push("This is fun");
        StackString.Push("Hi there!  ");
        StackString.Print();
    }
}

比较泛型和非泛型栈

类型参数的约束


在泛型栈的示例中,栈除了保存和弹出它包含的一些项以外没作任何事情。它不会尝试添加、比较或作其余任何须要用到项自己的运算符的事情。理由是,泛型栈不知道它保存的项的类型是什么,也不知道这些类型实现的成员。
然而,C#对象都从object类继承,所以,栈能够确认,这些保存的项都实现了object类的成员。它们包括ToString、Equals以及GetType。
若是代码尝试使用除object类的其余成员,编译器会产生错误。

例:

class Simple<T>
{
    static public bool LessThan(T i1,T i2)
    {
        return i1<i2;      //错误
    }
    ...
}

要让泛型变得更有用,咱们须要提供额外的信息让编译器知道参数能够接受哪些类型。这些信息叫作约束(constrain)。只有符合约束的类型才能替代类型参数。

Where子句

约束使用Where子句列出。

  • 每一个约束的类型参数有本身的where子句
  • 若是形参有多个约束,它们在where子句中使用逗号分隔

where子句语法以下:

      类型参数         约束列表
         ↓               ↓
where TypeParam:constraint,constraint,...
  ↑            ↑
关键字         冒号

有关where子句的要点:

  • 它们在类型参数列表的关闭尖括号以后列出
  • 它们不是用逗号或其余符号分隔
  • 它们次序任意
  • where是上下文关键字,能够在其余上下文中使用

例:where子句示例

class MyClass<T1,T2,T3>
              where T2:Customer
              where T3:IComparable
{
    ...
}
约束类型和次序

where子句能够以任何次序列出。然而where子句中的约束必须有特定顺序。

  • 最多只能有一个主约束,如有则必须放第一位
  • 能够有任意多的接口名约束
  • 如有构造函数约束,必须放最后

例:约束示例

class SortedList<S>
        where S:IComparable<S>{...}
class LinkedList<M,N>
        where M:IComparable<M>
        where N:ICloneable{...}
class MyDictionary<KeyType,ValueType>
        where KeyType:IEnumerable,
        new()              {...}

泛型方法


与其余泛型不同,方法是成员,不是类型。泛型方法能够在泛型和非泛型类以及结构和接口中声明。


声明泛型方法

泛型方法具备类型参数列表和可选的约束

  • 泛型方法有两个参数列表
    • 封闭在圆括号内的方法参数列表
    • 封闭在尖括号内的类型参数列表
  • 要声明泛型方法,须要:
    • 在方法名称后和方法参数列表前放置类型参数列表
    • 在方法参数列表后放置可选的约束子句
                  类型参数列表      约束子句
                       ↓             ↓
public void PrintData<S,T>(S p,T t)where S:Person
{                             ↑
    ...                  方法参数列表
}

记住,类型参数列表在方法名称后,在方法参数列表前。

调用泛型方法

调用方法,需在调用时提供类型实参,以下:

MyMethod<short,int>();
MyMethod<int,long>();

例:调用泛型方法示例


推断类型

若是咱们为方法传入参数,编译器有时能够从方法参数中推断出泛型方法的类型形参用到的那些类型。这样就可使方法调用更简单,可读性更强。
以下代码,若咱们使用int类型变量调用MyMethod,方法调用中的类型参数信息就多余了,由于编译器能够从方法参数得知它是int。

int myInt=5;
MyMethod<int>(myInt);

因为编译器能够从方法参数中推断类型参数,咱们能够省略类型参数和调用中的尖括号,以下:

MyMethod(myInt);
泛型方法示例
class Simple
{
    static public void ReverseAndPrint<T>(T[] arr)
    {
        Array.Reverse(arr);
        foreach(T item in arr)
        {
            Console.WriteLine("{0},",item.ToString());
        }
        Console.WriteLine("");
    }
}
class Program
{
    static void Main()
    {
        var intArray=new int[]{3,5,7,9,11};
        var stringArray=new string[]{"first","second","third"};
        var doubleArray=new double[]{3.567,7,891,2,345};
        Simple.ReverseAndPrint<int>(intArray);
        Simple.ReverseAndPrint(intArray);
        Simple.ReverseAndPrint<string>(stringArray);
        Simple.ReverseAndPrint(stringArray);
        Simple.ReverseAndPrint<double>(doubleArray);
        Simple.ReverseAndPrint(doubleArray);
    }
}

扩展方法和泛型类


在第7章中,咱们详细介绍了扩展方法,它也能够和泛型类结合使用。它容许咱们将类中的静态方法关联到不一样的泛型类上,还容许咱们像调用类结构实例的实例方法同样来调用方法。
和非泛型类同样,泛型类的扩展方法:

  • 必须声明为static
  • 必须是静态类的成员
  • 第一个参数类型中必须有关键字this,后面是扩展的泛型类的名字
static class ExtendHolder
{
    public static void Print<T>(this Holder<T>h)
    {
        T[] vals=h.GetValue();
        Console.WriteLine("{0},\t{1},\t{2}",vals[0],vals[1],vals[2]);
    }
}
class Holder<T>
{
    T[] Vals=new T[3];
    public Holder(T v0,T v1,T v2)
    {
        Vals[0]=v0;Vals[1]=v1;Vals[2]=v2;
        public T[] GetValues(){return Vals;}
    }
}
class Program
{
    static void Main()
    {
        var intHolder=new Holder<int>(3,5,7);
        var stringHolder=new Holder<string>("a1","b2","c3");
        intHolder.Print();
        stringHolder.Print();
    }
}

泛型结构


与泛型类类似,泛型结构能够有类型参数和约束。泛型结构的规则和条件与泛型类同样。

struct PieceOfData<T>
{
    public PieceOfData(T value){_data=value;}
    private T _data;
    public T Data
    {
        get{return _data;}
        set{_data=value;}
    }
}
class Program
{
    static void Main()
    {
        var intData=new PieceOfData<int>(10);
        var stringData=new PieceOfData<string>("Hi there.");
        Console.WriteLine("intData    ={0}",intData.Data);
        Console.WriteLine("stringData ={0}",stringData.Data);
    }
}

泛型委托


泛型委托与非泛型委托很是类似,不过类型参数决定能接受什么样的方法。

  • 要声明泛型委托,在委托名称后、委托参数列表前的尖括号中放置类型参数列表
  • `delegate R MyDelegate<T,R>(T Value);`
  • 注意,有两个参数列表:委托形参列表和类型参数列表
  • 类型参数的范围包括:
    • 返回值
    • 形参列表
    • 约束子句

例:泛型委托示例

delegate void MyDelegate<T>(T value);
class Simple
{
    static public void PrintString(string s)
    {
        Console.WriteLine(s);
    }
    static public void PrintUpperString(string s)
    {
        Console.WriteLine("{0}",s.ToUpper());
    }
}
class Program
{
    static void Main()
    {
        var myDel=new MyDelegate<string>(Simple.PrintString);
        myDel+=Simple.PrintUpperString;
        myDel("Hi There.");
    }
}

另外一个 泛型委托示例

C#的LINQ(第19章)特性在不少地方使用了泛型委托,但在介绍LINQ前,有必要给出另一个示例。

public delegate TR Func<T1,T2,TR>(T1 p1,T2 p2);//泛型委托
class Simple
{
    static public string PrintString(int p1,int p2)
    {
        int total=p1+p2;
        return total.ToString();
    }
}
class Program
{
    static void Main()
    {
        var myDel=new Fun<int,int,string>(Simple.PrintString);
        Console.WriteLine("Total:{0}",myDel(15,13));
    }
}

泛型接口


泛型接口容许咱们编写参数和返回类型是泛型类型参数的接口。

例:IMyIfc泛型接口

interface IMyIfc<T>
{
    T ReturnIt(T inValue);
}
class Simple<S>:IMyIfc<S>
{
    public S ReturnIt(S inValue)
    {
        return inValue;
    }
}
class Program
{
    static void Main()
    {
        var trivInt=new Simple<int>();
        var trivString=new Simple<string>();
        Console.WriteLine("{0}",trivInt.ReturnIt(5));
        Console.WriteLine("{0}",trivString.ReturnIt("Hi there."));
    }
}

使用泛型接口的示例

以下示例演示了泛型接口的两个额外能力:

  • 实现不一样类型参数的泛型接口是不一样的接口
  • 能够在非泛型类型中实现泛型接口

例:Simple是实现泛型接口的非泛型类。

interface IMyIfc<T>
{
    T ReturnIt(T inValue);
}
class Simple:IMyIfc<int>,IMyIfc<string>     //非泛型类
{
    public int ReturnIt(int inValue)        //实现int类型接口
    {return inValue;}
    public string ReturnIt(string inValue)  //实现string类型接口
    {return inValue;}
}
class Program
{
    static void Main()
    {
        var trivial=new Simple();
        Console.WriteLine("{0}",trivial.ReturnIt(5));
        Console.WriteLine("{0}",trivial.ReturnIt("Hi there."));
    }
}
泛型接口的实现必须惟一

实现泛型类接口时,必须保证类型实参组合不会在类型中产生两个重复的接口。

例:Simple类使用了两个IMyIfc接口的实例化。
对于泛型接口,使用两个相同接口自己没有错,但这样会产生一个潜在冲突,由于若是把int做为类型参数来替代第二个接口中的S的话,Simple可能会有两个相同类型的接口,这是不容许的。

interface IMyIfc<T>
{
    T ReturnIt(T inValue);
}
class Simple<S>:IMyIfc<int>,IMyIfc<S>    //错误
{
    public int ReturnIt(int inValue)
    {return inValue;}
    public S ReturnIt(S inValue)   //若是它不是int类型的
    {return inValue;}              //将和上个示例的接口同样
}

说明:泛型接口的名字不会和非泛型冲突。例如,在前面的代码中咱们还能够声明一个名为IMyIfc的非泛型接口。

协变


纵观本章,你们已经看到,若是你建立泛型类型的实例,编译器会接受泛型类型声明以及类型参数来构造类型。可是,你们一般会错误的将派生类型分配给基类型的变量。下面咱们来看一下这个主题,这叫作可变性(variance)。它分为三种–协变(convariance)、逆变(contravariance)和不变(invariance)。
首先回顾已学内容,每一个变量都有一种类型,能够将派生类对象的实例赋值给基类变量,这叫赋值兼容性

例:赋值兼容性

class Animal
{
    public int NumberOfLegs=4;
}
class Dog:Animal
{
}
class Program
{
    static void Main()
    {
        var a1=new Animal();
        var a2=new Dog();
        Console.WriteLine("Number of dog legs:{0}",a2.NumberOfLegs);
    }
}

如今,咱们来看一个更有趣的例子,用下面的方式对代码进行扩展。

  • 增长一个叫作Factory的泛型委托,它接受类型参数T,不接受方法参数,而后返回一个类型为T的对象
  • 添加一个叫MakeDog的方法,不接受参数但返回一个Dog对象。若是咱们使用Dog做为类型参数的话,这个方法能够匹配Factory委托
class Animal{public int NumberOfLegs=4;}
class Dog:Animal{}
delegate T Factory<T>();
class Program
{
    static Dog MakeDog()
    {
        return new Dog();
    }
    static void Main()
    {
        Factory<Dog> dogMaker=MakeDog;
        Factory<Animal>animalMaker=dogMaker;
        Console.WriteLine(animalMaker().Legs.ToString());
    }
}

上面代码在Main的第二行会报错,编译器提示:不能隐式把右边的类型转换为左边的类型。
看上去由派生类型构造的委托应该能够赋值给由基类构造的委托,那编译器为什么报错?难道赋值兼容性原则不成立了?
不是,原则依然成立,可是对于这种状况不适用!问题在于尽管Dog是Animal的派生类,可是委托Factory<Dog>没有从委托Factory<Animal>派生。相反,两个委托对象是同级的,它们都从delegate类型派生。

再仔细分析一下这种状况,咱们能够看到,若是类型参数只用做输出值,则一样的状况也适用于任何泛型委托。对于全部这样的状况,咱们应该可使用由派生类建立的委托类型,这样应该可以正常工做,由于调用代码老是指望获得一个基类的引用,这也正是它会获得的。
若是派生类只是用于输出值,那么这种结构化的委托有效性之间的常数关系叫作协变。为了让编译器知道这是咱们的指望,必须使用out关键字标记委托声明中的类型参数。
增长out关键字后,代码就能够经过编译并正常工做了。

delegate T Factory<out T>();
                    ↑
            关键字指定了类型参数的协变
  • 图左边栈中的变量是T Factory<out T>()的委托类型,其中类型变量T是Animal类
  • 图右边堆上实际构造的委托是使用Dog类类型变量进行声明的,Dog从Animal派生
  • 这是可行的,尽管调用委托时,调用代码接受Dog类型的对象,而不是指望的Animal类型对象,可是调用代码能够像以前指望的那样自由地操做对象的Animal部分

逆变


如今来看另外一种状况。

class Animal{public int NumberOfLegs=4;}
class Dog:Animal{}
delegate T Factory<T>();
class Program
{
    delegate void Action1<in T>(T a);
    static void ActOnAnimal(Animal a)
    {
        Console.WriteLine(a.NumberOfLegs);
    }
    static void Main()
    {
        Action1<Animal> act1=ActOnAnimal;
        Action1<Dog> dog1=act1;
        dog1(new Dog());
    }
}

和以前状况类似,默认状况下不能够赋值两种不兼容的类型。但在某些状况下可让这种赋值生效。
其实,若是类型参数只用做委托中方法的输入参数的话就能够了。由于即便调用代码传入了一个程度更高的派生类的引用,委托中的方法也只指望一个程度低一些的派生类的引用,固然,它也仍然接受并知道如何操做。
这种指望传入基类时容许传入派生对象的特性叫作逆变。能够在类型参数中显式使用in关键字来使用。

  • 图左边栈上的变量是void Action1<in T>(T p)类型的委托,其类型变量是Dog类
  • 图右边实际构建的委托使用Animal类的类型变量来声明,它是Dog类的基类
  • 这样能够工做,由于在调用委托时,调用代码为方法ActOnAnimal传入Dog类型的变量,而它指望的是Animal类型的对象。方法固然能够像指望的那样自由操做对象的Animal部分

下图总结了泛型委托中协变和逆变的不一样


  • 上面的图演示了协变:
    • 左边栈上的变量是F<out T>()类型的委托,类型变量是叫作Base的类
    • 在右边实际构建的委托,使用Derived类的类型变量声明,这个类派生自Base
    • 这样能够工做,由于在调用时,方法返回指向派生类型的对象的引用,派生类型一样指向其基类,调用代码可正常工做
  • 下面的图演示了逆变:
    • 左边栈上的变量是F<in T>(T p)类型的委托,类型参数是Derived类
    • 在右边实际构建的委托,使用Base类的类型变量声明,这个类是Derived类的基类
    • 这样能够工做,由于在调用时,调用代码传入了派生类型的变量,方法指望的只是其基类,方法彻底能够像之前那样操做对象的基类部分
接口的协变和逆变

如今你应该已经理解了协变和逆变能够应用到委托上。其实相同的原则也可用到接口上,能够在声明接口的时候使用out和in关键字。

例:使用协变的接口

class Animal{public string Name;}
class Dog:Animal{};
interface IMyIfc<out T>
{
    T GetFirst();
}
class SimpleReturn<T>:IMyIfc<T>
{
    public T[] items=new T[2];
    public T GetFirst()
    {
        return items[0];
    }
}
class Program
{
    static void DoSomething(IMyIfc<Animal>returner)
    {
        Console.WriteLine(returner.GetFirst().Name);
    }
    static void Main()
    {
        SimpleReturn<Dog> dogReturner=new SimpleReturn<Dog>();
        dogReturner.items[0]=new Dog(){Name="Avonlea"};
        IMyIfc<Animal> animalReturner=dogReturner;
        DoSomething(dogReturner);
    }
}
有关可变性的更多内容

以前的两小节解释了显式的协变和逆变。还有一些状况编译器能够自动识别某个已构建的委托是协变或是逆变并自动进行类型强制转换。这一般发生在没有为对象的类型赋值的时候,以下代码演示了该例子。

class Animal{public int Legs=4;}
class Dog:Animal{}
class Program
{
    delegate T Factory<out T>();
    static Dog MakeDog()
    {
        return new Dog();
    }
    static void Main()
    {
        Factory<Animal> animalMaker1=MakeDog;//隐式强制转换
        Factory<Dog> dogMaker=MakeDog;
        Factory<Animal> animalMaker2=dogMaker;//须要out标识符
        Factory<Animal> animalMaker3
                   =new Factory<Dog>(MakeDog);//须要out标识符
    }
}

有关可变性的其余一些重要事项以下:

  • 变化处理的是使用派生类替换基类的安全状况,反之亦然。所以变化只适用于引用类型,由于不能从值类型派生其余类型
  • 显式变化使用in和out关键字只适用于委托和接口,不适用于类、结构和方法
  • 不包括in和out关键字的委托和接口类型参数叫作不变。这些类型参数不能用于协变或逆变
                         协变
                          ↓
delegate T Factory<out R,in S,T>();
                     ↑        ↑
                    逆变     不变
相关文章
相关标签/搜索