理解C#泛型運作原理

前言

 我們都知道泛型在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

 貌似輸出結果是正確的,能夠動態進行擴容,同樣的支持值類型Structint32和引用類型的字符串,但是其實這裡會發現一些問題,那就是

  1. 引用類型string進行了類型轉換的驗證
  2. 值類型int32進行了裝箱和拆箱操作,同時進行類型轉換類型的檢驗
  3. 發生的這一切都是在運行時的,假如類型轉換錯誤,得在運行時才能報錯

大致執行模型如下:

引用類型:

值類型:

 那麼有沒有一種方法能夠避免上面遇到的三種問題呢?在借鑒了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作為佔位符,起一個模板的作用,我們對其實例化類型參數的時候,補足那個佔位符,我們可以在編譯期就知道了其類型,且不用在運行時進行類型檢測,而我們也可以對比ArrayExpandableArrayExpandable<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),還能通過反射體現出泛型的靈活性,後面會講到

 其實有了解ArrayListList的朋友就知道,ArrayExpandableArrayExpandable<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,這裡還提一句,我實現的ArrayExpandableArrayExpandable<T>性能都差於ArrayListList,我還沒實現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,因此可以在繼承的時候不實例化類型
  • FooIFoo接口沒定義相同的類型參數,因此可以在繼承的時候實例化出接口的類型參數StringBuild出來
  • IFooIFooBase沒定義相同的類型參數,因此可以在繼承的時候實例化出接口的類型參數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的約束

 說到outin之前,我們可以說下協變和逆變,在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;//編譯通過,運行時不報錯

因此我們可以得出以下結論:

  • 由於FooBaseFoo的父類,並不包含子類的自由的成員,轉為為子類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>定義一份定義表,生成兩份規格,也就是當你實例化類型參數為intstring的時候,分別生成了兩份規格代碼,同時還發現以下的現象:

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體系中一個很重要的編程思想,主要有以下亮點:

  • 編譯期確定類型,避免值類型的拆裝箱和不必要的運行時類型檢驗,同樣運行時也能通過isas進行類型檢驗
  • 通過約束進行對類型參數實例化的範圍
  • 同時在IL層面,實例化相同類型參數的時候共享一份本地代碼
  • 由於元數據的存在,也能在運行時進行反射,增強其靈活性

參考

Design and Implementation of Generics for the .NET Common Language Runtime

//docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/

《CLR Via C# 第四版》

《你必須知道的.NET(第二版)》

Tags: