C# 泛型深入理解介紹
引言:
在上一個(gè)專題中介紹了C#2.0 中引入泛型的原因以及有了泛型后所帶來(lái)的好處,然而上一專題相當(dāng)于是介紹了泛型的一些基本知識(shí)的,對(duì)于泛型的性能為什么會(huì)比非泛型的性能高卻沒(méi)有給出理由,所以在這個(gè)專題就中將會(huì)介紹原因和一些關(guān)于泛型的其他知識(shí)。
一、泛型類型和類型參數(shù)
如果沒(méi)有為類型參數(shù)提供類型實(shí)參,此時(shí)我們就聲明了一個(gè)未綁定的泛型類型,如果指定了類型實(shí)參,此時(shí)的類型就叫做已構(gòu)造類型(這里同樣可以以書占位置去理解),然而已構(gòu)造類型又可以是開(kāi)放類型或封閉類型的,這里先給出這個(gè)兩個(gè)概念的定義的:開(kāi)放類型——具有類型參數(shù)的類型就是開(kāi)放類型(所有的未綁定的泛型類型都屬于開(kāi)放類型的),封閉類型——為每個(gè)類型參數(shù)都傳遞了實(shí)際的數(shù)據(jù)類型。對(duì)于開(kāi)放類型,我們創(chuàng)建開(kāi)放類型的實(shí)例。
注意:在C#代碼中,我們唯一可以看到未綁定泛型類型的地方(除了作為聲明之外)就是在typeof操作符里。
下面通過(guò)以下代碼來(lái)更好的說(shuō)明這點(diǎn):
using System;
using System.Collections.Generic;
namespace CloseTypeAndOpenType
{
// 聲明開(kāi)放泛型類型
public sealed class DictionaryStringKey<T> : Dictionary<string, T>
{
}
public class Program
{
static void Main(string[] args)
{
object o = null;
// Dictionary<,>是一個(gè)開(kāi)放類型,它有2個(gè)類型參數(shù)
Type t = typeof(Dictionary<,>);
// 創(chuàng)建開(kāi)放類型的實(shí)例(創(chuàng)建失敗,出現(xiàn)異常)
o = CreateInstance(t);
Console.WriteLine();
// DictionaryStringKey<>也是一個(gè)開(kāi)放類型,但它有1個(gè)類型參數(shù)
t = typeof(DictionaryStringKey<>);
// 創(chuàng)建該類型的實(shí)例(同樣會(huì)失敗,出現(xiàn)異常)
o = CreateInstance(t);
Console.WriteLine();
// DictionaryStringKey<int>是一個(gè)封閉類型
t = typeof(DictionaryStringKey<int>);
// 創(chuàng)建封閉類型的一個(gè)實(shí)例(成功)
o = CreateInstance(t);
Console.WriteLine("對(duì)象類型 = " + o.GetType());
Console.Read();
}
// 創(chuàng)建類型
private static object CreateInstance(Type t)
{
object o = null;
try
{
// 使用指定類型t的默認(rèn)構(gòu)造函數(shù)來(lái)創(chuàng)建該類型的實(shí)例
o = Activator.CreateInstance(t);
Console.WriteLine("已創(chuàng)建{0}的實(shí)例", t.ToString());
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
return o;
}
}
}
運(yùn)行結(jié)果為(從結(jié)果中也可以看出開(kāi)放類型不能創(chuàng)建該類型的一個(gè)實(shí)例,異常信息中指出類型中包含泛型參數(shù)):
二、泛型類型中的靜態(tài)字段和靜態(tài)構(gòu)造函數(shù)
首先實(shí)例字段是屬于一個(gè)實(shí)例的,靜態(tài)字段是從屬于它們聲明的類型,即如果在某個(gè)Myclass類中聲明了一個(gè)靜態(tài)字段field,則不管創(chuàng)建Myclass的多少個(gè)實(shí)例,也不管從Myclass中派生出多少個(gè)實(shí)例,都只有一個(gè)Myclass.x字段。然而每個(gè)封閉類型都有它自己的靜態(tài)字段(使用類型實(shí)參時(shí),實(shí)際上CLR會(huì)定義一個(gè)新的類型對(duì)象, 所以每個(gè)靜態(tài)字段都是不一樣對(duì)象里面的靜態(tài)字段,所以才會(huì)每個(gè)都有各自的值) 通過(guò)以下代碼來(lái)更好說(shuō)明下——每個(gè)封閉類型都有它自己的靜態(tài)字段:
View Code
namespace GenericStaticFieldAndStaticFunction
{
// 泛型類,具有一個(gè)類型參數(shù)
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)
{
// 使用類型實(shí)參時(shí),實(shí)際上CLR會(huì)定義一個(gè)新的類型對(duì)象
// 所以每個(gè)靜態(tài)字段都是不一樣對(duì)象里面的靜態(tài)字段,所以才會(huì)每個(gè)都有各自的值
// 對(duì)泛型類型類的靜態(tài)字段賦值
TypeWithStaticField<int>.field = "一";
TypeWithStaticField<string>.field = "二";
TypeWithStaticField<Guid>.field = "三";
// 此時(shí)filed 值只會(huì)有一個(gè)值,每個(gè)賦值都是改變了原來(lái)的值
NoGenericTypeWithStaticField.field = "非泛型類靜態(tài)字段一";
NoGenericTypeWithStaticField.field = "非泛型類靜態(tài)字段二";
NoGenericTypeWithStaticField.field = "非泛型類靜態(tài)字段三";
NoGenericTypeWithStaticField.OutField();
// 證明每個(gè)封閉類型都有一個(gè)靜態(tài)字段
TypeWithStaticField<int>.OutField();
TypeWithStaticField<string>.OutField();
TypeWithStaticField<Guid>.OutField();
Console.Read();
}
}
}
運(yùn)行結(jié)果:

