zoukankan      html  css  js  c++  java
  • 《深入理解C#》整理1-泛型

    泛型是C#2最重要的新特性,同时也是.NET2.0的CLR中最重要的新特性,它实现了类型和方法的参数化(可作为参数传递)。它们增强了性能,是代码更富表现力,并且将大量安全检查从执行时转移到了编译时进行。

    1、为什么需要泛型

    1、使用如ArrayList这类为不同数据类型而设计的类型时,每次foreach都需要隐式的强制转换。强制转换意味着本因由我们为编译器提供更多的信息,转变为让编译器生成一个检查以便执行时运行,这是一个糟糕的选择。如果需要将某处的信息传递给编译器,那么使用你代码的人同样需要该类信息,而保留这类信息的理想位置通常是声明变量或方法的位置。有了泛型,用户在程序中使用错误的参数调用库时,就无法通过编译(注意编译时运行检查相比执行时检查的重要性)。

    2、由于编译器能执行更多的检查,所以执行时检查就能少做;其次,JIT能够更为聪明的处理值类型,能消除很多情况下的装箱拆箱处理;某些情况下,无论是速度还是内存消耗上,使用泛型都会表现的更为优异。泛型的好处就像静态语言较之动态语言的优点:更好的编译时检查,更多代码中能够直接表现的信息,更多的IDE支持,更好的性能。

    2、日常使用的泛型

    1、泛型有两种形式,泛型类型(包括类、接口、委托、结构)和泛型方法。类型参数是真实类型的占位符,在泛型声明中,类型参数(type parameter)需要放在一对尖括号内,以逗号隔开;而使用泛型类型或方法时,需要用真实的类型代替,这类真实的类型即为类型实参(type argument)。

    2、如果没有为泛型类型提供类型实参,那么就是一个未绑定泛型类型;如果指定了类型实参,那么该类型就是一个已构造类型。如果类型是对象的蓝图,那么未绑定泛型类型就是已构造类型的蓝图,它是一种额外的抽象层。而已构造类型可以是开放或关封闭的,开放类型包含一个类型参数,而封闭类型则是不开放的,类型中的每个部分都是明确的。所有代码实际上都是在一个封闭的已构造类型的上下文中执行的。

    image-20201020194027956

    3、在泛型类型中,构造函数不在尖括号中列出类型参数,类型参数从属于类型,而非从属于某个特定的构造函数,所以才会在声明类型时声明。成员(仅限方法)仅在引入新的类型参数时才需要声明。泛型类型是可以重载的,只需要改变一下参数的数量就可以了,这一点同样适用于泛型方法。

    4、泛型方法示例(需要注意的是非泛型类型也可以实现泛型方法):

    image-20201020200404429

    3、深化与提高

    3.1泛型约束

    1、引用类型约束:用于确保使用的类型实参是引用类型,表示成T:Class,且必须是为类型参数指定的第一个约束。类型实参可以是任何类、接口、数组、委托或者是已知引用类型的另一个类型参数。使用此类约束后,可以使用==和!=来比较引用,但是需要注意的是,除非还存在其他约束,否则只能比较引用,即使该类型中重载了那些操作符。

    2、值类型约束:约束表示成T:struct,可以确保使用类型的实参是值类型,包括枚举,但是它将可空类型排除在外。类型参数被约束为值类型后,就不允许使用==和!=进行比较

    3、构造函数类型约束:表示成T:new(),必须是所有类型参数的最后一个约束,它检查类型实参是否有一个可以用于创建类型实参的无参构造函数。这适用于所有值类型,所有没有显式声明构造函数的非静态、非抽象类,以及所有显式声明了一个公共无参构造函数的非抽象类。

    C#规范规定,所有值类型都有一个默认的无参构造函数,而且显式声明的构造函数和无参构造函数是用相同的语法来调用的,具体调用哪一个,需要依赖于编译器正在底层进行的工作。CLI规范则没有这些要求,不过它提供了一个特殊的指令,可以在不指定任何参数的情况下创建默认值。

    4、转换类型约束:允许你指定另一个类型,类型参数必须可以通过一致性、引用或装箱转换隐式地转换为该类型。你还可以规定一个类型实参必须可以转换为另一个类型实参,这称为类型参数约束。示例如下:

    image-20201021194027610

    转换类型约束无法指定枚举约束和委托约束,但其并非是CLR的限制,如果在IL中构造适当的代码是可以工作的

    5、组合约束:没有类型即是引用类型又是值类型,所以不允许此类组合;每个值类型都有一个无参构造函数,所以假如已经有一个值类型约束,就不允许再指定一个构造函数约束;如果存在多个转换类型约束,并且其中一个为类,那么它应该出现在接口的前面,并且不能多次指定同一个接口。

    规范中对约束的分类略有不同,它将其划分为主要约束、次要约束和构造函数约束。主要约束可以分为引用类型约束、值类型约束或使用类的转换类型约束;次要约束为使用接口或其他类型参数的转换类型约束。主要约束是可选的,但只能有一个;次要约束则可以有多个;构造函数约束也是可选的。

    3.2、泛型方法类型实参的类型推断

    为了简化工作,C#2编译器被赋予了一定的智能,让我们在调用方法时,可以不需要显式声明类型实参。其基本步骤如下:

    ①对于每一个方法实参,都尝试用十分简单的技术推断出泛型方法的一些类型实参;

    ②验证步骤1中的所有结果都是一致的,如果不同方法推断出的类型实参为不同类型,推断会失败;

    ③验证泛型方法需要的所有类型实参都已经被推断出来,但也存在不能让编译器推断的部分,可以显式的指定,但必须全部显式指定

    此外需要注意的时类型推断只适用于泛型方法,不适用于泛型类型。

    3.3、实现泛型

    实现泛型时需要留意几个方面:

    1、默认表达式:泛型默认值很少使用,但也有像例如Dictionary<Tkey,TValue>这样可以使用TValue填充输出参数的情况(在方法正常返回前需要赋值);所以C#2提供了默认值表达式default(T);

    2、直接比较:

    ①如果一个类型参数是未约束的,那么且只能在将该类型的值与null进行比较时才能使用==和!=操作符

    ②不能直接比较两个T类型的值

    ③如果类型实参是一个引用类型,会进行正常的引用比较

    ④如果为T提供的类型实参是一个非可空值类型,与null进行比较的结果总是显示它们不相等

    ⑤如果类型实参是可空值类型,那么就会自然而然地与类型的空值进行比较

    ⑥如果一个类型参数被约束成值类型,就完全不能为它使用==和!=。

    ⑦如果被约束成引用类型,那么具体执行的比较将完全取决于类型参数被约束成什么类型。如果它只是一个引用类型,那么执行的是简单的引用比较。如果它被进一步约束成继承自某个重载了==和!=操作符的特定类型,就会使用重载的操作符。但要注意,假如调用者指定的类型实参恰巧也进行了重载,那么这个重载操作符是不会使用的

    ⑧遇到泛型类型时,编译器会在编译未绑定的泛型类型时就解析好所有方法重载,而不是等到执行时,才去为每个可能的方法调用重新考虑是否存在更具体的重载

    3、泛型比较接口

    共有4个主要的泛型接口可用于比较。IComparer和IComparable用于排序(判断某个值是小于、等于还是大于另一个值),而IEqualityComparer和IEquatable通过某种标准来比较两个项的相等性,或查找某个项的散列(通过与相等性概念匹配的方式)

    如果换一种方式来划分这4个接口,IComparaer和IEqualityComparer的实例能够比较两个不同的值,而IComparable和<TIEquatable的实例则可以比较它们本身和其他值

    4、高级泛型

    1、静态字段和静态构造函数

    就像实例字段从属于一个实例一样,静态字段从属于声明它们的类型,因而每个封闭类型都有它自己的静态字段集。同样的规则也适用于静态初始化器和静态构造函数。虽然一个泛型类型可能嵌套在另一个泛型类型中,而且一个类型可能有多个泛型参数,但是它和非泛型类型一样,每个不同的类型实参列表都被看做一个不同的封闭类型。

    2、JIT编译器如何处理泛型

    对于所有不同的封闭类型,JIT的职责就是将泛型类型的IL转换成本地代码,使其能真正运行起来。JIT为每个以值类型作为类型实参的封闭类型都创建不同的代码。然而,所有使用引用类型作为类型实参的封闭类型都共享相同的本地代码。之所以能这样做,是由于所有引用都具有相同的大小(32位CLR上是4字节,64位CLR上是8字节)。无论实际引用的是什么,引用构成的数组的大小是不会发生变化的,栈上一个引用所需的空间始终是相同的。

    image-20201021204622044

    在.NET 1.1中,为了将单独的字节添加到一个ArrayList中,需要对每个字节进行装箱,并存储对每个已装箱值的引用。使用List则无此问题——List用一个T[]类型的成员数组替代了ArrayList中的object[],而且那个数组具有恰当的类型,会占用恰当(大小)的空间。上图展示了一个ArrayList和一个List,它们分别包含6个相同的值。数组本身拥有不止6个元素,从而允许扩充。List和ArrayList都有一个缓冲区,在必要时会创建一个更大的缓冲区。

    假定ArrayList使用的是一个32位CLR,每个已装箱的字节都要产生8字节的对象开销,另加4字节用于数据本身。除此之外,引用本身也要消耗4字节。所以,每个有效数据都要花费至少16字节。除此之外,缓冲区中还要为引用准备一些额外的未使用的空间。List中的每个字节都占用元素数组中一个字节的空间。缓冲区仍有“浪费”的空间,可用于新增项,但最起码,每个未使用的元素只会浪费一个字节。不仅节省了空间,而且加快了执行速度。现在不需要花时间进行装箱,不需要因为对字节进行拆箱而检查类型,也不需要对不再引用的已装箱值进行垃圾回收。

    3、泛型迭代

    对集合执行的最常见的操作之一就是遍历(迭代)它的所有元素。最简单的办法就是使用foreach语句。在C# 1中,为了使用foreach,集合要么必须实现System.Collections. IEnumerable接口,要么必须有一个类似的GetEnumerator()方法。C# 2使过程变得容易了一些。foreach语句的规则得到了扩展,现在还可以使用System. Collections.Generic.IEnumerable接口及其搭档IEnumerator

    在少数情况下,当需要为自己的某个类型实现迭代时,你会发现由于IEnumerable扩展了旧的IEnumerable接口,所以要实现两个不同的方法:IEnumerator GetIEnumerator()和IEnumerator GetIEnumerator();如果使用“显式接口实现”来实现IEnumerable,就可以用一个“普通”的方法来实现IEnumerable。幸好,由于IEnumerator扩展了IEnumerator,所以两个方法可以使用相同的返回值,而且只需调用一下泛型版本,就可实现非泛型方法。

    上述两个方法只是返回类型不同,而根据C#的重载规则,一般不允许写这样的两个方法。这样做的目的是基于一个基本原则:如果没有问题,泛型接口都应该继承对应的非泛型接口,这样可以实现协变性。例如,假如以前为.NET 1.1写的一个函数要获取IEnumerable类型的参数,而现在有了IEnumerable。假如IEnumerable不继承IEnumerable,这个函数就不能接受IEnumerable类型的参数

    4、泛型和反射

    1、对泛型类型使用typeof:可通过两种方式作用于泛型类型,一种方式是获取泛型类型定义,另一种方式是获取特定的已构造类型。为了获取泛型类型定义,需要提供声明的类型名称,删除所有类型参数名称,但保留逗号;为了获取已构造类型,需要采取与声明泛型类型变量时相同的方式指定类型实参就可以了。示例:

    image-20201021210758593

    2、System.Type的属性和方法:新增的许多新的方法和属性中有两个方法是最重要的GetGenericTypeDefinition和MakeGenericType。两个方法所执行的操作实际上是相反的——第一个作用于已构造的类型,获取它的泛型类型定义;第二个作用于泛型类型定义,返回一个已构造类型。示例:

    image-20201021211041544

    3、反射泛型方法:首先获取泛型方法定义,然后使用MakeGenericMethod返回一个已构造的泛型方法。从泛型类型定义获取的方法不能直接调用——相反,必须从一个已构造的类型获取方法。无论泛型方法还是非泛型方法,这一点都适用


    总结:

    泛型的3个主要优点:编译时的类型安全性、性能和代码的表现力。

    值类型在性能上的获益是最大的。在强类型的泛型API中使用时,它们不再需要装箱和拆箱。引用类型的性能也有所提升,只是幅度较小。

    使用泛型,代码能更清楚地表达其意图,而不是只能通过一条注释或者一个很长的变量名来准确地描述涉及的类型。

    作者:Jscroop
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    java常用类
    java throw和catch同时使用
    HTML5 input 类型: email及url
    Android中集成支付宝
    HTML5 预加载
    SQLite数据库
    Android开发中如何加载API源码帮助开发
    Java中的static
    HTML5 Web Storage 特性
    gdal1.10编译经验
  • 原文地址:https://www.cnblogs.com/Jscroop/p/13868268.html
Copyright © 2011-2022 走看看