泛型

上一节我们说到了两种数据类型数组和集合,数组是指包含同一类型的多个元素,集合是指.net中提供数据存储和检索的专用类。

数组使用前需要先指定大小,并且检索不方便。集合检索和声明方便,但是存在类型安全问题,本来使一个类型安全的 C# 变得不安全了。

集合为了解决数组预设大小的问题,采取了一种自动扩容的办法,这样当大小不够时,他就创建一个新的存储区域,把原有集合的元素复制过来。如此又对性能上也是有很大的影响。

上节我们说到解决这些缺陷的方法,那就是 .NET 2.0 以后,微软程序猿们推出来的新特性——泛型。

1.什么是泛型?

泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个占位符。

这个概念听起来比较绕,其实理解起来也不难,我的理解是类、接口、委托、结构或方法中有类型参数就是泛型类型,这样就有类型参数的概念。泛型集合类可以将类型参数用作它存储对象的点位符;类型参数作为其字段或方法的参数类型出现(这是 MSDN 中的描述)。

泛型集合所在的命我空间为: System.Collections.Generic

而 L ist类是 A rrayList的泛型等效类。该类使用大小按需动态增加的数组实现 IL ist接口。使用方法就是 IL ist<T>和 L ist<T>,这个 T 就是你要指定的集合的数据或对象类型。

2.泛型声明

泛型类: class Name<t>{}

泛型方法: void Name(T t){}

泛型接口: interface IName<T>{}

泛型结构: struct Name<T>{}

泛型委托: public delegate void N ame<T>(T param);

3.泛型方法

泛型我们在定义的时候,说明了他是可以使用占位符来占位类、结构、接口和方法的。我们先看一下方法使用泛型的例子。

我们还是使用前面的例子来看一下使用泛型:

类之间的关系 UML 图如下:

我们调用假如要实现,让每个动物都叫几声。该如何写呢?

/// <summary>
/// 动物类(父类 抽象类)
/// </summary>
abstract class Animal
{
    /// <summary>
    /// 名字
    /// 说明:类和子类可访问
    /// </summary>
    protected string name;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="name"></param>
    public Animal(string name)
    {
        this.name = name;
    }

    private int shoutNum = 8;
    public int ShoutNum
    {
        get { return shoutNum; }
        set { shoutNum = value; }
    }

    /// <summary>
    /// 名字(虚属性)
    /// </summary>
    public virtual string MyName
    {
        get { return this.name; }
    }

    /// <summary>
    /// 叫声,这个方法去掉虚方法,把循环写在这里
    /// </summary>
    public void Shout()
    {
        string result = "";
        for (int i = 0; i < ShoutNum; i++)
            result += getShoutSound() + "!";

        Console.WriteLine(MyName);
        Console.WriteLine(result);
    }

    /// <summary>
    /// 创建一个叫声的虚方法,子类重写
    /// </summary>
    /// <returns></returns>
    public virtual string getShoutSound()
    {
        return "";
    }

    /// <summary>
    /// 让所有动集合类的动物叫三次并报名字 (泛型)
    /// </summary>
    /// <param name="animal"></param>
    public static void AnimalShout(IList<Animal> animal)
    {
        DateTime dt = System.DateTime.Now;
        foreach (Animal anm in animal)
        {
            anm.Shout();
        }
        Console.WriteLine("使用泛型让所有动物叫一遍所用时间为:" + (System.DateTime.Now - dt).TotalMilliseconds +"毫秒");
    }
    /// <summary>
    /// 让所有动集合类的动物叫三次并报名字 (重载方法 集合)
    /// </summary>
    /// <param name="animal"></param>
    public static void AnimalShout(ArrayList animal)
    {
        DateTime dt = System.DateTime.Now;
        foreach (Animal anm in animal)
        {
            anm.Shout();
        }
        Console.WriteLine("使用集合让所有动物叫一遍所用时间为:" + (System.DateTime.Now - dt).TotalMilliseconds + "毫秒");
    }

    /// <summary>
    /// 让所有动集合类的动物叫三次并报名字 (重载方法 数组)
    /// </summary>
    /// <param name="animal"></param>
    public static void AnimalShout(Animal[] animal)
    {
        DateTime dt = System.DateTime.Now;
        foreach (Animal anm in animal)
        {
            anm.Shout();
        }
        Console.WriteLine("使用数组让所有动物叫一遍所用时间为:" + (System.DateTime.Now - dt).TotalMilliseconds + "毫秒");
    }
}


/// <summary>
/// 狗(子类)
/// </summary>
class Dog : Animal
{
    string myName;
    public Dog(string name)
        : base(name)
    {
        myName = name;
    }

    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:狗狗,我叫:" + this.name; }
    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "汪!";
    }
}

/// <summary>
/// 狗(子类)
/// </summary>
class ShepherdDog : Dog
{
    string myName;
    public ShepherdDog(string name)
        : base(name)
    {
        myName = name;
    }

    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:牧羊犬,我叫:" + this.name; }
    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "汪~呜!";
    }
}

/// <summary>
/// 猫(子类)
/// </summary>
class Cat : Animal
{
    string myName;
    public Cat(string name)
        : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:猫咪,我叫:" + this.name; }

    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "喵!";
    }
}

/// <summary>
/// 猫(子类)
/// </summary>
class PersianCat : Cat
{
    string myName;
    public PersianCat(string name)
        : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:波斯猫,我叫:" + this.name; }

    }

    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "喵~呜!";
    }
}

/// <summary>
/// 羊(子类)
/// </summary>
class Sheep : Animal
{
    string myName;
    public Sheep(string name)
        : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:羊羊,我叫:" + this.name; }

    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "咩!";
    }
}

调用方法:

//数组
Animal[] animalArray = new Animal[] { new Dog("旺财"), new Cat("小花"), new Cat("阿狸"), new Sheep("纯羊"), new Dog("小白"), new ShepherdDog("汪羊"), new PersianCat("机猫") };

//泛型
IList<Animal> animal = new List<Animal>();
animal.Add(new Dog("旺财"));
animal.Add(new Cat("小花"));
animal.Add(new Cat("阿狸"));
animal.Add(new Sheep("纯羊"));
animal.Add(new Dog("小白"));
animal.Add(new ShepherdDog("汪羊"));
animal.Add(new PersianCat("机猫"));

//集合
ArrayList animalArrayList = new ArrayList();
animalArrayList.Add(new Dog("旺财"));
animalArrayList.Add(new Cat("小花"));
animalArrayList.Add(new Cat("阿狸"));
animalArrayList.Add(new Sheep("纯羊"));
animalArrayList.Add(new Dog("小白"));
animalArrayList.Add(new ShepherdDog("汪羊"));
animalArrayList.Add(new PersianCat("机猫"));


//调用重载方法看它们的执行叫8次并报名字所需时间
Animal.AnimalShout(animalArray);
Animal.AnimalShout(animal);
Animal.AnimalShout(animalArrayList);
Console.ReadLine();

执行结果如下:

以上的实例并没有模拟出能客观测试效率的环境,因为根据我们的经验数组并不能接受不同类型的元素,而集合和泛型可以,如果使用不同数据测试,也不是客观的。以上实例主要反映了泛型、数组、集合的使用方法,小伙伴们不要太纠结测试时间,不过泛型的时间确实是比较快的。有了泛型小伙伴们就不要再使用ArrayList这个不安全类型了。

数组、 List 和 ArrayList 的区别 :

上节说了数组和集合 A rrayList的区别,这节我们使用了泛型,再说一下他们三者的区别

数组: ( 1 )在内存中是连续存储的,所以它的索引速度是非常的快,而且赋值与修改元素也很简单。

(2)但是数组也存在一些不足的地方。比如在数组的两个数据间插入数据也是很麻烦的,还有我们在声明数组的时候,必须同时指明数组的长度,数组的长度过长,会造成内存浪费,数组和长度过短,会造成数据溢出的错误。这样如果在声明数组时我们并不清楚数组的长度,就变的很麻烦了 .

集合ArrayList:

集合的出现就是为了解决数组的缺陷,但他本身也有缺陷,直到 .NET 2.0 以后出现泛型,我们可以说这是微软设计上的失误。 (1).ArrayList 并非类型安全

ArrayList 不论什么类型都接受,实际是接受一个 object 类型。

比如如下操作:

ArrayList ar = new ArrayList();

ar.Add(111);

ar.Add("bbb");

我们使用 foreach 遍历的时候 foreach(int array in ar){} 那么遇到 ”bbb” 则程度报错,因此我们说他是非安全类型。

(2). 遍历 ArrayList 资源消耗大

因此类型的非安全,我们在使用 ArrayList 的时候,就意味着增加一个元素,就需要值类型转换为 Object 对象。遍历的时候,又需要将 Object 转为值类型。

就是装箱 (boxing, 指将值类型转换为引用类型 ) 和 拆箱( unboxing, 指将引用类型转换为值类型)

由于装箱了拆箱频繁进行,需要大量计算,因此开销很大。

