C# 泛型

一.泛型所解决的问题及适用情形

假设我们声明了一个叫做MyIntStack的类,它实现了把一个int类型的值压入栈以及把它们弹出。

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

然后我们希望相同的功能应用于float类型的值,我们对MyIntStack类进行复制粘贴,并修改了相关数据类型。

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

如果我们需要将这个功能应用于stringdouble或者是long类型的值,继续沿用上面的方法有如下缺点:

  • 过于繁琐。
  • 需要占据额外的空间。
  • 调试和维护这些相似的实现复杂且容易出错。

二.C#中的泛型

泛型generic特性提供了一种更优雅的方式,可以让多个类型共享一组代码。泛型允许我们声明类型参数化的代码。也就是说,我们可以用 “类型占位符” 来写代码,然后在创建类的实例时指明真实的类型。
我们知道,类型是实例的模板。对于泛型类型,它是类型的模板。如图1所示。
图1:泛型类和类型以及实例的关系
C#提供了5种泛型:类,结构,接口,委托和方法。前面4个是类型,而方法是成员。
在上面栈的示例中,MyIntStackMyFloatStack两个类的主体声明都差不多,只不过在保存类型时有些不同。我们从MyIntStack通过以下步骤创建一个泛型类:

  • MyIntStack类定义中,使用类型占位符T而不是float来替换int
  • 修改类的名称为MyStack
  • 在类名后放置<T>
class MyStack <T>
{
    int StackPointer = 0;
    T[] StackArray;
    public void Push(T x)
    {
        //...
    }
    public T Pop()
    {
        //...
    }
}

在以上泛型类的声明中,有尖括号和T构成的字符串表明T是类型占位符(也不一定是T,他可以是任何标识符。)在类声明的主体中,每一个T都会被编译器替换为实际类型。


三.泛型类

泛型类的使用有以下几个步骤:

  • 在某些类型上使用占位符来声明一个泛型类
  • 为占位符提供真实类型,创建真实类的定义,该类型为构造类型
  • 创建构造类型的实例

步骤图示如下:
图2:泛型类使用步骤

//创建泛型类(where及其子句为类型参数的约束,暂时不必关心)
class SomeClas <T1,T2> 
    where T1 : new()
    where T2 : new()
{
    public T1 SomeVar = new T1();
    public T2 OtherVar = new T2();
}
class Program
{
    static void Main(string[] args)
    {
        //创建构造类型及其实例
        SomeClass <short, int> MyClass1 = new SomeClass<short, int>();
        var                    MyClass2 = new SomeClass<short, int>();
    }       
}

注意以下几个名词:

类型参数:类名后尖括号中的"类型占位符",以","间隔,如<T1,T2>
构造类型:将泛型类中的"类型占位符"替换为真实的类型后,所创建的真实的类。
类型实参:用于替换"类型占位符"的真实的类型。如:<short, int>


四.类型参数的约束

观察以下泛型类,类中声明了名为LessThan的方法,接受了两个泛型类型的变量。LessThan尝试用小于运算符返回结果。但是由于不是所有的类都实现了小于运算符,也就不能用任何类来代替T,所以编译器会产生一个错误消息。

class Simple<T> 
{
    static public bool LessThan(T t1,T t2)
    {
        return t1 < t2;           //错误
    }    
}

以下展示了一个类型参数被约束了的泛型类:

class MyClass<T1, T2, T3>
    where T2 : Customer
    where T3 : IComparable
{
   //...
}

关于类型参数的约束格式,需要注意以下几点:

  • 约束格式:where 类型参数 :约束1,约束2,...
  • 约束位置:在类型参数列表的尖括号之后。
  • 对多个类型参数进行约束时,where子句之间没有任何符号分隔。
  • 对多个类型参数进行约束时,where子句可以以任何次序列出。
  • 没有被约束的类型参数又被称为未绑定的类型参数。

关于类型参数的约束类型,有以下几种:

类名:只有这个类型的类或者从它继承的类才能用作类型实参。
class:任何引用类型,包括类,数组,委托和接口都可以用作类型实参。
struct:任何值类型都可以用作类型实参。
接口名:只有这个接口或实现这个接口的类型才能用作类型实参。
new():任何带有无参公共构造函数的类型都可以用作类型实参。这叫做构造函数约束。

虽然where子句可以以任何次序列出,但是where子句的约束必须有特定的次序。关于类型参数的约束次序,需要注意以下几点:

  • 最多只能有一个主约束,如果有必须放在第一位。主约束包括:类名classstruct
  • 可以有任意多的接口约束。
  • 如果存在构造函数约束,必须放在最后。