同樣每個(gè)封閉類型都有一個(gè)靜態(tài)構(gòu)造函數(shù)的,通過(guò)下面的代碼可以讓大家更加明白這點(diǎn):
// 靜態(tài)構(gòu)造函數(shù)的例子
public static class Outer<Tx>
{
// 嵌套類
public class Inner<Ty>
{
// 靜態(tài)構(gòu)造函數(shù)
static Inner()
{
Console.WriteLine("Outer<{0}>.Inner<{1}>", typeof(Tx), typeof(Ty));
}
public static void Print()
{
}
}
}
class Program
{
static void Main(string[] args)
{
#region 靜態(tài)函數(shù)的演示
// 靜態(tài)構(gòu)造函數(shù)會(huì)運(yùn)行多次
// 因?yàn)槊總€(gè)封閉類型都有單獨(dú)的一個(gè)靜態(tài)構(gòu)造函數(shù)
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
}
}
運(yùn)行結(jié)果:

從上圖的運(yùn)行結(jié)果可能會(huì)發(fā)現(xiàn),我們代碼中7個(gè)需要輸出的,但是結(jié)果中只有6個(gè)結(jié)果輸出的,這是因?yàn)槿魏畏忾]類型的靜態(tài)構(gòu)造函數(shù)只執(zhí)行一次,最后一行的 Outer<string>.Inner<int>.Print();這行不會(huì)產(chǎn)生第7行輸出, 因?yàn)镺uter<string>.Inner<int>.Print();的靜態(tài)構(gòu)造函數(shù)在之前已經(jīng)執(zhí)行過(guò)的(第三行已經(jīng)執(zhí)行過(guò)了)。
三、編譯器如何解析泛型
在上一個(gè)專題中,我只是貼出了泛型與非泛型的比較結(jié)果來(lái)說(shuō)明泛型具有高性能的好處,卻沒(méi)有給出具體導(dǎo)致泛型比非泛型效率高的原因,所以在這個(gè)部分來(lái)剖析下泛型效率的具體原因。
這里先貼出上一個(gè)專題中說(shuō)明泛型高性能好處的代碼,然后再查看IL代碼來(lái)說(shuō)明泛型的高性能(針對(duì)泛型和非泛型,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();
// 非泛型數(shù)組
ArrayList arraylist = new ArrayList();
// 泛型數(shù)組
List<int> genericlist= new List<int>();
// 開(kāi)始計(jì)時(shí)
stopwatch.Start();
for (int i = 1; i < 10000000; i++)
{
//genericlist.Add(i);
arraylist.Add(i);
}
// 結(jié)束計(jì)時(shí)
stopwatch.Stop();
// 輸出所用的時(shí)間
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("運(yùn)行的時(shí)間: " + elapsedTime);
Console.Read();
}
}
}
當(dāng)使用非泛型的的ArrayList數(shù)組時(shí),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代碼中,我用紅色的標(biāo)記的代碼主要是在執(zhí)行裝箱操作(裝箱過(guò)程肯定是要消耗的事件的吧, 就像生活中寄包裹一樣,包裝起來(lái)肯定是要花費(fèi)一定的時(shí)間的, 裝箱操作同樣會(huì),然而對(duì)于泛型類型就可以避免裝箱操作,下面會(huì)貼出使用泛型類型的IL代碼的截圖)——這個(gè)操作也是影響非泛型的性能不如泛型類型的根本原因。然而為什么使用ArrayList類型在調(diào)用Add方法來(lái)向數(shù)組添加元素之前要裝箱的呢?原因其實(shí)主要出在Add方法上的, 大家可以用Reflector反射工具查看ArrayList的Add方法定義,下面是一張Add方法原型的截圖:

從上面截圖可以看出,Add(objec value)需要接收object類型的參數(shù),然而我們代碼中需要傳遞的是int實(shí)參,此時(shí)就需要會(huì)發(fā)生裝箱操作(值類型int轉(zhuǎn)化為object引用類型,這個(gè)過(guò)程就是裝箱操作),這樣也就解釋了為什么調(diào)用Add方法執(zhí)行裝箱操作的, 同時(shí)也就說(shuō)明泛型的高性能的好處。
下面是使用泛型List<T>的IL代碼截圖(從圖片中可以看出,使用泛型時(shí),沒(méi)有執(zhí)行裝箱的操作,這樣就少了裝箱的時(shí)間,這樣當(dāng)然就運(yùn)行的快了,性能就好了。):

四、小結(jié)
說(shuō)到這里本專題的內(nèi)容也就介紹結(jié)束了,本專題主要是進(jìn)一步介紹了泛型的其他內(nèi)容的,由于篇幅的關(guān)于我將泛型的其他內(nèi)容放在下一專題中,如果都在放在這個(gè)專題中內(nèi)容會(huì)顯得非常多,這樣也不利于大家的消化和大家的閱讀,所以我在下一個(gè)專題中繼續(xù)介紹泛型的其他的一些內(nèi)容。
下面先附上泛型專題中用到的所有Demo的源代碼:GeneralDemo_jb51.rar
相關(guān)文章
WPF實(shí)現(xiàn)類似ChatGPT逐字打印效果的示例代碼
前一段時(shí)間ChatGPT類的應(yīng)用十分火爆,這類應(yīng)用在回答用戶的問(wèn)題時(shí)逐字打印輸出,像極了真人打字回復(fù)消息,本文就來(lái)利用WPF模擬一下這種逐字打印的效果吧2023-08-08Unity3D開(kāi)發(fā)實(shí)戰(zhàn)之五子棋游戲
這篇文章主要為大家詳細(xì)介紹了Unity3D開(kāi)發(fā)實(shí)戰(zhàn)之五子棋游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09Unity UGUI實(shí)現(xiàn)簡(jiǎn)單拖拽圖片功能
這篇文章主要為大家詳細(xì)介紹了Unity UGUI實(shí)現(xiàn)簡(jiǎn)單拖拽圖片功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06