泛型 L ist:

List和 A raayList都是继承于 IL ist,属于等效类,他们之间的区别也是对集合 A rrayList局限性的修改

( 1 )类型安全,通过允许指定泛型类或方法操作的特定类型,泛型功能将类型安全的任务从您转移给了编译器。不需要编写代码来检测数据类型是否正确,因为会在编译时强制使用正确的数据类型。减少了类型强制转换的需要和运行时错误的可能性。

( 2 )减少开销,泛型提供了类型安全但没有增加多个实现的开销。

3. 泛型类

对于泛型类,我们先解决一下实际例子:假如我们有一个泛型类,不知道是什么类型,在初始化的时候再指定类型。类里面有一个方法,可以接受初始化的参数类型,也就是一个泛型方法。

/// <summary>
/// 泛型有一个泛型方法,该方法有一个泛型参数
/// </summary>
/// <typeparam name="T"></typeparam>
class MyClass<T>
{
    public static T  F(T param)
    {
        return param;
    }
}

调用:

//泛型类调用
int param = 2, param2= 3;
string result1 = (MyClass<string>.F(param.ToString()) + param2).ToString();
string result2 = (MyClass<int>.F(param) + param2).ToString();

Console.WriteLine(result1);
Console.WriteLine(result2);
Console.ReadLine();

可以看到,输出结果为: 23 和 5 ,使用泛型,我们不会因为数据类型不同,就需要去复制一个方法去处理了。

4.类型约束

我们上面定义了泛型,他们默认没有类型约束,就是说可以是任意类型。既然定义了泛型,为何还要约束,其实我们这么理解,泛型首先是一种安全类型,约束他只是让他的范围更小一点而已。

比如某些情况下,我们需要约束类型

主要有以下六种类型的约束:

约束 说明
T:结构 类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。有关更多信息,请参见使用可空类型(C# 编程指南)。
T:类 类型参数必须是引用类型,包括任何类、接口、委托或数组类型。
T:new() 类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。
T:<基类名> 类型参数必须是指定的基类或派生自指定的基类。
T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。
T:U 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束。

类型约束的特点:

A.指定的就是类型参数必须是引用类型,

B.包括任何类、接口、委托或数组类型,如果除了这几样就是非法的了

C.可以输入任何的引用类型同时也确定了范围,防止了输入值类型引发的不安全

D.约束的类型必须是非封闭的类型

E. 建议不要对类型参数使用 == 和 != 运算符,因为这些运算符仅测试引用同一性而不测试值相等性

F. 如果有 new (),则一定要放在最后

未绑定的类型参数

没有约束的类型参数(如公共类 SampleClass<T>{} 中的 T )称为未绑定的类型参数。未绑定的类型参数具有以下规则:

·不能使用 != 和 == 运算符,因为无法保证具体类型参数能支持这些运算符。

·可以在它们与 System.Object 之间来回转换,或将它们显式转换为任何接口类型。

·可以将它们与 null 进行比较。将未绑定的参数与 null 进行比较时,如果类型参数为值类型,则该比较将始终返回 false 。

裸类型约束

·用作约束的泛型类型参数称为裸类型约束。

·当具有自己的类型参数的成员函数需要将该参数约束为包含类型的类型参数时,裸类型约束很有用,泛型类的裸类型约束的作用非常有限,因为编译器除了假设某个裸类型约束派生自 System.Object 以外,不会做其他任何假设。

·在希望强制两个类型参数之间的继承关系的情况下,可对泛型类使用裸类型约束。

5. 约束举例说明

下面对这几种约束举例说明

还是以这个动物系列为例,下面是实现代码:

/// <summary>
/// 动物类(父类 抽象类)
/// </summary>
abstract class Animal
{
    /// <summary>
    /// 名字
    /// 说明:类和子类可访问
    /// </summary>
    protected string name;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="name"></param>
    public Animal(string name)
    {
        this.name = name;
    }

    private int shoutNum = 3;
    public int ShoutNum
    {
        get { return shoutNum; }
        set { shoutNum = value; }
    }

    /// <summary>
    /// 名字(虚属性)
    /// </summary>
    public virtual string MyName
    {
        get { return this.name; }
    }

    /// <summary>
    /// 叫声,这个方法去掉虚方法,把循环写在这里
    /// </summary>
    public void Shout()
    {
        string result = "";
        for (int i = 0; i < ShoutNum; i++)
            result += getShoutSound() + "!";

        Console.WriteLine(MyName);
        Console.WriteLine(result);
    }

    /// <summary>
    /// 创建一个叫声的虚方法,子类重写
    /// </summary>
    /// <returns></returns>
    public virtual string getShoutSound()
    {
        return "";
    }
}

/// <summary>
/// 声明一个接口 ISpeak(讲话)
/// </summary>
interface ISpeak
{
    void Speak();
}

/// <summary>
/// 狗(子类)
/// </summary>
class Dog : Animal
{
    string myName;
    public Dog(string name): base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:狗狗,我叫:" + this.name; }
    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "汪!";
    }
}