图3:约束次序


五.泛型方法

以下展示了一个泛型方法的声明和调用:

class MyClass
{
    //声明泛型方法
    public void MyMethod1<S, T>() where S : Pesron
    {
        //...
    }
    public void MyMethod2<T>(T t) 
    {
        //...
    }
}
class Program
{
    static void Main(string[] args)
    {
        MyClass myClass = new MyClass();
        //普通调用泛型方法
        myClass.MyMethod1<Pesron, string>();
        //在调用时通过传入参数的类型推断类型参数,从而省略类型参数列表
        int myInt = 5;
        myClass.MyMethod2(myInt);
    }       
}

关于泛型方法的声明和调用,要注意以下几点:

  • 类型参数列表放置在方法名后,方法参数列表之前。
  • 约束放置在方法参数列表之后。
  • 可省略类型参数列表,因为编译器可从传入参数推断类型参数。

六.泛型结构

泛型结构的使用与泛型类相似,以下展示了一个泛型结构示例:

struct MyStruct<T> 
{
    //...
}
class Program
{
    static void Main(string[] args)
    {
        MyStruct<int> myIntStruct = new MyStruct<int>();
        MyStruct<string> myStringStruct = new MyStruct<string>();
    }       
}

七.泛型委托

以下展示了泛型委托的示例:

//定义泛型委托
delegate R MyDelegate<T, R>(T value);
class Program
{
    static void Main(string[] args)
    {
        //构造委托类型,创建委托变量,同时初始化委托变量。
        MyDelegate<bool, bool> myDelegate1 = new MyDelegate<bool, bool>(PrintBool);
        MyDelegate<int, int>   myDelegate2 = new MyDelegate<int, int>(PrintInt); 
        myDelegate1(true);
        myDelegate2(5);
    }
    static bool PrintBool(bool myBool)
    {
        Console.WriteLine(myBool);
        return myBool;
    }
    static int PrintInt(int myInt) 
    {
        Console.WriteLine(myInt);
        return myInt;
    }
}

关于泛型委托,需要注意以下几点:

  • 在泛型委托的定义中,类型参数列表放置在委托名之后,形参列表之前。
  • 类型参数列表包括返回值类型和形参列表类型。

八.泛型接口

以下展示了泛型接口的使用示例:

//声明泛型接口
interface IMyIfc<T> 
{
    T ReturnIt(T inValue);
}
//在泛型类中实现泛型接口
class Simple1<S> : IMyIfc<S>
{
    public S ReturnIt(S inValue)
    {
        return inValue;
    }
}
//在非泛型类中实现泛型接口,需要构建构造接口类型
class Simple2 : IMyIfc<int>,IMyIfc<String>
{
    public int ReturnIt(int inValue)
    {
        return inValue;
    }
    public string ReturnIt(string inValue)
    {
        return inValue;
    }
}
class Program
{
    static void Main(string[] args)
    {
        //调用泛型类方法
        Simple1<int> myIntSimple1 = new Simple1<int>();
        myIntSimple1.ReturnIt(5);
        Simple1<string> myStringSimple1 = new Simple1<string>();
        myStringSimple1.ReturnIt("Hello");
        //调用非泛型类方法
        Simple2 mySimple2 = new Simple2();
        mySimple2.ReturnIt(5);
        mySimple2.ReturnIt("Hello");
    }

关于泛型接口的使用,有以下几点需要注意:

  • 类型参数列表放置在接口名之后。
  • 实现不同类型参数的泛型接口是不同的接口。
  • 既可在泛型类中实现泛型接口,也可在非泛型类中实现泛型接口。
  • 在泛型类中实现泛型接口时,可选择性构建构造接口类型,但必须保证不能产生重复的接口。
  • 在非泛型类中实现泛型接口时,需要构建构造接口类型,也就是说"类型占位符"需要替换为具体的类型。
  • 泛型接口的名字不会和非泛型接口的名字冲突,因此,可声明两个同名接口,一个为非泛型,另外一个为泛型。

注意:在泛型类中实现泛型接口时,源自同一泛型接口的不同类型参数的接口,必须保证不能产生两个重复的接口。例如:

//错误,IMyIfc<S>已经包含IMyIfc<int>
class Simple1<S> : IMyIfc<S>,IMyIfc<int>
{
    public S ReturnIt(S inValue)
    {
        return inValue;
    }
    public int ReturnIt(int inValue)
    {
        return inValue;
    }
}

九.可变性(协变,逆变,不变)

在了解可变性之前,先了解以下名词:

赋值兼容性:将派生类对象的引用赋值给基类变量。

关于逆变和协变的官方解释:

在C#中,协变和逆变能够实现数组类型,委托类型,泛型类型的参数的隐式引用转换。协变保留了赋值兼容性,逆变反转了赋值兼容性。

需要注意的是:

