《NET CLR via C#》---第十二章(泛型)

news/2024/9/24 16:38:38

泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。

简单来说,开发人员先定义好算法,必然排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。

泛型为开发人员提供了以下优势:

  • 源代码保护:使用泛型算法的开发人员不需要访问算法的源代码。然后,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
  • 类型安全:将泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与指定数据类型兼容的对象才能用于算法。
  • 更清晰的代码:由于编译器强制类型安全性,所以减少了源代码中必须进行的强制类型转换次数,使代码更容易编写和维护。
  • 更佳的性能:没有泛型的时候,要想定义常规化的算法,它的所有成员都要定义成操作Object数据类型。要用这个算法来操作值类型的实例,CLR必须在调用算法的成员之前对值类型实例进行装箱。

为了理解性能优化,我们可以通过如下的代码进行测试:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;class Program
{static void Main(string[] args){ValueTypePerfTest();ReferenceTypePerfTest();}private static void ValueTypePerfTest(){const int count = 100000000;using(new OperationTimer("List<int>")){List<int> l = new List<int>();for (int i = 0; i < count; i++){l.Add(i);int x = l[i];}l = null;}using (new OperationTimer("ArrayList of int")){ArrayList l = new ArrayList();for (int i = 0; i < count; i++){l.Add(i);           // 装箱int x = (int)l[i];  // 拆箱}l = null;}}private static void ReferenceTypePerfTest(){const int count = 100000000;using (new OperationTimer("List<string>")){List<string> l = new List<string>();for (int i = 0; i < count; i++){l.Add("X");string x = l[i];}l = null;}using (new OperationTimer("ArrayList of string")){ArrayList l = new ArrayList();for (int i = 0; i < count; i++){l.Add("X");          string x = (string)l[i];  }l = null;}}
}internal sealed class OperationTimer : IDisposable
{private Stopwatch stopwatch;private string text;private int collectionCollect;public OperationTimer(string text){PrepareForOperation();this.text = text;collectionCollect = GC.CollectionCount(0);stopwatch = Stopwatch.StartNew();}public void Dispose(){Console.WriteLine($"{stopwatch.Elapsed} (GCs={GC.CollectionCount(0) - collectionCollect}) {text}");}private static void PrepareForOperation(){GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();}
}

最后会得到如下输出:
image
很明显,在操作值类型时,泛型算法比非泛型算法快了近乎11倍了。此外用ArrayList操作值类型,会造成大量装箱,最终要进行293次垃圾回收。

不过,引用类型,差异则没有那么明显了,GC一样都是0,时间虽然泛型略快一点,但也不像值类型有这么大的差距。

开放类型和封闭类型

具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型,值类型,接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,CLR禁止构造开放开放类型的任何实例。

代码引用泛型类型时可指定一组泛型类型参数。为所有类型参数都传递了实际的数据类型,类型就称为封闭类型。CLR允许构造封闭类型的实例。例如以下例子中,我分别尝试用反射的方法去实例化一个开放类型和封闭类型:

using System;
using System.Collections.Generic;class Program
{static void Main(string[] args){try{Type t1 = typeof(Dictionary<,>);var o1 = Activator.CreateInstance(t1);Console.WriteLine($"{t1.ToString()}实例化传功");}catch(ArgumentException e){Console.WriteLine(e);}try{Type t2 = typeof(Dictionary<int, int>);var o2 = Activator.CreateInstance(t2);Console.WriteLine($"{t2.ToString()}实例化传功");}catch(ArgumentException e){Console.WriteLine(e);}}
}--------输出结果------
ystem.ArgumentException: Cannot create an instance of System.Collections.Generic.Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.at System.RuntimeType.CreateInstanceCheckThis()at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)at System.Activator.CreateInstance(Type type, Boolean nonPublic, Boolean wrapExceptions)at System.Activator.CreateInstance(Type type)at Program.Main(String[] args) in C:\Users\LH89\source\repos\ConsoleApp3\Program.cs:line 13
System.Collections.Generic.Dictionary`2[System.Int32,System.Int32]实例化传功

可以看出,只有封闭类型才能实例化成功。从输出类型可以看出,类型名以“`”字符和一个数字结尾。数字代表类型的元数,也就是类型要求的类型参数个数。

还要注意,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,则List<int>和List<string>不会共享这些静态字段。另外,假如泛型类型定义了静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。

泛型类型和继承

泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义了一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。

class Program
{static void Main(string[] args){TypeNode<int> a = new TypeNode<int>();TypeNode<string> b = new TypeNode<string>();Node start = new Node();start.next = a;start.next.next = b;}
}public class Node
{public Node next;
}public class TypeNode<T> : Node { }

例如上例中,Node<int>和Node<string>都继承Node基类,我们也可以利用到多态的特点,将值类型和引用类型装入同一个链表中,同时避免了值类型装箱拆箱的特点。

同一性

为了对语法进行增强,有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有类型实参,例如:

public class DateTimeList : List<DateTime> { }

此时就可以简化创造列表代码:

List<DateTime> list1 = new List<DateTime>(); ->
DateTimeList list2 = new DateTimeList();

这样做表面上是简化了代码书写,但其实不妥!绝对不要出于增强源码可读性的目的来定义一个新类。这样会散失同一性(identity)和相等性(equivalence),例如我们此时比较 list1.GetType() == list2.GetType(),会返回一个false,因为比较的是不同类型的两个对象。这也意味着如果方法的原型接受一个DateTimeList,我们无法把List<DateTime>传给他,这会导致开发非常混乱。

C#也考虑到了泛型的书写困难,所以他提供了简化的语法来引用泛型封闭类型,例如我们可以在源文件顶部这样声明,就不会丧失同一性也能保证代码可读性:

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

代码爆炸

使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码。这样有个缺点:CLR要为每种不同的方法/类型组合生成本机代码,这种现象称为代码爆炸。它会使得应用程序的工作集显著增大,从而损害性能。

幸好,CLR采用了一些优化措施缓解了代码爆炸:

  1. 假如为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。
  2. CLR认为所有引用类型实参都完全相同,所以代码能够共享。例如,CLR为List<String>的方法编译的代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。CLR之所以能执行这个优化,是因为所有引用类型的实参或变量实际只是指向堆上对象的指针,而所有对象指针都以相同的方式操纵。
    但如果类型实参是值类型,CLR就必须专门为那个值类型生成本机代码。这是因为值类型的大小不定。即使2个值类型大小一样(比如int32和uint32,都是32位),CLR仍然无法共享代码,因为可能要用不同的本机CPU指令来操纵这些值。

泛型接口

没有泛型接口,每次用非泛型接口(入IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。因此,CLR提供了对泛型接口的支持,例如:

public interface IAnimal<T>
{T animal { get; }
}public class Dog : IAnimal<Dog>
{public Dog animal => new Dog();
}public class Number : IAnimal<int>
{public int animal => 0;
}

泛型委托

CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。
具体例子先暂时跳过,看完17章泛型再来补充

泛型方法

泛型方法的存在,为开发人员提供了极大的灵活性。例如:

    private void Swap<T>(ref T o1, ref T o2){T temp = o1;o1 = o2;o2 = temp;}

有一点要注意的是,作为out/ref实参传递的变量必须具有与方法参数相同的类型,以防止损坏类型安全性。

可验证性和约束

约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型指向更多操作。例如:

    public static T Min<T>(T o1, T o2) where T : IComparable<T>{if(o1.CompareTo(o2) < 0){return o1;}return o2;}

C#的where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。

约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。

重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法指定的约束上,事实上,根本不允许为重写的方法的类型参数指定任何约束。单类型参数的名称是可以改变的。

主要约束

类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void。

指定引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的类型。例如:

    public static T Min<T>(T o1, T o2) where T : List<int>{return o1.Count < o2.Count ? o1 : o2;}

有两个特殊的主要约束:class和struct。
class约束:向编译器承诺类型实参是引用类型。(任何类类型、接口类型、委托类型或者数组类型都满足这个约束)
struct约束:向编译器承诺类型实参是值类型。(包括枚举在内的任何值类型都满足这个约束,但编译器和CLR将任何System.Nullable<T>值类型视为特殊类型,不满足这个struct约束)

原因是Nullable<T>类型将它的类型参数约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的递归类型。

次要约束

类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。这种约束向编译器承诺,类型实参实现了接口,由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。

还有一种次要约束称为类型参数约束,有时也称为裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么是约束的类型,要么是约束的类型的派生类。例如:

    private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase{List<TBase> baseList = new List<TBase>(list.Count);foreach(var item in list){baseList.Add(item);}return baseList;}

构造器约束

类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。例如:

    private class ConstructorConstaint<T> where T : new() {public static T Factory(){return new T();}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/64225.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

Java对接企业微信审批回调

一、需求说明用Java程序对接企业微信的审批流程,获取审批数据数据,例如:报销费用金额二、操作步骤 2.1、分析需求根据需求分析其实也不知道该怎么去做对接,通过不断试错,发现不需要对接第三方应用,也就是不需要进入服务商后台页面,只需要在自建应用是做好配置即可。 具体…

Vuex 和 Vue-router 知识点

Vuexactions里面才能做异步操作(访问后端API ajax请求 访问数据库中的数据) mutations原子操作,同步操作Vue-routermode:history 路由配置该怎么配置就怎么配置 route: [...]配置:path 和 component懒加载:component:对应一个函数,import导入这个组件。前面说过用impor…

Chrome浏览器下载时提示“保留”

1. 提示情况 具体提示情况情况如下:2. 解决方法 2.1. 选中地址栏“查看网站信息” 具体弹出框如下 2.2. 修改“网站设置” 在确认网站安全的情况下,把“自动下载项”修改为“允许”,把“不安全内容”修改为“允许”。2.3. 关闭“网站设置”页面 重新下载,就不在出现“保留…

利用IDEA创建Web Service服务端和客户端的详细过程

创建服务端 一、file–>new–>project 二、点击next后输入服务端名,点击finish,生成目录如下三、在 HelloWorld.Java 文件中右击,选 WebServices ,再选 Generate Wsdl From Java Code ,确定点击ok会自动给我们生成HelloWorld.wsdl,画红线处注意四、配置tomcat,此…

9.24上课记录

今日收获:可以用来阅读知网等 https://cajviewer.cnki.net

怎么去除URL地址中index.php文件?

PbootCMS内核框架采用单入口pathinfo方式,因此所有地址的访问默认都带有index.php, 要去除它很简单,只要开启伪静态即可扫码添加技术【解决问题】专注中小企业网站建设、网站安全12年。熟悉各种CMS,精通PHP+MYSQL、HTML5、CSS3、Javascript等。承接:企业仿站、网站修改、网…