19. 泛型

19.1 什么是泛型

很多时候,如果我们可以把类的行为提取或重构出来,使之不仅能应用到它们编码的数据类型上,而且还能应用到其他类型上的话,类就会更有用。

有了泛型我们就可以做到这一点了。我们可以重构代码并且额外增加一个抽象层,对于这样的代码来说,数据类型就不用硬编码了。这是专门为多段代码在不同的数据类型上执行相同指令的情况专门设计的。

栈的示例:压入int,long,double,string的栈类时,我们要写4个方法才可以完成。调试和维护复杂而且容易出错。




19.2 C#中的泛型

在C#2.0中,微软引入了泛型(generic)特性,它提供了一种更准确地使用有一种以上的类型的代码的方式。泛型允许我们声明类型参数化的代码,我们可以用不同的类型进行实例化。也就是说,我们可以用“类型占位符”来写代码,然后在创建类的实例时提供真实的类型。

至此,我们应该很清楚类型不是对象而是对象的模板这个概念了。同样地,泛型类型也不是类型,而是类型的模板。

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

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

    class MyStack<T>
    {
        int StackPoint = 0;
        T[] StackArray;

        public void Push(T x) { }

        public T Pop() { }
    }



19.3 泛型类

创建和使用常规的、非泛型的类的过程中有两个步骤:声明类并创建类的实例。但是泛型类不是实际的类,而是类的模板,所以我们必须先从它们构建实际的类类型,然后创建这个构建后的类类型的实例。

  • 在某些类型上使用占位符来声明一个类。
  • 为占位符提供真实类型。这样就有了真实类型的定义,填补了所有的“空缺”。
  • 从“填空后”的类定义创建实例。



19.4 声明泛型类

声明一个简单的泛型类和声明普通类差不多,有如下的区别:

  • 在类名之后放置一组尖括号。
  • 在尖括号中用逗号分隔的占位符字符串来表示希望提供的类型。这被叫做类型参数(type parameter)。
  • 在泛型类声明的主体中使用类型参数来表示应该被替代的类型。

    class SomeClass<T1, T2>
    {
        public T1 SomeVar = new T1();
        public T2 OtherVar = new T2();
    }

在泛型声明中没有特殊的关键词或标志。尖括号中的类型参数列表的表示将泛型类声明与普通类声明区分开。




19.5 创建构造类型

我们不能直接从泛型类型创建类对象。首先,我们需要告诉编译器使用哪些真实类型来替代占位符(类型参数)。编译器获取这些真实类型并从它创建一个真实类型对象。

要从泛型类构建类类型,列出类名字并在尖括号中提供真实类型来替代类型参数。要替代类型参数的真实类型叫做类型实参(type argument)。

SomeClass<short,int>

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


泛型类声明有类型参数。

在创建构造类型时提供的真实类型是类型实参。




19.6 创建变量和实例

在创建引用和实例时构造类类型的使用和常规类型差不多。

例如,如下代码演示了两个类对象的创建。

  • 第一行显示了普通非泛型类型对象的创建。这应该是我们非常熟悉的形式。
  • 第二行代码显示了SomeClass泛型类型对象的创建,使用short和int类型进行实例化。这种形式和上面一行差不多,只不过把普通类型名改为构造类形式。
  • 第三行和第二行的语法一样,没有在等号两边都列出构造类型,而是使用var关键词使使用类型引用。

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

和非泛型类一样,引用和实例可以分开创建。

可以从同一个泛型类型构建出很多不同的类类型。每一个都有独立的类类型,就好像它们都有独立的非泛型类声明一样。


比较泛型和非泛型栈

非泛型栈和泛型栈之间的区别
 非泛型泛型
源代码大小更大:我们需要为每一种类型进行一个新的实现更小:不管构造类型的数量有多少,我们只需要一个实现
可执行大小无论每一个版本的栈是否会被使用,都会在编译的版本中出现可执行文件中只会出现有构造类型的类型
写的难易度易于书写比较难写
维护的难易度更容易出问题,因为所有修改需要应用到每一个可用的类型上易于维护,因为只需要修改一个地方。




19.7 类型参数的约束

在泛型栈的示例中,栈除了保存和弹出它们包含的一些项之外没有做任何事情。它不会尝试添加、比较或做其他任何需要用到项本身运算符的事 情。理由还是很简单的,由于泛型栈不会知道它们保存的项的类型是什么,它不会知道这些类型实现的成员。

然而,所有的C#对象最终都从object类继承,因此,栈可以确认的是,这些保存的项都实现了object类的成员。它们包括ToString、Equals以及GetType。除了这些,它不知道还有哪些成员可用。