  • 赋值兼容性存在于C#任何类型(预定义类型和用户自定义类型)中。
  • 可变性存在于数组类型,委托类型,泛型类型中。

个人理解(都说了是个人理解,内含有个人大胆猜测和比喻,有错误欢迎指正,请勿乱喷,求生欲Max!):

:这里引用了高数函数概念。官方并没有"A兼容B"这样的说法。
设有类型ABB继承A。如果B的实例可以赋值给A类型的变量,那么说明A兼容B
f(A)f(B)中的f指的是将A类型变成A类型的数组/委托/泛型f(A),将B类型变成B类型的数组/委托/泛型f(B)
如果f(B)的实例可以赋值给f(A)类型的变量,那么称f(A)兼容f(B),这与运算前相同,因此称这个过程保留了赋值兼容性,为协变。
如果f(A)的实例可以赋值给f(B)类型的变量,那么称f(B)兼容f(A),这与运算前相反,因此称这个过程反转了赋值兼容性,为逆变。

以下示例体现了赋值兼容性:

//体现赋值兼容性
string myString = "Hello";
object myObject = myString;

以下示例体现了协变:

//协变:保留了赋值兼容性
string[] myStringArray = new string[10];
object[] myObjectArray = myStringArray;

以下示例体现了逆变:

//逆变:反转了赋值兼容性
Action<object> myObjectAction = new Action<object>(MyMethod);
Action<string> myStringAction = myObjectAction;

void MyMethod(object obj)
{
    //...
}

泛型类型的可变性与其他类型的可变性又有所区别,主要有以下两点:

  • 泛型中的协变和逆变需要在类型参数前加outin关键字,用于显式的标识对哪一个类型参数使用协变或者逆变。
  • 泛型中的协变用于输出参数,因此out添加在与输出参数对应的类型参数前,逆变用于传入参数,因此in添加在与输入参数对应的类型参数前。

以下示例展示了泛型中的协变:

class Animal
{
    public int Legs = 4;    
}
class Cat : Animal
{
    //...
}
//泛型用于输出参数,并用out关键字标记为协变
delegate T Factory<out T>();
class Program
{
    static void Main(string[] args)
    {
        
        Factory<Cat> catMaker = MakeCat;
        Factory<Animal> animalMaker = catMaker;
        Console.WriteLine(animalMaker().Legs.ToString());
    }
    static Cat MakeCat()
    {
        return new Cat();
    }
}

如果将派生类委托赋值给基类委托变量,却没有在泛型委托的类型参数列表中使用 outin进行标记,上面示例的下列代码行会报错,提示无法进行隐式类型转换。

 Factory<Animal> animalMaker = catMaker;

报错的原因:

并不是因为赋值兼容性不成立,而是因为不适用。因为派生类委托并不继承基类委托。

以下示例展示了泛型中的逆变:

class Animal
{
    public int Legs = 4;    
}
class Cat : Animal { }
//泛型用于输入参数,并用in关键字标记为逆变
delegate void Factory<in T>(T t);
class Program
{
    static void Main(string[] args)
    {
        Factory<Animal> animalMaker = MakeAnimal;
        Factory<Cat> catMaker = animalMaker;

    }
    static void MakeAnimal(Animal a) 
    {
        Console.WriteLine(a.Legs);
    }
}

到这里会出现一个疑问,为什么在非泛型类型中协变和逆变时不用 outin标记,因为:

C#能自动进行协变和逆变的类型转换,但是为了防止编程人员在不知情的情况下进行了一些导致错误编程,因此需要显式的进行标记,像是约定了一种规则。

总结,关于可变性,需要注意以下几点:

可变性只适用于引用类型。
outin关键字只适用于委托接口,不适用于类,结构和方法。
不包括outin关键字的委托和接口类型参数叫做不变。这些参数不能用于协变或逆变。


十.参考

书籍:《C#图解教程 第4版》
博客:10分钟了解C#中的协变和逆变协变和逆变(官方文档)

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值