C# 泛型深入理解介绍_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会定义一个新的类型对象, 所以每个静态字段都是不一样对象里面的静态字段,所以才会每个都有各自的值) 通过以下代码来更好说明下——每个封闭类型都有它自己的静态字段:

复制代码 代码如下:

View Code

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();
}
}
}

当使用非泛型的的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代码截图(从图片中可以看出,使用泛型时,没有执行装箱的操作,这样就少了装箱的时间,这样当然就运行的快了,性能就好了。):

四、小结

说到这里本专题的内容也就介绍结束了,本专题主要是进一步介绍了泛型的其他内容的,由于篇幅的关于我将泛型的其他内容放在下一专题中,如果都在放在这个专题中内容会显得非常多,这样也不利于大家的消化和大家的阅读,所以我在下一个专题中继续介绍泛型的其他的一些内容。

下面先附上泛型专题中用到的所有Demo的源代码:GeneralDemo_jb51.rar

时间: 2024-09-14 12:14:22

C# 泛型深入理解介绍_C#教程的相关文章

C# 泛型数组学习小结_C#教程

C# 泛型和数组在 C# 2.0 中,下限为零的一维数组自动实现 IList<T>.这使您可以创建能够使用相同代码循环访问数组和其他集合类型的泛型方法.此技术主要对读取集合中的数据很有用.IList<T> 接口不能用于在数组中添加或移除元素:如果试图在此上下文中调用 IList<T> 方法(如数组的 RemoveAt),将引发异常.下面的代码示例演示带有 IList<T> 输入参数的单个泛型方法如何同时循环访问列表和数组,本例中为整数数组. C# 泛型和数组

C#基础知识之this关键字介绍_C#教程

一.this可以代表引用类的当前实例,包括继承而来的方法,通常可以省略. 复制代码 代码如下: public class Person {     public string Name { get; set; }     public int Age { get; set; }     public Person(string Name, int Age)     {         this.Age = Age;         this.Name = Name;     } } 这个不用多说

C#基础知识之new关键字介绍_C#教程

一.运算符 用于创建对象和调用构造函数.这种大家都比较熟悉,没什么好说的了. 二.修饰符 在用作修饰符时,new 关键字可以显式隐藏从基类继承的成员. 无new关键字代码: 有new关键字代码: 结果: 注意: 在子类中用 new 关键字修饰定义的与父类中同名的方法,叫覆盖.覆盖不会改变父类方法的功能. 当子类创建父类时,代码中A c = new B(),覆盖不会改变父类的功能.依然还是调用父类的功能. 三.new 约束 用于在泛型声明中约束可能用作类型参数的参数的类型. public clas

C#零基础学习理解委托_C#教程

   说来惭愧,在大学的课程中,竟然没有听说过委托这个名称.那么今天我就带着大家一起探讨下委托和事件. 咱们先来看下委托 我主要从以下几个方面讲解 1,  为什么使用委托  2.什么是委托  3.委托如何使用 为什么使用委托? 委托是c#中非常重要的一个概念,使用委托使程序员可以将方法引用封装在委托对象内.然后可以将该委托对象传递给可调用所引用方法的代码,而不必在编译时知道将调用哪个方法.与C或C++中的函数指针不同,委托是面向对象,而且是类型安全的. 什么是委托? 委托是一种引用方法的类型,一

C# 中的var关键字详细介绍_C#教程

C# var关键字详解 var 是3.5新出的一个定义变量的类型 其实也就是弱化类型的定义 VAR可代替任何类型 编译器会根据上下文来判断你到底是想用什么类型的 至于什么情况下用到VAR 我想就是你无法确定自己将用的是什么类型 就可以使用VAR 类似 OBJECT 但是效率比OBJECT高点. 或者通俗的讲:   var可以理解为匿名类型,我们可以认为它是一个声明变量的占位符.它主要用于在声明变量时,无法确定数据类型时使用. 使用var定义变量时有以下四个特点: 1. 必须在定义时初始化.也就是

C#中数组Array,ArrayList,泛型List详细对比_C#教程

在C#中数组Array,ArrayList,泛型List都能够存储一组对象,但是在开发中根本不知道用哪个性能最高,下面我们慢慢分析分析. 一.数组Array 数组是一个存储相同类型元素的固定大小的顺序集合.数组是用来存储数据的集合,通常认为数组是一个同一类型变量的集合. Array 类是 C# 中所有数组的基类,它是在 System 命名空间中定义. 数组在内存中是连续存储的,所以它的索引速度非常快,而且赋值与修改元素也非常简单. Array数组具体用法: using System; names

C# LINQ to XML应用介绍_C#教程

W3C制定了XML DOM标准,.Net为了支持W3C的标准,从1.1版本开始就引入了XmlDocument类.我在前一篇博客中,介绍了如何使用XmlDocument类来对XML文档进行操作.后来 .Net又引入了LINQ,于是LINQ to XML也就应运而生,所以在.Net中,不仅可以用W3C XML DOM标准,还可以使用LINQ to XML来操作XML文档.下面就来简单介绍一下如何使用LINQ to XML. (一) 加载 加载XML比较常用的有三种方法: 复制代码 代码如下: pub

C#读写操作app.config中的数据应用介绍_C#教程

读语句: 复制代码 代码如下: String str = ConfigurationManager.AppSettings["DemoKey"]; 写语句: 复制代码 代码如下: Configuration cfa = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); 2 cfa.AppSettings.Settings["DemoKey"].Value = "D

c#结构和类的相关介绍_C#教程

我们不关心对象内部是怎么实现的,我们关心的是他提供给我什么接口,有什么操作.从技术上来说,结构属于值类型,而类属于引用类型.结构不能指定继承基类类型,类可以.不过结构和类都能实现接口. 一.应用场合结构的应用场合: 一.自定义数据类型,数据成员是公开的,提供工具函数. 二.抽象的数据类型,数据成员是密封的,提供相关的数据操作函数. 总之,都是围绕数据作文章. 类的应用场合: 一.提供一组类,形成一个有机整体,形成一个系统,类数据成员是密封的,只提供相互通信的函数接口. 类主要通过不同的类组成一个