前言
我們都知道泛型在C#的重要性,泛型是OOP語言中三大特徵的多型的最重要的體現,幾乎泛型撐起了整個.NET框架,在講泛型之前,我們可以丟擲一個問題,我們現在需要一個可擴容的陣列類,且滿足所有型別,不管是值型別還是引用型別,那麼在沒有用泛型方法實現,如何實現?
一.泛型之前的故事
我們肯定會想到用object
來作為型別引數,因為在C#中,所有型別都是基於Object
型別的。因此Object是所有型別的最基類,那麼我們的可擴容陣列類如下:
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;
}
}
然後我們來驗證下:
var 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}");
輸出:
ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
貌似輸出結果是正確的,能夠動態進行擴容,同樣的支援值型別Struct
的int32
和引用型別的字串,但是其實這裡會發現一些問題,那就是
- 引用型別
string
進行了型別轉換的驗證 - 值型別
int32
進行了裝箱和拆箱操作,同時進行型別轉換型別的檢驗 - 發生的這一切都是在執行時的,假如型別轉換錯誤,得在執行時才能報錯
大致執行模型如下:
引用型別:
值型別:
那麼有沒有一種方法能夠避免上面遇到的三種問題呢?在借鑑了cpp的模板和java的泛型經驗,在C#2.0的時候推出了更適合.NET體系下的泛型
二.用泛型實現
public 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;
}
}
}
那麼測試程式碼則改寫為如下:
var 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}");
輸出:
ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
我們通過擷取部分ArrayExpandable<T>
的IL檢視其本質是個啥:
//宣告類
.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: nop
IL_0001: ldarg.0
IL_0002: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size
IL_0007: ldarg.0
IL_0008: ldfld !0[] class MetaTest.ArrayExpandable`1<!T>::_items
IL_000d: ldlen
IL_000e: conv.i4
IL_000f: ceq
IL_0011: stloc.0
IL_0012: ldloc.0
IL_0013: brfalse.s IL_0024
IL_0015: ldarg.0
IL_0016: ldarg.0
IL_0017: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size
IL_001c: ldc.i4.1
IL_001d: add
IL_001e: call instance void class MetaTest.ArrayExpandable`1<!T>::EnsuresCapacity(int32)
IL_0023: nop
IL_0024: ldarg.0
IL_0025: ldfld !0[] class MetaTest.ArrayExpandable`1<!T>::_items
IL_002a: ldarg.0
IL_002b: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size
IL_0030: ldarg.1
IL_0031: stelem !T
IL_0036: ldarg.0
IL_0037: ldarg.0
IL_0038: ldfld int32 class MetaTest.ArrayExpandable`1<!T>::_size
IL_003d: ldc.i4.1
IL_003e: add
IL_003f: stfld int32 class MetaTest.ArrayExpandable`1<!T>::_size
IL_0044: ret
} // end of method ArrayExpandable`1::Add
原來定義的時候就是用了個T
作為佔位符,起一個模板的作用,我們對其例項化型別引數的時候,補足那個佔位符,我們可以在編譯期就知道了其型別,且不用在執行時進行型別檢測,而我們也可以對比ArrayExpandable
和ArrayExpandable<T>
在型別為值型別中的IL,檢視是否進行拆箱和裝箱操作,以下為IL擷取部分:
ArrayExpandable:
IL_0084: newobj instance void GenericSample.ArrayExpandable::.ctor()
IL_0089: stloc.2
IL_008a: ldc.i4.0
IL_008b: stloc.s V_6
IL_008d: br.s IL_00bc
IL_008f: nop
IL_0090: ldloc.2
IL_0091: ldloc.s V_6
IL_0093: box [System.Runtime]System.Int32 //box為裝箱操作
IL_0098: callvirt instance void GenericSample.ArrayExpandable::Add(object)
IL_009d: nop
IL_009e: ldloc.2
IL_009f: ldloc.s V_6
IL_00a1: callvirt instance object GenericSample.ArrayExpandable::get_Item(int32)
IL_00a6: unbox.any [System.Runtime]System.Int32 //unbox為拆箱操作
ArrayExpandable
IL_007f: newobj instance void class GenericSample.ArrayExpandable`1<int32>::.ctor()
IL_0084: stloc.2
IL_0085: ldc.i4.0
IL_0086: stloc.s V_6
IL_0088: br.s IL_00ad
IL_008a: nop
IL_008b: ldloc.2
IL_008c: ldloc.s V_6
IL_008e: callvirt instance void class GenericSample.ArrayExpandable`1<int32>::Add(!0)
IL_0093: nop
IL_0094: ldloc.2
IL_0095: ldloc.s V_6
IL_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 來測試其效能對比,程式碼如下:
[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基金會牛逼
三.泛型的多型性
多型的宣告
類、結構、介面、方法、和委託可以宣告一個或者多個型別引數,我們直接看程式碼:
interface 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());
}
}
控制檯測試程式碼:
static 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}");
}
輸出如下:
建構函式: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
我們通過例子可以看到的是:
- 類(結構也可以),介面,委託,方法都可以宣告一個或多個型別引數,體現了宣告的多型性
- 類的函式成員:屬性,欄位,索引,構造器,運算子只能引入類宣告的型別引數,不能夠宣告,唯有方法這一函式成員具備宣告和引用型別引數兩種功能,由於具備宣告功能,因此可以宣告和委託一樣的型別引數並且引用它,這也體現了方法的多型性
多型的繼承
父類和實現類或介面的介面都可以是例項化型別,直接看程式碼:
interface 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
出來- 上述都體現出繼承的多型性
多型的遞迴
我們定義如下一個類和一個方法,且不會報錯:
class D<T> { }
class C<T> : D<C<C<T>>>
{
void Foo()
{
var foo = new C<C<T>>();
Console.WriteLine(foo.ToString());
}
}
因為T
能在例項化的時候確定其型別,因此也支援這種迴圈套用自己的類和方法的定義
四.泛型的約束
where的約束
我們先上程式碼:
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類:
class 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#中,只有泛型介面和泛型委託可以支援協變和逆變
協變
我們先看下程式碼:
class 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,那段程式碼就會編譯正常,但是會引出另外一段編譯報錯:
interface 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
也是這麼定義的:
public interface IEnumerable<out T> : IEnumerable
{
new IEnumerator<T> GetEnumerator();
}
逆變
我們將上面程式碼改下:
class 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,發現出現另外一處編譯失敗
interface 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
就是個逆變的例子:
public delegate void Action<in T>(T obj);
五.泛型的反射
我們先來看看以下程式碼:
static 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方法:
void 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: nop
IL_0001: newobj instance void class MetaTest.ArrayExpandable`1/*02000003*/<int32>/*1B000001*/::.ctor() /* 0A00000C */
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldc.i4.1
IL_0009: callvirt instance void class MetaTest.ArrayExpandable`1/*02000003*/<int32>/*1B000001*/::Add(!0) /* 0A00000D */
IL_000e: nop
IL_000f: newobj instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::.ctor() /* 0A00000E */
IL_0014: stloc.1
IL_0015: ldloc.1
IL_0016: ldstr "ryzen" /* 70000001 */
IL_001b: callvirt instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::Add(!0) /* 0A00000F */
IL_0020: nop
IL_0021: newobj instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::.ctor() /* 0A00000E */
IL_0026: stloc.2
IL_0027: ldloc.1
IL_0028: ldstr "ryzen" /* 70000001 */
IL_002d: callvirt instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::Add(!0) /* 0A00000F */
IL_0032: nop
IL_0033: ret
} // end of method Program::Main
開啟後設資料表將上面所涉及到的後設資料定義表和型別規格表列出:
metainfo:
-----------定義部分
TypeDef #2 (02000003)
-------------------------------------------------------
TypDefName: MetaTest.ArrayExpandable`1 (02000003)
Flags : [Public] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100001)
Extends : 0100000C [TypeRef] System.Object
1 Generic Parameters
(0) GenericParamToken : (2a000001) Name : T flags: 00000000 Owner: 02000003
Method #8 (0600000a)
-------------------------------------------------------
MethodName: Add (0600000A)
Flags : [Public] [HideBySig] [ReuseSlot] (00000086)
RVA : 0x000021f4
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
1 Arguments
Argument #1: Var!0
1 Parameters
(1) ParamToken : (08000007) Name : value flags: [none] (00000000)
------型別規格部分
TypeSpec #1 (1b000001)
-------------------------------------------------------
TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< I4> //14代表int32
MemberRef #1 (0a00000c)
-------------------------------------------------------
Member: (0a00000c) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
MemberRef #2 (0a00000d)
-------------------------------------------------------
Member: (0a00000d) Add:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
1 Arguments
Argument #1: Var!0
TypeSpec #2 (1b000002)
-------------------------------------------------------
TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< String>
MemberRef #1 (0a00000e)
-------------------------------------------------------
Member: (0a00000e) .ctor:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.
MemberRef #2 (0a00000f)
-------------------------------------------------------
Member: (0a00000f) Add:
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
1 Arguments
Argument #1: Var!0
這時候我們就可以看出,後設資料為泛型類ArrayExpandable<T>
定義一份定義表,生成兩份規格,也就是當你例項化型別引數為int
和string
的時候,分別生成了兩份規格程式碼,同時還發現以下的現象:
var lsInt = new ArrayExpandable<int>();//引用的是型別規格1b000001的成員0a00000c .ctor構造
lsInt.Add(1);//引用的是型別規格1b000001的成員0a00000d Add
var lsStr = new ArrayExpandable<string>();//引用的是型別規格1b000002的成員0a00000e .ctor構造
lsStr.Add("ryzen");//引用的是型別規格1b000002的成員0a00000f Add
var lsStr1 = new ArrayExpandable<string>();//和lsStr一樣
lsStr.Add("ryzen");//和lsStr一樣
非常妙的是,當你例項化兩個一樣的型別引數string
,是共享一份型別規格的,也就是同享一份原生程式碼,因此上面的程式碼線上程堆疊和託管堆的大致是這樣的:
由於泛型也有後設資料的存在,因此可以對其做反射:
Console.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}");
}
輸出:
-----------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(第二版)》