第十七章 泛型(generic)

17.1 什么是泛型

专门为多段代码在不同的数据类型上执行相同指令的情况专门设计

17.2 C#中的泛型

泛型特性提供了一种更优雅的方式,可以让多个类型共享一组代码。泛型允许我们声明类型参数化的代码,可以用不同的类型进行实例化。即可以用 类型占位符 来写代码,然后在创建类的实例时指明真实的类型。

  • 类型不是对象而是对象的模板
  • 泛型类型不是类型而是类型的模板

C# 提供了5种泛型:

  • 类(class)
  • 结构(struct)
  • 接口(interface)
  • 委托(delegate)
  • 方法(method)

前4个是类型,而方法是成员


17.3 泛型类

创建和使用常规的、非泛型的类需两步:声明类和创建类的实例
泛型类不是实际的类,而是模板,须先从他们构建实际的类类型,然后创建这个构建后的类类型的实例

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

17.4 声明泛型类

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

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

如下代码声明了一个SomeClass的泛型类。类型参数列在尖括号中,然后当作真实类型在声明的主体中使用

                 类型参数
                    ↓
class SomeClass < T1, T2 >
{          通常在这些位置使用类型
            ↓                ↓
    public T1 SomeVar = new T1();
    public T2 SomeVar = new T2();
}

在泛型类型声明中并没有特殊的关键字。取而代之的是尖括号中的类型参数列表,它可以区分泛型类与普通类的声明

17.5 创建构造类型

创建了编译类型,需要告诉编译器能使用哪些真实类型来替代占位符(类型参数)。编译器获取这些真实类型并创建构造类型(用来创建真实类对象的模板)。

创建构造类型的语法如下,包括列出类名并在尖括号中提供真实类型来替代类型参数。要替代类型参数的真实类型叫做 类型实参

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


类型参数和类型实参的区别:

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


17.6 创建变量和实例

在创建引用和实例方面,构造类类型的使用和常规类型差不多。如下代码演示了两个类对象的创建。

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

和非泛型类一样,引用和实例可以分开创建,下图可以看出内存中出现的情况与非泛型类是一样的。

  • 泛型类声明下面的第一行在栈上 myInst 分配了一个引用,值是 null
  • 第二行在堆上分配实例,并且把引用赋值给变量

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

下面代码是从 SomeClass泛型类创建了两个类型

  • 一个类型使用 short 和 int 构造
  • 另一个类型使用 int 和 long 构造

17.7 类型参数的约束

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

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

class Simple<T>
{
    static public bool LessThan(T i1, T i2)
    {
        return i1 < i2;     //错误:运算符“<”无法用于“T”和“T”类型的操作符
    }
}

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

17.7.1 Where子句

约束使用Where子句列出。

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

where子句的语法如下:

        类型参数           约束列表
          ↓                  ↓
where TypeParam : constraint, constraint, ...
  ↑             ↑
关键字          冒号
  • 它们在类型参数列表的关闭尖括号之后列出
  • 它们不使用逗号或其他符号分隔
  • 它们可以以任何次序列出
  • where 是上下文关键字,所以可以在其他上下文中使用

如下泛型类有3个类型参数。T1是未绑定,对于T2,只有 Customer 类型或从 Customer 继承的类型的类才能用作类型实参,而对于T3,只有实现 IComparable 接口的类才能用于类型实参。

             未绑定  具有约束
                ↓     ↓            没有分隔符
class MyClass < T1, T2, T3 >           ↓
                   where T2 : Customer         //T2的约束
                   where T3 : IComparable      //T3的约束
{                                         ↑
     ...                              没有分隔符
}
17.7.2 约束类型和次序

共有5中类型的约束

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

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

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

如下声明给出一个where子句的示例:

17.8 泛型方法

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

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

17.8.1 声明泛型方法

泛型方法具有类型参数和可选的约束。

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

注:类型参数列表在方法名称之后,在方法参数列表之前


17.8.2 调用泛型方法

要调用泛型方法,应该在方法调用时提供类型实参:

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

下图演示了一个 DoStuff 的泛型方法的声明,接受两个类型参数。

推断类型

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

下面代码声明了 MyMethod,它接受了一个与类型参数同类型的方法参数。

public void MyMethod <T> (T myVal) { ... }
                      ↑   ↑
                   两个都是T类型

如下所示,如果我们使用 int 类型的变量调用 MyMethod,方法调用中的类型参数的信息就多余了,因为编译器可以从方法参数中得知它是 int。

int myInt = 5;
MyMethod <int> (MyInt);
           ↑      ↑
          两个都是int

由于编译器可以从方法参数中推断类型参数,可省略类型参数和调用中的尖括号:
MyMethod(myInt)

17.12 泛型接口

泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口。泛型接口的声明和非泛型接口的声明差不多,但需要在接口名称之后的尖括号中放置类型参数。
下例代码声明了叫做IMyIfc的泛型接口:

  • 泛型类 Simple 实现了泛型接口
  • Main 实例化了泛型类的两个对象,int 类型和 string 类型
    interface IMyIfc<T>       //泛型接口
    {
        T ReturnIt(T inValue);
    }

    class Simple<S> : IMyIfc<S>     //泛型类
    {
        public S ReturnIt(S inValue) { return inValue; }  //实现泛型接口
    }

    class Program
    {
        static void Main(string[] args)
        {
            var trivInt = new Simple<int>();
            var trivString = new Simple<string>();

            Console.WriteLine("{0}", trivInt.ReturnIt(5));
            Console.WriteLine("{0}", trivString.ReturnIt("Hi there"));
        }
    }

输出如下:
5
Hi there


17.12.1 使用泛型接口的示例

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

  • 与其他泛型相似,实现不同类型参数的泛型接口是不同的接口
  • 我们可以在非泛型类型中实现泛型接口

下例中 Simple 是实现泛型接口的非泛型类。它实现了两个 IMyIfc的实例。一个实例使用 int 类型实例化,一个使用 string 类型实例化。

    interface IMyIfc<T>        //泛型接口
    {
        T ReturnIt(T inValue);
    }

    class Simple : IMyIfc<int>, IMyIfc<string>        //非泛型类
    {
        public int ReturnIt(int inValue) { return inValue; }  //实现 int 类型接口
        public string ReturnIt(string inValue) { return inValue; }  //实现 string 类型接口
    }

    class Program
    {
        static void Main(string[] args)
        {
            Simple trivial = new Simple();

            Console.WriteLine("{0}", trivial.ReturnIt(5));
            Console.WriteLine("{0}", trivial.ReturnIt("Hi there"));
        }
    }

输出结果与上例一致。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容