理解C#.NET泛型运作原理


理解C#.NET泛型运作原理

前言

 我们都知道泛型在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. 发生的这一切都是在运行时的,假如类型转换错误,得在运行时才能报错

大致执行模型如下:

引用类型:

理解C#泛型运作原理

值类型:

理解C#泛型运作原理

 那么有没有一种方法能够避免上面遇到的三种问题呢?在借鉴了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<T>:

 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方法为基准,性能测试结果如下:

理解C#泛型运作原理

用更直观的柱形图来呈现:

理解C#泛型运作原理

 我们能看到在这里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,是共享一份类型规格的,也就是同享一份本地代码,因此上面的代码在线程堆栈和托管堆的大致是这样的:

理解C#泛型运作原理

由于泛型也有元数据的存在,因此可以对其做反射:

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

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

《CLR Via C# 第四版》

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

版权声明:本文为开发框架文库发布内容,转载请附上原文出处连接
C/S框架网
上一篇:C#使用PInvoke.Kernel32加载非托管DLL嵌入资源
下一篇:C#.NET理解Task和async await原理
评论列表

发表评论

评论内容
昵称:
关联文章

理解C#.NET运作原理
C#.NET理解Task和async await原理
C# 日期(DateTime?)扩展方法 DateTimeExtension
C#判断对象类型为值类型、字符串、对象引用类型或
C# 动态对象转换为实体对象支持(DynamicToEntity)
C# 使用List<String>读取和保存文本文件
IDatabase.ExecuteReader返回对象列表Func函数的使用方法
.Net 下高性能分表分库组件 (类似ShardingSphere原理
【原创】C# 深度拷贝对象 使用.NET反射+递归原理实现深度克隆
C#/.NET 基础学习
Winform界面权限控制原理|C/S框架网
【原创】WebApi开发框架:Token生成、Token缓存原理、Token验证、令牌机制与原理
C/S框架后台数据更新模型之ORM自动生成SQL基本原理
SQL注入攻击的原理及其防范措施
学习C#.NET基础知识(学习重点请下载附件)
C#.NET 消息机制
C# Winform软件快速开发框架|软件开发平台 - 界面多语言实现原理
UDP打洞(UDP Hole Punching)原理
SQL Server连接池基本原理
C#.NET LINQ入门基础

热门标签
.NET5 .NET6 .NET7 APP Auth-软件授权注册系统 Axios B/S B/S开发框架 Bug Bug记录 C#加密解密 C#源码 C/S CHATGPT CMS系统 CodeGenerator CSFramework.DB CSFramework.EF CSFrameworkV1学习版 CSFrameworkV2标准版 CSFrameworkV3高级版 CSFrameworkV4企业版 CSFrameworkV5旗舰版 CSFrameworkV6.0 DAL数据访问层 Database datalock DbFramework Demo教学 Demo下载 DevExpress教程 DOM EF框架 Element-UI EntityFramework ERP ES6 Excel FastReport GIT HR IDatabase IIS JavaScript LINQ MES MiniFramework MIS NavBarControl Node.JS NPM OMS ORM PaaS POS Promise API Redis SAP SEO SQL SQLConnector TMS系统 Token令牌 VS2022 VSCode VUE WCF WebApi WebApi NETCore WebApi框架 WEB开发框架 Windows服务 Winform 开发框架 Winform 开发平台 WinFramework Workflow工作流 Workflow流程引擎 版本区别 报表 踩坑日记 操作手册 代码生成器 迭代开发记录 基础资料窗体 架构设计 角色权限 开发sce 开发技巧 开发教程 开发框架 开发平台 开发指南 客户案例 快速搭站系统 快速开发平台 秘钥 密钥 权限设计 软件报价 软件测试报告 软件简介 软件开发框架 软件开发平台 软件开发文档 软件体系架构 软件下载 软著证书 三层架构 设计模式 生成代码 实用小技巧 收钱音箱 数据锁 数据同步 微信小程序 未解决问题 文档下载 喜鹊ERP 喜鹊软件 系统对接 详细设计说明书 行政区域数据库 需求分析 疑难杂症 蝇量级框架 蝇量框架 用户管理 用户开发手册 用户控件 在线支付 纸箱ERP 智能语音收款机 自定义窗体 自定义组件 自动升级程序