[C# 基础知识系列]专题七: 泛型深入理解(一) (转载)
引言:
在上一个专题中介绍了C#2.0 中引入泛型的原因以及有了泛型后所带来的好处,然而上一专题相当于是介绍了泛型的一些基本知识的,对于泛型的性能为什么会比非泛型的性能高却没有给出理由,所以在这个专题就中将会介绍原因和一些关于泛型的其他知识。
一、泛型类型和类型参数
泛型类型和其他int,string一样都是一种类型,泛型类型有两种表现形式的:泛型类型(包括类、接口、委托和结构,但是没有泛型枚举的)和泛型方法。那什么样的类、接口、委托和方法才称作泛型类型的呢 ?我的理解是类、接口、委托、结构或方法中有类型参数就是泛型类型,这样就有类型参数的概念的。 类型参数 ——是一个真实类型的一个占位符(我想到一个很形象的比喻的,比如大家在学校的时候,一到中午下课的时候食堂人特别多的,所以很多应该都有用书本占位置的习惯的, 书本就相当于一个占位符,真真坐在位置上的当然是自己的,讲到占位置,以前听过我同学说,他们班有个很牛逼的MM,中午下完课的时候用手机占位子的,等它打完饭回来的时候手机已经不见, 当时听完我就和我同学说,你们班这位女生真牛逼的,后面我们就),泛型声明中,类型参数必须放在一对尖括号里面(即<>这个符号),并且用逗号分隔多个类型参数,如List<T>类中T就是类型参数,在使用泛型类型或方法的时候,我们要用真实类型来代替,就像用书本占位子一个,书本只是暂时的在那个位置上,等打好饭了就要换成你坐在位置上了,同样在C#中泛型也是同样道理,类型参数只是暂时的在那个位置,真真使用中要用真实的类型去代替它的位置,此时我们把真实类型又取名为类型实参,如上一专题的代码中List<int>,类型实参就是int(代替T的位置)。
如果没有为类型参数提供类型实参,此时我们就声明了一个未绑定的泛型类型,如果指定了类型实参,此时的类型就叫做已构造类型(这里同样可以以书占位置去理解),然而已构造类型又可以是开放类型或封闭类型的,这里先给出这个两个概念的定义的:开放类型——具有类型参数的类型就是开放类型(所有的未绑定的泛型类型都属于开放类型的),封闭类型——为每个类型参数都传递了实际的数据类型。对于开放类型,我们创建开放类型的实例。
注意:在C#代码中,我们唯一可以看到未绑定泛型类型的地方(除了作为声明之外)就是在typeof操作符里。
下面通过以下代码来更好的说明这点:
using System; using System.Collections.Generic; namespace CloseTypeAndOpenType { // 声明开放泛型类型 public sealed class DictionaryStringKey<T> : Dictionary<string, T> { } public class Program { static void Main(string[] args) { object o = null; // Dictionary<,>是一个开放类型,它有2个类型参数 Type t = typeof(Dictionary<,>); // 创建开放类型的实例(创建失败,出现异常) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<>也是一个开放类型,但它有1个类型参数 t = typeof(DictionaryStringKey<>); // 创建该类型的实例(同样会失败,出现异常) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<int>是一个封闭类型 t = typeof(DictionaryStringKey<int>); // 创建封闭类型的一个实例(成功) o = CreateInstance(t); Console.WriteLine("对象类型 = " + o.GetType()); Console.Read(); } // 创建类型 private static object CreateInstance(Type t) { object o = null; try { // 使用指定类型t的默认构造函数来创建该类型的实例 o = Activator.CreateInstance(t); Console.WriteLine("已创建{0}的实例", t.ToString()); } catch(Exception ex) { Console.WriteLine(ex.Message); } return o; } } }
运行结果为(从结果中也可以看出开放类型不能创建该类型的一个实例,异常信息中指出类型中包含泛型参数):
二、泛型类型中的静态字段和静态构造函数
首先实例字段是属于一个实例的,静态字段是从属于它们声明的类型,即如果在某个Myclass类中声明了一个静态字段field,则不管创建Myclass的多少个实例,也不管从Myclass中派生出多少个实例,都只有一个Myclass.x字段。然而每个封闭类型都有它自己的静态字段(使用类型实参时,实际上CLR会定义一个新的类型对象, 所以每个静态字段都是不一样对象里面的静态字段,所以才会每个都有各自的值) 通过以下代码来更好说明下——每个封闭类型都有它自己的静态字段:
namespace GenericStaticFieldAndStaticFunction { // 泛型类,具有一个类型参数 public static class TypeWithStaticField<T> { public static string field; public static void OutField() { Console.WriteLine(field+":"+typeof(T).Name); } } // 非泛型类 public static class NoGenericTypeWithStaticField { public static string field; public static void OutField() { Console.WriteLine(field); } } class Program { static void Main(string[] args) { // 使用类型实参时,实际上CLR会定义一个新的类型对象 // 所以每个静态字段都是不一样对象里面的静态字段,所以才会每个都有各自的值 // 对泛型类型类的静态字段赋值 TypeWithStaticField<int>.field = "一"; TypeWithStaticField<string>.field = "二"; TypeWithStaticField<Guid>.field = "三"; // 此时filed 值只会有一个值,每个赋值都是改变了原来的值 NoGenericTypeWithStaticField.field = "非泛型类静态字段一"; NoGenericTypeWithStaticField.field = "非泛型类静态字段二"; NoGenericTypeWithStaticField.field = "非泛型类静态字段三"; NoGenericTypeWithStaticField.OutField(); // 证明每个封闭类型都有一个静态字段 TypeWithStaticField<int>.OutField(); TypeWithStaticField<string>.OutField(); TypeWithStaticField<Guid>.OutField(); Console.Read(); } } }
namespace GenericStaticFieldAndStaticFunction { // 泛型类,具有一个类型参数 public static class TypeWithStaticField<T> { public static string field; public static void OutField() { Console.WriteLine(field+":"+typeof(T).Name); } } // 非泛型类 public static class NoGenericTypeWithStaticField { public static string field; public static void OutField() { Console.WriteLine(field); } } class Program { static void Main(string[] args) { // 使用类型实参时,实际上CLR会定义一个新的类型对象 // 所以每个静态字段都是不一样对象里面的静态字段,所以才会每个都有各自的值 // 对泛型类型类的静态字段赋值 TypeWithStaticField<int>.field = "一"; TypeWithStaticField<string>.field = "二"; TypeWithStaticField<Guid>.field = "三"; // 此时filed 值只会有一个值,每个赋值都是改变了原来的值 NoGenericTypeWithStaticField.field = "非泛型类静态字段一"; NoGenericTypeWithStaticField.field = "非泛型类静态字段二"; NoGenericTypeWithStaticField.field = "非泛型类静态字段三"; NoGenericTypeWithStaticField.OutField(); // 证明每个封闭类型都有一个静态字段 TypeWithStaticField<int>.OutField(); TypeWithStaticField<string>.OutField(); TypeWithStaticField<Guid>.OutField(); Console.Read(); } } }
运行结果:
同样每个封闭类型都有一个静态构造函数的,通过下面的代码可以让大家更加明白这点:
// 静态构造函数的例子 public static class Outer<Tx> { // 嵌套类 public class Inner<Ty> { // 静态构造函数 static Inner() { Console.WriteLine("Outer<{0}>.Inner<{1}>", typeof(Tx), typeof(Ty)); } public static void Print() { } } } class Program { static void Main(string[] args) { #region 静态函数的演示 // 静态构造函数会运行多次 // 因为每个封闭类型都有单独的一个静态构造函数 Outer<int>.Inner<string>.Print(); Outer<int>.Inner<int>.Print(); Outer<string>.Inner<int>.Print(); Outer<string>.Inner<string>.Print(); Outer<object>.Inner<string>.Print(); Outer<object>.Inner<object>.Print(); Outer<string>.Inner<int>.Print(); Console.Read(); #endregion } }
运行结果:
从上图的运行结果可能会发现,我们代码中7个需要输出的,但是结果中只有6个结果输出的,这是因为任何封闭类型的静态构造函数只执行一次,最后一行的 Outer<string>.Inner<int>.Print();这行不会产生第7行输出, 因为Outer<string>.Inner<int>.Print();的静态构造函数在之前已经执行过的(第三行已经执行过了)。
三、编译器如何解析泛型
在上一个专题中,我只是贴出了泛型与非泛型的比较结果来说明泛型具有高性能的好处,却没有给出具体导致泛型比非泛型效率高的原因,所以在这个部分来剖析下泛型效率的具体原因。
这里先贴出上一个专题中说明泛型高性能好处的代码,然后再查看IL代码来说明泛型的高性能(针对泛型和非泛型,C#编译器是如何解析为IL代码的):
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; namespace GeneralDemo { public class Program { static void Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); // 非泛型数组 ArrayList arraylist = new ArrayList(); // 泛型数组 List<int> genericlist= new List<int>(); // 开始计时 stopwatch.Start(); for (int i = 1; i < 10000000; i++) { //genericlist.Add(i); arraylist.Add(i); } // 结束计时 stopwatch.Stop(); // 输出所用的时间 TimeSpan ts = stopwatch.Elapsed; string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds/10); Console.WriteLine("运行的时间: " + elapsedTime); Console.Read(); } } }
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; namespace GeneralDemo { public class Program { static void Main(string[] args) { Stopwatch stopwatch = new Stopwatch(); // 非泛型数组 ArrayList arraylist = new ArrayList(); // 泛型数组 List<int> genericlist= new List<int>(); // 开始计时 stopwatch.Start(); for (int i = 1; i < 10000000; i++) { //genericlist.Add(i); arraylist.Add(i); } // 结束计时 stopwatch.Stop(); // 输出所用的时间 TimeSpan ts = stopwatch.Elapsed; string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds/10); Console.WriteLine("运行的时间: " + elapsedTime); Console.Read(); } } }
当使用非泛型的的ArrayList数组时,IL的代码如下(这里只是贴出了部分主要的中间代码,具体的大家可以下载示例源码用IL反汇编程序查看的):
IL_001f: ldloc.1 IL_0020: ldloc.3 IL_0021: box [mscorlib]System.Int32 IL_0026: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object) IL_002b: pop IL_002c: nop IL_002d: ldloc.3 IL_002e: ldc.i4.1 IL_002f: add
在上面的IL代码中,我用红色的标记的代码主要是在执行装箱操作(装箱过程肯定是要消耗的事件的吧, 就像生活中寄包裹一样,包装起来肯定是要花费一定的时间的, 装箱操作同样会,然而对于泛型类型就可以避免装箱操作,下面会贴出使用泛型类型的IL代码的截图)——这个操作也是影响非泛型的性能不如泛型类型的根本原因。然而为什么使用ArrayList类型在调用Add方法来向数组添加元素之前要装箱的呢?原因其实主要出在Add方法上的, 大家可以用Reflector反射工具查看ArrayList的Add方法定义,下面是一张Add方法原型的截图:
从上面截图可以看出,Add(objec value)需要接收object类型的参数,然而我们代码中需要传递的是int实参,此时就需要会发生装箱操作(值类型int转化为object引用类型,这个过程就是装箱操作),这样也就解释了为什么调用Add方法会执行装箱操作的, 同时也就说明泛型的高性能的好处。
下面是使用泛型List<T>的IL代码截图(从图片中可以看出,使用泛型时,没有执行装箱的操作,这样就少了装箱的时间,这样当然就运行的快了,性能就好了。):
四、小结
说到这里本专题的内容也就介绍结束了,本专题主要是进一步介绍了泛型的其他内容的,由于篇幅的关于我将泛型的其他内容放在下一专题中,如果都在放在这个专题中内容会显得非常多,这样也不利于大家的消化和大家的阅读,所以我在下一个专题中继续介绍泛型的其他的一些内容。
[C# 基础知识系列]专题七: 泛型深入理解(一) (转载)的更多相关文章
- [C# 基础知识系列]专题六:泛型基础篇——为什么引入泛型
引言: 前面专题主要介绍了C#1中的2个核心特性——委托和事件,然而在C# 2.0中又引入一个很重要的特性,它就是泛型,大家在平常的操作中肯定会经常碰到并使用它,如果你对于它的一些相关特性还不是很了解 ...
- [C# 基础知识系列]专题四:事件揭秘 (转载)
引言: 前面几个专题对委托进行了详细的介绍的,然后我们在编写代码过程中经常会听到“事件”这个概念的,尤其是写UI的时候,当我们点击一个按钮后VS就会自动帮我们生成一些后台的代码,然后我们就只需要在Cl ...
- [C# 基础知识系列]专题九: 深入理解泛型可变性
引言: 在C# 2.0中泛型并不支持可变性的(可变性指的就是协变性和逆变性),我们知道在面向对象的继承中就具有可变性,当方法声明返回类型为Stream,我们可以在实现中返回一个FileStream的类 ...
- [C# 基础知识系列]专题一:深入解析委托——C#中为什么要引入委托
转自http://www.cnblogs.com/zhili/archive/2012/10/22/Delegate.html 引言: 对于一些刚接触C# 不久的朋友可能会对C#中一些基本特性理解的不 ...
- [C# 基础知识系列]专题八: 深入理解泛型(二)
引言: 本专题主要是承接上一个专题要继续介绍泛型的其他内容,这里就不多说了,就直接进入本专题的内容的. 一.类型推断 在我们写泛型代码的时候经常有大量的"<"和"& ...
- [C# 基础知识系列]专题五:当点击按钮时触发Click事件背后发生的事情 (转载)
当我们在点击窗口中的Button控件VS会帮我们自动生成一些代码,我们只需要在Click方法中写一些自己的代码就可以实现触发Click事件后我们Click方法中代码就会执行,然而我一直有一个疑问的—— ...
- 方法字段[C# 基础知识系列]专题二:委托的本质论
首先声明,我是一个菜鸟.一下文章中出现技术误导情况盖不负责 引言: 上一个专题已和大家分享了我懂得的——C#中为什么须要委托,专题中简略介绍了下委托是什么以及委托简略的应用的,在这个专题中将对委托做进 ...
- [C# 基础知识系列]专题十六:Linq介绍
转自http://www.cnblogs.com/zhili/archive/2012/12/24/Linq.html 本专题概要: Linq是什么 使用Linq的好处在哪里 Linq的实际操作例子— ...
- [C# 基础知识系列]专题三:如何用委托包装多个方法——委托链 (转载)
引言: 上一专题介绍了下编译器是如何来翻译委托的,从中间语言的角度去看委托,希望可以帮助大家进一步的理解委托,然而之前的介绍都是委托只是封装一个方法,那委托能不能封装多个方法呢?因为生活中经常会听到, ...
随机推荐
- WDA基础十六:ALV的颜色
这里介绍三种类型的颜色:列的背景色,单元格的背景色,单元格文本的颜色. 1.给ALV结构添加颜色控制字段: 三个字段都是同一类型:WDY_UIE_LIBRARY_ENUM_TYPE COL_BACKG ...
- Loadrunner11中webservice协议脚本总结
Loadrunner11中webservice协议脚本总结 简介 webservices协议是建立可交互操作的分布式应用程序的新平台,它通过一系列的标准和协议来保证程序之间的动态连接,其中最基 ...
- Android proguard混淆签名打包出现"android proguard failed to export application"解决方案
刚刚接触安卓,不是很熟悉.发现之前可以正常打包的项目出现添加混淆再进行打包签名的APK之后提示"android proguard failed to export application&q ...
- visual studio 找不到模板信息
问题: 创建项目提示“ 找不到visual studio模板信息” 解决方案: 打开Visual Studio 在菜单->工具->选项->项目和解决方案-> "Vi ...
- 图融合之加载子图:Tensorflow.contrib.slim与tf.train.Saver之坑
import tensorflow as tf import tensorflow.contrib.slim as slim import rawpy import numpy as np impor ...
- react 的基础
首先下载React 的安装包,可以到官网下载.也可以使用React Demos 已经自带 React 源码,不用另外安装,只需把这个库拷贝到硬盘中使用. (可参考http://www.ruanyife ...
- [LeetCode] 80. Remove Duplicates from Sorted Array II ☆☆☆(从有序数组中删除重复项之二)
https://leetcode.com/problems/remove-duplicates-from-sorted-array-ii/discuss/27976/3-6-easy-lines-C% ...
- Spring Boot(一)
(一)如何使用IDEA新建一个Spring Boot项目 https://www.cnblogs.com/wmyskxz/p/9010832.html
- qt程序编译错误:could not exec ‘/usr/lib/x86_64-linux-gnu/qt4/bin/qmake’
linux下安装Qt5.7后添加qmake环境变量后出现错误 执行: qmake -v 出现错误:qmake: could not exec ‘/usr/lib/x86_64-linux-gnu/qt ...
- 大量数据的excel导出
对于大型excel的创建且不会内存溢出的,就只有SXSSFWorkbook了.它的原理很简单,用硬盘空间换内存(就像hash map用空间换时间一样). private void writeToAla ...