CSharp中的泛型

C#中的泛型

泛型的来历

  在写程序中常常遇到两个模块的功能非常相似,只是一个是处理int数据,另一个是处理string数据,或者其他自定义的数据类型,但我们没有办法,只能分别写多个方法处理每个数据类型,因为方法的参数类型不同。有没有一种办法,在方法中传入通用的数据类型,这样不就可以合并代码了吗?具体在C#中,基本集合是没有类型化的,需要把object项转化为集合中实际存储的对象类型,而且继承自System.object的任何对象都可以存储在ArrayList中,这就在处理问题时需要特别仔细。如果在一开始就使用强化类型的集合,就可以解决问题。

  泛型就是为此而生的,泛型类是在实例化的过程中提供的类型或类为基础建立的。这里与C++模板所不同的是,C++是在编译时便检测在哪里使用了模板的某个特定类型,而C#中所有的操作都是在运行期间进行。

一个简单的栗子

这个例子功能简单,就是实现一个栈,且只能处理int数据类型:

public class Stack

    {

        private int[] m_item;

        public int Pop(){...}

        public void Push(int item){...}

        public Stack(int i)

        {
            this.m_item = new int[i];
        }
    }

但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:

public class Stack

    {
        private object[] m_item;

        public object Pop(){...}

        public void Push(object item){...}

        public Stack(int i)

        {
            this.m_item = new[i];
        }    

    }

这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:

当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。
在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。
在数据类型的强制转换上还有更严重的问题(假设stack是Stack的一个实例):

Node1 x = new Node1();

stack.Push(x);

Node2 y = (Node2)stack.Pop();

这里push的时一个Node1类型的数据,但是在pop时却要求强制转换为Node2类型的数据,由于存的时object类型的,所有这个类型转换异常就逃离了编译器的检查。

使用泛型

可以用泛型来重写上述的代码,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。

public class Stack<T>

    {

        private T[] m_item;

        public T Pop(){...}

        public void Push(T item){...}

        public Stack(int i)

        {

            this.m_item = new T[i];

        }

     }

这个通用数据类型T是可以适用于任何数据类型,且是类型安全的,这个类的调用方法是:

//实例化只能保存int类型的类

Stack<int> a = new Stack<int>(100);

a.Push(10);

a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据

 int x = a.Pop();

//实例化只能保存string类型的类

Stack<string> b = new Stack<string>(100);

b.Push(10); //这一行编译不通过,因为类b只接收string类型的数据

b.Push("8888");

string y = b.Pop();

这个类和object实现的类有截然不同的区别:

  1. 他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。

  2. 无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。

  3. 无需类型转换。

  C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,我们可以这样认为:

泛型类的不同的封闭类是分别不同的数据类型

Stack和Stack是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。

定义泛型类型

泛型的定义过程

定义泛型类型

要创建泛型类,在类定义中包含尖括号就是了

class MyGenericClass<T1,T2,T3>

定义了这些类型后,就可以把他们作为,成员变量的类型,属性或方法等成员的返回类型,或者方法的参数类型。

这里有一点是需要注意的,处理对象集合是可以的,因为这不需要对对象类型做出任何假设,如果要求对对象类型进行实际假设,如==号,就需要了解类中使用的类型,这就一般是不能通过编译的。

对于没有任何约束的泛型(无绑定)类型可以使用default关键词

private T1 innerT1Object;

innerT1Object=default(T1);

如果T1是引用类型就给他赋null值,如果是值类型就给他赋默认值,对于数字类型,默认值是0。

约束类型

程序员在编写泛型类时,总是会对通用数据类型T进行有意或无意地有假想,也就是说这个T一般来说是不能适应所有类型,但怎样限制调用者传入的数据类型呢?这就需要对传入的数据类型进行约束,约束的方式是指定T的祖先,即继承的接口或类。因为C#的单根继承性,所以约束可以有多个接口,但最多只能有一个类,并且类必须在接口之前。

class MyGenericClass<T1,T2>: MyBaseClass,IMyInterface
    where T1: constraint1 where T2:constraint
{...}

这里的constraint可以取:
struct 类型必须是值类型,

class 类型必须是引用类型,

baseClass 必须是基类或者继承自基类,使用类名作为约束,

interface 必须是接口或者实现了接口,

new() 类型必须有一个公共的无参数构造函数,用于类中能实例话T类型的变量,例如在构造函数中实例化。

