理解C#泛型运作原理
前言
?我們都知道泛型在C#的重要性,泛型是OOP語言中三大特征的多態的最重要的體現,幾乎泛型撐起了整個.NET框架,在講泛型之前,我們可以拋出一個問題,我們現在需要一個可擴容的數組類,且滿足所有類型,不管是值類型還是引用類型,那么在沒有用泛型方法實現,如何實現?
一.泛型之前的故事
?我們肯定會想到用object來作為類型參數,因為在C#中,所有類型都是基于Object類型的。因此Object是所有類型的最基類,那么我們的可擴容數組類如下:
Copy public class ArrayExpandable{private object?[] _items = null;private int _defaultCapacity = 4;private int _size;public object? this[int index]{get{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));return _items[index];}set{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));_items[index] = value;}}public int Capacity{get => _items.Length;set{if (value < _size){throw new ArgumentOutOfRangeException(nameof(value));}if (value != _items.Length){if (value > 0){object[] newItems = new object[value];if (_size > 0){Array.Copy(_items, newItems, _size);}_items = newItems;}else{_items = new object[_defaultCapacity];}}}}public int Count => _size;public ArrayExpandable(){_items = new object?[0];}public ArrayExpandable(int capacity){_items = new object?[capacity];}public void Add(object? value){//數組元素為0或者數組元素容量滿if (_size == _items.Length) EnsuresCapacity(_size + 1);_items[_size] = value;_size++;}private void EnsuresCapacity(int size){if (_items.Length < size){int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;if (newCapacity < size) newCapacity = size;Capacity = newCapacity;}}然后我們來驗證下:
Copyvar arrayStr = new ArrayExpandable(); var strs = new string[] { "ryzen", "reed", "wymen" }; for (int i = 0; i < strs.Length; i++) {arrayStr.Add(strs[i]);string value = (string)arrayStr[i];//改為int value = (int)arrayStr[i] 運行時報錯Console.WriteLine(value); } Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");var array = new ArrayExpandable(); for (int i = 0; i < 5; i++) {array.Add(i);int value = (int)array[i];Console.WriteLine(value); } Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");輸出:
Copyryzen reed wymen gavin Now arrayStr Capacity:4 Now array Capacity:8?貌似輸出結果是正確的,能夠動態進行擴容,同樣的支持值類型Struct的int32和引用類型的字符串,但是其實這里會發現一些問題,那就是
引用類型string進行了類型轉換的驗證
值類型int32進行了裝箱和拆箱操作,同時進行類型轉換類型的檢驗
發生的這一切都是在運行時的,假如類型轉換錯誤,得在運行時才能報錯
大致執行模型如下:
引用類型:
值類型:
?那么有沒有一種方法能夠避免上面遇到的三種問題呢?在借鑒了cpp的模板和java的泛型經驗,在C#2.0的時候推出了更適合.NET體系下的泛型
二.用泛型實現
Copypublic class ArrayExpandable<T> {private T[] _items;private int _defaultCapacity = 4;private int _size;public T this[int index]{get{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));return _items[index];}set{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));_items[index] = value;}}public int Capacity{get => _items.Length;set{if (value < _size){throw new ArgumentOutOfRangeException(nameof(value));}if (value != _items.Length){if (value > 0){T[] newItems = new T[value];if (_size > 0){Array.Copy(_items, newItems, _size);}_items = newItems;}else{_items = new T[_defaultCapacity];}}}}public int Count => _size;public ArrayExpandable(){_items = new T[0];}public ArrayExpandable(int capacity){_items = new T[capacity];}public void Add(T value){//數組元素為0或者數組元素容量滿if (_size == _items.Length) EnsuresCapacity(_size + 1);_items[_size] = value;_size++;}private void EnsuresCapacity(int size){if (_items.Length < size){int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;if (newCapacity < size) newCapacity = size;Capacity = newCapacity;}}}那么測試代碼則改寫為如下:
Copyvar arrayStr = new ArrayExpandable<string>(); var strs = new string[] { "ryzen", "reed", "wymen", "gavin" }; for (int i = 0; i < strs.Length; i++) {arrayStr.Add(strs[i]);string value = arrayStr[i];//改為int value = arrayStr[i] 編譯報錯Console.WriteLine(value); } Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");var array = new ArrayExpandable<int>(); for (int i = 0; i < 5; i++) {array.Add(i);int value = array[i];Console.WriteLine(value); } Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");輸出:
Copyryzen reed wymen gavin Now arrayStr Capacity:4 0 1 2 3 4 Now array Capacity:8我們通過截取部分ArrayExpandable<T>的IL查看其本質是個啥:
Copy//聲明類 .class public auto ansi beforefieldinit MetaTest.ArrayExpandable`1<T>extends [System.Runtime]System.Object {.custom instance void [System.Runtime]System.Reflection.DefaultMemberAttribute::.ctor(string) = ( 01 00 04 49 74 65 6D 00 00 ) } //Add方法 .method public hidebysig instance void Add(!T 'value') cil managed {// 代碼大小 69 (0x45).maxstack 3.locals init (bool V_0)IL_0000: nopIL_0001: ldarg.0IL_0002: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_0007: ldarg.0IL_0008: ldfld !0[] class MetaTest.ArrayExpandable`1<!T>::_itemsIL_000d: ldlenIL_000e: conv.i4IL_000f: ceqIL_0011: stloc.0IL_0012: ldloc.0IL_0013: brfalse.s IL_0024IL_0015: ldarg.0IL_0016: ldarg.0IL_0017: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_001c: ldc.i4.1IL_001d: addIL_001e: call instance void class MetaTest.ArrayExpandable`1<!T>::EnsuresCapacity(int32)IL_0023: nopIL_0024: ldarg.0IL_0025: ldfld !0[] class MetaTest.ArrayExpandable`1<!T>::_itemsIL_002a: ldarg.0IL_002b: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_0030: ldarg.1IL_0031: stelem !TIL_0036: ldarg.0IL_0037: ldarg.0IL_0038: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_003d: ldc.i4.1IL_003e: addIL_003f: stfld int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_0044: ret } // end of method ArrayExpandable`1::Add?原來定義的時候就是用了個T作為占位符,起一個模板的作用,我們對其實例化類型參數的時候,補足那個占位符,我們可以在編譯期就知道了其類型,且不用在運行時進行類型檢測,而我們也可以對比ArrayExpandable和ArrayExpandable<T>在類型為值類型中的IL,查看是否進行拆箱和裝箱操作,以下為IL截取部分:
ArrayExpandable:
Copy IL_0084: newobj instance void GenericSample.ArrayExpandable::.ctor()IL_0089: stloc.2IL_008a: ldc.i4.0IL_008b: stloc.s V_6IL_008d: br.s IL_00bcIL_008f: nopIL_0090: ldloc.2IL_0091: ldloc.s V_6IL_0093: box [System.Runtime]System.Int32 //box為裝箱操作IL_0098: callvirt instance void GenericSample.ArrayExpandable::Add(object)IL_009d: nopIL_009e: ldloc.2IL_009f: ldloc.s V_6IL_00a1: callvirt instance object GenericSample.ArrayExpandable::get_Item(int32)IL_00a6: unbox.any [System.Runtime]System.Int32 //unbox為拆箱操作ArrayExpandable:
Copy IL_007f: newobj instance void class GenericSample.ArrayExpandable`1<int32>::.ctor()IL_0084: stloc.2IL_0085: ldc.i4.0IL_0086: stloc.s V_6IL_0088: br.s IL_00adIL_008a: nopIL_008b: ldloc.2IL_008c: ldloc.s V_6IL_008e: callvirt instance void class GenericSample.ArrayExpandable`1<int32>::Add(!0)IL_0093: nopIL_0094: ldloc.2IL_0095: ldloc.s V_6IL_0097: callvirt instance !0 class GenericSample.ArrayExpandable`1<int32>::get_Item(int32)?我們從IL也能看的出來,ArrayExpandable<T>的T作為一個類型參數,在編譯后在IL已經確定了其類型,因此當然也就不存在裝拆箱的情況,在編譯期的時候IDE能夠檢測類型,因此也就不用在運行時進行類型檢測,但并不代表不能通過運行時檢測類型(可通過is和as),還能通過反射體現出泛型的靈活性,后面會講到
?其實有了解ArrayList和List的朋友就知道,ArrayExpandable和ArrayExpandable<T>其實現大致就是和它們一樣,只是簡化了很多的版本,我們這里可以通過?BenchmarkDotNet?來測試其性能對比,代碼如下:
Copy [SimpleJob(RuntimeMoniker.NetCoreApp31,baseline:true)][SimpleJob(RuntimeMoniker.NetCoreApp50)][MemoryDiagnoser]public class TestClass{[Benchmark]public void EnumAE_ValueType(){ArrayExpandable array = new ArrayExpandable();for (int i = 0; i < 10000; i++){array.Add(i);//裝箱int value = (int)array[i];//拆箱}array = null;//確保進行垃圾回收}[Benchmark]public void EnumAE_RefType(){ArrayExpandable array = new ArrayExpandable();for (int i = 0; i < 10000; i++){array.Add("r");string value = (string)array[i];}array = null;//確保進行垃圾回收}[Benchmark]public void EnumAE_Gen_ValueType(){ArrayExpandable<int> array = new ArrayExpandable<int>();for (int i = 0; i < 10000; i++){array.Add(i);int value = array[i];}array = null;//確保進行垃圾回收;}[Benchmark]public void EnumAE_Gen_RefType(){ArrayExpandable<string> array = new ArrayExpandable<string>();for (int i = 0; i < 10000; i++){array.Add("r");string value = array[i];}array = null;//確保進行垃圾回收;}[Benchmark]public void EnumList_ValueType(){List<int> array = new List<int>();for (int i = 0; i < 10000; i++){array.Add(i);int value = array[i];}array = null;//確保進行垃圾回收;}[Benchmark]public void EnumList_RefType(){List<string> array = new List<string>();for (int i = 0; i < 10000; i++){array.Add("r");string value = array[i];}array = null;//確保進行垃圾回收;}[Benchmark(Baseline =true)]public void EnumAraayList_valueType(){ArrayList array = new ArrayList();for (int i = 0; i < 10000; i++){array.Add(i);int value = (int)array[i];}array = null;//確保進行垃圾回收;}[Benchmark]public void EnumAraayList_RefType(){ArrayList array = new ArrayList();for (int i = 0; i < 10000; i++){array.Add("r");string value = (string)array[i];}array = null;//確保進行垃圾回收;}}?我還加入了.NETCore3.1和.NET5的對比,且以.NETCore3.1的EnumAraayList_valueType方法為基準,性能測試結果如下:
用更直觀的柱形圖來呈現:
?我們能看到在這里List的性能在引用類型和值類型中都是所以當中是最好的,不管是執行時間、GC次數,分配的內存空間大小,都是最優的,同時.NET5在幾乎所有的方法中性能都是優于.NETCore3.1,這里還提一句,我實現的ArrayExpandable和ArrayExpandable<T>性能都差于ArrayList和List,我還沒實現IList和各種方法,只能說句dotnet基金會牛逼
三.泛型的多態性
多態的聲明
類、結構、接口、方法、和委托可以聲明一個或者多個類型參數,我們直接看代碼:
Copyinterface IFoo<InterfaceT> {void InterfaceMenthod(InterfaceT interfaceT); }class Foo<ClassT, ClassT1>: IFoo<StringBuilder> {public ClassT1 Field;public delegate void MyDelegate<DelegateT>(DelegateT delegateT);public void DelegateMenthod<DelegateT>(DelegateT delegateT, MyDelegate<DelegateT> myDelegate){myDelegate(delegateT);}public static string operator +(Foo<ClassT, ClassT1> foo,string s){return $"{s}:{foo.GetType().Name}";}public List<ClassT> Property{ get; set; }public ClassT1 Property1 { get; set; }public ClassT this[int index] => Property[index];//沒判斷越界public Foo(List<ClassT> classT, ClassT1 classT1){Property = classT;Property1 = classT1;Field = classT1;Console.WriteLine($"構造函數:parameter1 type:{Property.GetType().Name},parameter2 type:{Property1.GetType().Name}");}//方法聲明了多個新的類型參數public void Method<MenthodT, MenthodT1>(MenthodT menthodT, MenthodT1 menthodT1){Console.WriteLine($"Method<MenthodT, MenthodT1>:{(menthodT.GetType().Name)}:{menthodT.ToString()}," +$"{menthodT1.GetType().Name}:{menthodT1.ToString()}");}public void Method(ClassT classT){Console.WriteLine($"{nameof(Method)}:{classT.GetType().Name}:classT?.ToString()");}public void InterfaceMenthod(StringBuilder interfaceT){Console.WriteLine(interfaceT.ToString());} }控制臺測試代碼:
Copystatic void Main(string[] args) {Test();Console.ReadLine(); }static void Test() {var list = new List<int>() { 1, 2, 3, 4 };var foo = new Foo<int, string>(list, "ryzen");var index = 0;Console.WriteLine($"索引:索引{index}的值:{foo[index]}");Console.WriteLine($"Filed:{foo.Field}");foo.Method(2333);foo.Method<DateTime, long>(DateTime.Now, 2021);foo.DelegateMenthod<string>("this is a delegate", DelegateMenthod);foo.InterfaceMenthod(new StringBuilder().Append("InterfaceMenthod:this is a interfaceMthod"));Console.WriteLine(foo+"重載+運算符"); }static void DelegateMenthod(string str) {Console.WriteLine($"{nameof(DelegateMenthod)}:{str}"); }輸出如下:
Copy構造函數:parameter1 type:List`1,parameter2 type:String 索引:索引0的值:1 Filed:ryzen Method:Int32:classT?.ToString() Method<MenthodT, MenthodT1>:DateTime:2021/03/02 11:45:40,Int64:2021 DelegateMenthod:this is a delegate InterfaceMenthod:this is a interfaceMthod 重載+運算符:Foo`2我們通過例子可以看到的是:
類(結構也可以),接口,委托,方法都可以聲明一個或多個類型參數,體現了聲明的多態性
類的函數成員:屬性,字段,索引,構造器,運算符只能引入類聲明的類型參數,不能夠聲明,唯有方法這一函數成員具備聲明和引用類型參數兩種功能,由于具備聲明功能,因此可以聲明和委托一樣的類型參數并且引用它,這也體現了方法的多態性
多態的繼承
父類和實現類或接口的接口都可以是實例化類型,直接看代碼:
Copyinterface IFooBase<IBaseT>{}interface IFoo<InterfaceT>: IFooBase<string> {void InterfaceMenthod(InterfaceT interfaceT); }class FooBase<ClassT> {}class Foo<ClassT, ClassT1>: FooBase<ClassT>,IFoo<StringBuilder>{}我們可以通過例子看出:
由于Foo的基類FooBase定義的和Foo有著共享的類型參數ClassT,因此可以在繼承的時候不實例化類型
而Foo和IFoo接口沒定義相同的類型參數,因此可以在繼承的時候實例化出接口的類型參數StringBuild出來
IFoo和IFooBase沒定義相同的類型參數,因此可以在繼承的時候實例化出接口的類型參數string出來
上述都體現出繼承的多態性
多態的遞歸
我們定義如下一個類和一個方法,且不會報錯:
Copy class D<T> { }class C<T> : D<C<C<T>>> { void Foo(){var foo = new C<C<T>>();Console.WriteLine(foo.ToString());}}因為T能在實例化的時候確定其類型,因此也支持這種循環套用自己的類和方法的定義
四.泛型的約束
where的約束
我們先上代碼:
Copy class FooBase{ }class Foo : FooBase {}class someClass<T,K> where T:struct where K :FooBase,new(){}static void TestConstraint(){var someClass = new someClass<int, Foo>();//通過編譯//var someClass = new someClass<string, Foo>();//編譯失敗,string不是struct類型//var someClass = new someClass<string, long>();//編譯失敗,long不是FooBase類型}再改動下Foo類:
Copyclass Foo : FooBase {public Foo(string str){} }static void TestConstraint() {var someClass = new someClass<int, Foo>();//編譯失敗,因為new()約束必須類含有一個無參構造器,可以再給Foo類加上個無參構造器就能編譯通過 }?我們可以看到,通過where語句,可以對類型參數進行約束,而且一個類型參數支持多個約束條件(例如K),使其在實例化類型參數的時候,必須按照約束的條件對應實例符合條件的類型,而where條件約束的作用就是起在編譯期約束類型參數的作用
out和in的約束
?說到out和in之前,我們可以說下協變和逆變,在C#中,只有泛型接口和泛型委托可以支持協變和逆變
協變
我們先看下代碼:
Copyclass FooBase{ }class Foo : FooBase {}interface IBar<T> {T GetValue(T t); }class Bar<T> : IBar<T> {public T GetValue(T t){return t;} }static void Test() {var foo = new Foo();FooBase fooBase = foo;//編譯成功IBar<Foo> bar = new Bar<Foo>();IBar<FooBase> bar1 = bar;//編譯失敗}?這時候你可能會有點奇怪,為啥那段代碼會編譯失敗,明明Foo類可以隱式轉為FooBase,但作為泛型接口類型參數實例化卻并不能呢?使用out約束泛型接口IBar的T,那段代碼就會編譯正常,但是會引出另外一段編譯報錯:
Copyinterface IBar<out T> {T GetValue(string str);//編譯成功//T GetValue(T t);//編譯失敗 T不能作為形參輸入,用out約束T支持協變,T可以作為返回值輸出}IBar<Foo> bar = new Bar<Foo>(); IBar<FooBase> bar1 = bar;//編譯正常因此我們可以得出以下結論:
由于Foo繼承FooBase,本身子類Foo包含著父類允許訪問的成員,因此能隱式轉換父類,這是類型安全的轉換,因此叫協變
在為泛型接口用out標識其類型參數支持協變后,約束其方法的返回值和屬性的Get(本質也是個返回值的方法)才能引用所聲明的類型參數,也就是作為輸出值,用out很明顯的突出了這一意思
而支持迭代的泛型接口IEnumerable也是這么定義的:
Copy public interface IEnumerable<out T> : IEnumerable{new IEnumerator<T> GetEnumerator();}逆變
我們將上面代碼改下:
Copyclass FooBase{ }class Foo : FooBase {}interface IBar<T> {T GetValue(T t); }class Bar<T> : IBar<T> {public T GetValue(T t){return t;} }static void Test1() {var fooBase = new FooBase();Foo foo = (Foo)fooBase;//編譯通過,運行時報錯IBar<FooBase> bar = new Bar<FooBase>();IBar<Foo> bar1 = (IBar<Foo>)bar;//編譯通過,運行時報錯 }我們再改動下IBar,發現出現另外一處編譯失敗
Copyinterface IBar<in T> {void GetValue(T t);//編譯成功//T GetValue(T t);//編譯失敗 T不能作為返回值輸出,用in約束T支持逆變,T可以作為返回值輸出 }IBar<FooBase> bar = new Bar<FooBase>();IBar<Foo> bar1 = (IBar<Foo>)bar;//編譯通過,運行時不報錯IBar<Foo> bar1 = bar;//編譯通過,運行時不報錯因此我們可以得出以下結論:
由于FooBase是Foo的父類,并不包含子類的自由的成員,轉為為子類Foo是類型不安全的,因此在運行時強式轉換的報錯了,但編譯期是不能夠確認的
在為泛型接口用in標識其類型參數支持逆變后,in約束其接口成員不能將其作為返回值(輸出值),我們會發現協變和逆變正是一對反義詞
這里提一句,值類型是不支持協變和逆變的
同樣的泛型委托Action就是個逆變的例子:
Copypublic delegate void Action<in T>(T obj);五.泛型的反射
我們先來看看以下代碼:
Copystatic void Main(string[] args) {var lsInt = new ArrayExpandable<int>();lsInt.Add(1);var lsStr = new ArrayExpandable<string>();lsStr.Add("ryzen");var lsStr1 = new ArrayExpandable<string>();lsStr.Add("ryzen"); }然后通過ildasm查看其IL,開啟視圖-》顯示標記值,查看Main方法:
Copyvoid Main(string[] args) cil managed {.entrypoint// 代碼大小 52 (0x34).maxstack 2.locals /*11000001*/ init (class MetaTest.ArrayExpandable`1/*02000003*/<int32> V_0,class MetaTest.ArrayExpandable`1/*02000003*/<string> V_1,class MetaTest.ArrayExpandable`1/*02000003*/<string> V_2)IL_0000: nopIL_0001: newobj instance void class MetaTest.ArrayExpandable`1/*02000003*/<int32>/*1B000001*/::.ctor() /* 0A00000C */IL_0006: stloc.0IL_0007: ldloc.0IL_0008: ldc.i4.1IL_0009: callvirt instance void class MetaTest.ArrayExpandable`1/*02000003*/<int32>/*1B000001*/::Add(!0) /* 0A00000D */IL_000e: nopIL_000f: newobj instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::.ctor() /* 0A00000E */IL_0014: stloc.1IL_0015: ldloc.1IL_0016: ldstr "ryzen" /* 70000001 */IL_001b: callvirt instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::Add(!0) /* 0A00000F */IL_0020: nopIL_0021: newobj instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::.ctor() /* 0A00000E */IL_0026: stloc.2IL_0027: ldloc.1IL_0028: ldstr "ryzen" /* 70000001 */IL_002d: callvirt instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::Add(!0) /* 0A00000F */IL_0032: nopIL_0033: ret } // end of method Program::Main打開元數據表將上面所涉及到的元數據定義表和類型規格表列出:
metainfo:
Copy-----------定義部分 TypeDef #2 (02000003) -------------------------------------------------------TypDefName: MetaTest.ArrayExpandable`1 (02000003)Flags : [Public] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100001)Extends : 0100000C [TypeRef] System.Object1 Generic Parameters(0) GenericParamToken : (2a000001) Name : T flags: 00000000 Owner: 02000003Method #8 (0600000a) -------------------------------------------------------MethodName: Add (0600000A)Flags : [Public] [HideBySig] [ReuseSlot] (00000086)RVA : 0x000021f4ImplFlags : [IL] [Managed] (00000000)CallCnvntn: [DEFAULT]hasThis ReturnType: Void1 ArgumentsArgument #1: Var!01 Parameters(1) ParamToken : (08000007) Name : value flags: [none] (00000000)------類型規格部分 TypeSpec #1 (1b000001) -------------------------------------------------------TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< I4> //14代表int32MemberRef #1 (0a00000c)-------------------------------------------------------Member: (0a00000c) .ctor: CallCnvntn: [DEFAULT]hasThis ReturnType: VoidNo arguments.MemberRef #2 (0a00000d)-------------------------------------------------------Member: (0a00000d) Add: CallCnvntn: [DEFAULT]hasThis ReturnType: Void1 ArgumentsArgument #1: Var!0TypeSpec #2 (1b000002) -------------------------------------------------------TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< String>MemberRef #1 (0a00000e)-------------------------------------------------------Member: (0a00000e) .ctor: CallCnvntn: [DEFAULT]hasThis ReturnType: VoidNo arguments.MemberRef #2 (0a00000f)-------------------------------------------------------Member: (0a00000f) Add: CallCnvntn: [DEFAULT]hasThis ReturnType: Void1 ArgumentsArgument #1: Var!0?這時候我們就可以看出,元數據為泛型類ArrayExpandable<T>定義一份定義表,生成兩份規格,也就是當你實例化類型參數為int和string的時候,分別生成了兩份規格代碼,同時還發現以下的現象:
Copyvar lsInt = new ArrayExpandable<int>();//引用的是類型規格1b000001的成員0a00000c .ctor構造 lsInt.Add(1);//引用的是類型規格1b000001的成員0a00000d Addvar lsStr = new ArrayExpandable<string>();//引用的是類型規格1b000002的成員0a00000e .ctor構造 lsStr.Add("ryzen");//引用的是類型規格1b000002的成員0a00000f Add var lsStr1 = new ArrayExpandable<string>();//和lsStr一樣 lsStr.Add("ryzen");//和lsStr一樣?非常妙的是,當你實例化兩個一樣的類型參數string,是共享一份類型規格的,也就是同享一份本地代碼,因此上面的代碼在線程堆棧和托管堆的大致是這樣的:
由于泛型也有元數據的存在,因此可以對其做反射:
CopyConsole.WriteLine($"-----------{nameof(lsInt)}---------------"); Console.WriteLine($"{nameof(lsInt)} is generic?:{lsInt.GetType().IsGenericType}"); Console.WriteLine($"Generic type:{lsInt.GetType().GetGenericArguments()[0].Name}"); Console.WriteLine("---------Menthods:"); foreach (var method in lsInt.GetType().GetMethods()) {Console.WriteLine(method.Name); } Console.WriteLine("---------Properties:"); foreach (var property in lsInt.GetType().GetProperties()) {Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}"); }Console.WriteLine($"\n-----------{nameof(lsStr)}---------------"); Console.WriteLine($"{nameof(lsStr)} is generic?:{lsStr.GetType().IsGenericType}"); Console.WriteLine($"Generic type:{lsStr.GetType().GetGenericArguments()[0].Name}"); Console.WriteLine("---------Menthods:"); foreach (var method in lsStr.GetType().GetMethods()) {Console.WriteLine(method.Name); } Console.WriteLine("---------Properties:"); foreach (var property in lsStr.GetType().GetProperties()) {Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}"); }輸出:
Copy-----------lsInt--------------- lsInt is generic?:True Generic type:Int32 ---------Menthods: get_Item set_Item get_Capacity set_Capacity get_Count Add GetType ToString Equals GetHashCode ---------Properties: System.Int32:Item System.Int32:Capacity System.Int32:Count-----------lsStr--------------- lsStr is generic?:True Generic type:String ---------Menthods: get_Item set_Item get_Capacity set_Capacity get_Count Add GetType ToString Equals GetHashCode ---------Properties: System.String:Item System.Int32:Capacity System.Int32:Count六.總結
?泛型編程作為.NET體系中一個很重要的編程思想,主要有以下亮點:
編譯期確定類型,避免值類型的拆裝箱和不必要的運行時類型檢驗,同樣運行時也能通過is和as進行類型檢驗
通過約束進行對類型參數實例化的范圍
同時在IL層面,實例化相同類型參數的時候共享一份本地代碼
由于元數據的存在,也能在運行時進行反射,增強其靈活性
參考
Design and Implementation of Generics for the .NET Common Language Runtime
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/
《CLR Via C# 第四版》
《你必須知道的.NET(第二版)》
總結
以上是生活随笔為你收集整理的理解C#泛型运作原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET:使用 LinqSharp 简化
- 下一篇: c# char unsigned_dll