/// <summary>
/// 猫(子类)
/// </summary>
class Cat : Animal
{
    string myName;
    public Cat(string name): base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:猫咪,我叫:" + this.name; }
    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "喵!";
    }
}

/// <summary>
/// 蓝猫(子类)
/// 继承 Cat和接口ISpeak
/// </summary>
class BlueCat : Cat,ISpeak
{
    string myName;
    public BlueCat(string name) : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:蓝猫,我叫:" + this.name; }
    }

    /// <summary>
    /// 实现接口ISpeak的成员Speak
    /// </summary>
    public void Speak()
    {
        Console.WriteLine("我会说人话:“你好,我叫:" + this.name + "~~~”");
    }
}

/// <summary>
/// 羊(子类)
/// </summary>
class Sheep : Animal
{
    string myName;
    public Sheep(string name): base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:羊羊,我叫:" + this.name; }
    }
    /// <summary>
    /// 叫(重写父类方法)
    /// </summary>
    public override string getShoutSound()
    {
        return "咩!";
    }
}

/// <summary>
/// 喜羊羊(子类)
/// 继承 Sheep和接口ISpeak
/// </summary>
class PleasantSheep : Sheep, ISpeak
{
    string myName;
    public PleasantSheep(string name) : base(name)
    {
        myName = name;
    }
    /// <summary>
    /// 名字(重写父类属性)
    /// </summary>
    public override string MyName
    {
        get { return "我是:喜羊羊,我叫:" + this.name; }
    }
    /// <summary>
    /// 实现接口ISpeak的成员Speak
    /// </summary>
    public void Speak()
    {
        Console.WriteLine("我会说人话:“你好,我叫:" + this.name + "~~~”");
    }
}

5.1 基类约束

这个比较常见,就是约束类型的实参都必须继承同一基类

我们新建一个泛型类 CatShout ,约束它的基类实参为 Cat 类

//泛型约束 - 基类约束
class CatShout<T>
    where T : Cat
{   
    public  void Shout(T cat)
    {
        cat.Shout();
    }
}

调用及结果:

//泛型约束- 基类约束
CatShout<Cat> cat = new CatShout<Cat>();
cat.Shout(new BlueCat("兰喵"));
Console.ReadLine();
//CatShout<Dog> dog = new CatShout<Dog>(); 假如使用 Dog类实例化,约束生效,程序报错

5.2 接口约束

这次我们建立一个泛型类 AnimalSpeak 类,约束它派生自接口 ISpeak

//泛型约束 - 接口约束
class AnimalSpeak<T>
    where T : ISpeak
{
    public void Speak(T t)
    {
        t.Speak();
    }
}

调用及结果:

//泛型约束- 接口约束
AnimalSpeak<BlueCat> animalSpeak = new AnimalSpeak<BlueCat>();
animalSpeak.Speak(new BlueCat("兰喵"));

AnimalSpeak<PleasantSheep> animalSpeak2 = new AnimalSpeak<PleasantSheep>();
animalSpeak2.Speak(new PleasantSheep("肥咩"));

//假如使用以下调用,约束生效,程序报错,因为Cat并不继承接口ISpeak,不符合接口约束
//AnimalSpeak<Cat> animalSpeak3 = new AnimalSpeak<Cat>();
//animalSpeak2.Speak(new Cat("阿狸"));

说明:可以同时约束为类和接口,类在前面,接口在后。接口可以有多个,类只能一个,不可以继承多个类。

6.0 要点:

最后总结一下

A.泛型基本的使用比较简单,简单说就是一个取代数组和集合的安全类型

B.泛型包括,泛型类、方法、接口、委托、结构。

C.泛型在某些情况下为了调用更加安全,即在编译阶段就进行校验,经常使用约束

D.泛型的类型约束有几个方面:类约束,接口,构造函数,结构等。基类约束、接口约束和构造函数约束较为常见

E.类的约束使用 where 关键词,约束的几个方面有先后顺序,通常类,接口,构造函数,这样的顺序。

如果感觉约束这块儿有点难理解,小伙伴们先掌握好基本泛型使用方法。