可以通过baseClass约束把一个类型的参数用作另一个参数类型的约束:

class MyGenericClass<T1,T2> where T2:T1{...}

这里T2必须与T1类型相同或者继承于T1,这叫裸类型约束

但类型约束不能循环

class MyGenericClass<T1,T2> where T2:T1 where T1:T2 {...}

从泛型类型中继承

类可以从泛型类型继承例如:

class Farm<T>:where T:Animal{...}

class SuperFarm<T>: Farm<T> where T:SuperCow {...}//SuperCow继承于Cow继承于Animal

但是如果某个类型所继承的基类型中受到了约束,该类型就不能解除约束,该类型所受到的类型约束至少应该与基类型的约束相同,把T约束为Animal的一个子集是可以的,但是约束为超集就会编译失败,例如:

class SuperFarm<T>:Farm<T> where T:class {...}

另外,如果继承于一个泛型类型,就必须提供所必须的类型信息,一是可以使用上面的使用其他泛型类型的参数形式提供,也可以显式提供

public class Cards:List<Card>,ICloneable{...}//这是可行的
public class Cards:List<T>,ICloneable{...}//没有提供T的信息,所以无法编译

如果给泛型类型提供了参数,例如List<Card>就称该类型是关闭的,没有提供,如List<T> 就是继承一个打开的泛型类型

泛型运算符

在C#中也是可以进行运算符重写的

例如,隐式转换运算符,可以在Farm中定义:

public static implicit operator List<Animal>(Farm<T> farm){...}

泛型方法

泛型不仅能作用在类上,也可单独用在类的方法上,他可根据方法参数的类型自动适应各种参数,这样的方法叫泛型方法。

public class Stack2
    {
        public void Push<T>(Stack<T> s, params T[] p)
        {
            foreach (T t in p)
            {
                s.Push(t);
            }
        }
     }

原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法,这个方法的调用示例如下:

Stack<int> x = new Stack<int>(100);

    Stack2 x2 = new Stack2();

    x2.Push(x, 1, 2, 3, 4, 6);

    string s = "";

    for (int i = 0; i < 5; i++)

    {

        s += x.Pop().ToString();

    }    //至此,s的值为64321

泛型方法的重载

方法的重载要求重载具有不同的签名。在泛型类中,由于通用类型T在类编写时并不确定,所以在重载时有些注意事项。

public class Node<T, V>

    {

        public T add(T a, V b)          //第一个add

        {

            return a;

        }

        public T add(V a, T b)          //第二个add

        {

            return b;

        }

        public int add(int a, int b)    //第三个add

        {

            return a + b;

        }

     }

上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码:

Node<int, int> node = new Node<int, int>();

object x = node.add(2, 11);

这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。

Node<string, int> node = new Node<string, int>();

object x = node.add(2, "11");

这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。

由以上示例可知,C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。同时还得出一个重要原则:

当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。

如果类是泛型的,就必须为泛型方法类型使用不同的标识符

这段代码就不能编译

public class StringGetter<T>
{
  public string GetString<T>(T item)=>item.ToString();
} 

同一个泛型类型的参数T不能既用于泛型类又用于泛型方法,需要重命名其中的一个或两个,应改为

public class StringGetter<U>
{
  public string GetString<T>(T item)=>item.ToString();
}

变体

对于变体,先总结一句话:参数协变,返回值抗变

在多态性中,允许把派生类型的变量放到基类的变量中

Cow myCow=new Cow("sss");
Animal myAnimal=myCow;

这是允许的,因为Cow派生自Animal但是这不适用于接口

IMyInterface<Cow> cowInterface=myCow;
IMyInterface<Animal> animalInterface=cowInterface;

这就不行了。假定Cow支持IMyInterface接口第一行是没问题的,但是第二行预先假定了两个接口类型有某种关系,为了使上述代码工作,就要让IMyInterface<T>的类型参数是协变的,在IMyInterface<Cow>和IMyInterface<Animal>建立继承关系。协变是把泛型接口值放到使用基类的变量中,而抗变则是把泛型接口值放到使用派生类型的变量中,例如:

IMyInterface<Cow> cowInterface=myCow;
IMyInterface<SuperCow> superCowInterface=cowInterface;

协变是在类型定义中使用out关键词:

public interface IMyInterface<out T>{...}

抗变则是在类型定义中使用in关键词

public interface IMyInterface< in T>{...}