只要我们的代码不访问它处理的一些类型的对象(或者只要它始终是object类型的成员),泛型类就可以处理任何类型。符合约束的类型参数叫做未绑定的类型参数(unbounded type parameter)。然而,如果代码尝试使用其他成员,编译器会产生一个错误信息。

要让泛型变得更有用,我们需要提供额外的信息让编译器知道参数可以接受哪些类型。这些额外的信息叫做约束(constrain)。只有符合约束的实参才能用于类型参数。


Where子句

约束使用where子句列出。

  • 每一个有约束的类型参数有自己的where子句。
  • 如果形参有多个约束,它们在where子句中使用逗号分隔。
where子句的语法如下:

where TypeParam : constraint,constraint,........

有关where子句的要点如下:

  • 它们在类型参数列表的关闭括号之后列出。
  • 它们不使用逗号或其他符号分隔。
  • 它们可以以任何次序列出。
例:

class  MyClass <T1,T2,T3>

where T2:Customer   //没有分隔符
where T3:IComparable

{...}

约束类型和次序

共有5种类型的约束。

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

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

  • 最多只能有一个主约束,如果有则必须放第一位。
  • 可以有任意多的InterfaceName约束。
  • 如果存在构造函数约束,则必须放在最后。
例:

class SortedList<S>

where S:IComarable<S>{...}

class LinkedList<M,N>

where M:IComparable<M>

where N:ICloneable {...}


class MyDictionary<KeyType,ValueType>

where KeyType :IEnumerable,

new() {...}



19.8 泛型结构

与泛型类相似,泛型结构可以有类型参数和约束。泛型结构的规则和条件与泛型类是一样的。




19.9 泛型接口

泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。泛型接口的声明和非泛型接口的声明差不多,但是需要在接口名称之后的尖括号中有类型参数。


泛型接口的两个额外的能力:

  • 与其他泛型相比,实现不同类型参数的泛型接口是不同的接口。
  • 我们可以在非泛型类型中实现泛型接口。
实现泛型类型接口时,必须没有可能的类型实参组合会在类型中产生两个重复的接口。
另外,泛型接口的名字不会和非泛型冲突。



19.10 泛型委托

泛型委托和非泛型委托非常相似,除了决定哪种方法会被接受的类型参数之外。

  • 要声明泛型委托,在委托名称后、委托参数列表之前的尖括号中放类型参数列表。

delegate R MyDelegate<T,R>(T value);  //R返回类型  <T,R>类型参数 <T>委托形参

  • 注意,在这里有两个参数列表:委托形参列表和类型参数列表。
  • 类型参数的范围包括:
    • 返回值
    • 形参列表
    • 约束子句

另一个泛型委托的示例

C# 3.0的LINQ特性在很多地方使用了泛型委托。
如下代码声明了一个叫做Func的委托,它接受带有两个形参和一个返回值的方法。方法返回的类型被标识为TR,方法参数类型被标识为T1和T2.注意,委托返回的类型在泛型参数列表中的最后一个。

   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 Func<int, int, string>(Simple.PrintString);

           Console.WriteLine("Total :{0}",myDel(15,13));
       }
   }



19.11 泛型方法

与其他泛型不一样,方法是成员,不是类型。

泛型方法可以在泛型和非泛型类以及结构和接口中声明。


声明泛型方法

泛型方法和其他泛型一样,有类型参数列表和可选的约束。

  • 泛型方法和泛型委托相似,有两个参数列表:
    • 封闭在圆括号内的方法参数列表。
    • 封闭在尖括号内的类型参数列表。
  • 要声明泛型方法,需要:
    • 在方法名称和方法参数列表之间放类型参数列表。
    • 在方法参数列表后放可选的约束之句。
public void PrintData<S,T>(S p) where S:Person {...}


调用泛型方法

要调用泛型方法,应该在方法调用时提供类型实参。例如:MyMethod<short,int>();


推断类型

如果我们为方法传入参数,编译器有时可以从方法参数中推断出泛型方法的类型形参中用到的那些类型。这样就可以使方法调用更简单,可读性更强。

例:

public void MyMethod<T>(T myVal) {...}

int MyInt=5;

MyMethod <int>(MyInt)  等同于  MyMethod(MyInt);


19.12 扩展方法和泛型类

扩展方法可以和泛型类结合使用。它允许我们将类中的静态方法关联到不同的泛型类上,还允许我们像调用类构造实例方法一样来调用方法。

和非泛型类一样,泛型类的扩展方法:

  • 必须声明为static;
  • 必须是静态类成员;
  • 第一个参数类型中必须有关键词this,后面是扩展的泛型类的名字。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值