站在浪尖看红尘 發表於 2021-3-5 09:05:00

理解C#泛型运作原理

<h2 id="前言">前言</h2>
<p> 我们都知道泛型在C#的重要性,泛型是OOP语言中三大特征的多态的最重要的体现,几乎泛型撑起了整个.NET框架,在讲泛型之前,我们可以抛出一个问题,我们现在需要一个可扩容的数组类,且满足所有类型,不管是值类型还是引用类型,那么在没有用泛型方法实现,如何实现?</p>
<h2 id="一泛型之前的故事">一.泛型之前的故事</h2>
<p> 我们肯定会想到用<code>object</code>来作为类型参数,因为在C#中,所有类型都是基于<code>Object</code>类型的。因此Object是所有类型的最基类,那么我们的可扩容数组类如下:</p>
<pre><code class="language-c#"> public class ArrayExpandable
{
   private object?[] _items = null;

   private int _defaultCapacity = 4;

   private int _size;

   public object? this
   {
         get
         {
                if (index &lt; 0 || index &gt;= _size)
                  throw new ArgumentOutOfRangeException(nameof(index));
                return _items;
         }
         set
         {
                if (index &lt; 0 || index &gt;= _size)
                  throw new ArgumentOutOfRangeException(nameof(index));
                _items = value;
         }
   }

   public int Capacity
   {
         get =&gt; _items.Length;
         set
         {
            if (value &lt; _size)
            {
                  throw new ArgumentOutOfRangeException(nameof(value));
            }
            if (value != _items.Length)
            {
                  if (value &gt; 0)
                  {
                      object[] newItems = new object;
                      if (_size &gt; 0)
                      {
                        Array.Copy(_items, newItems, _size);
                      }
                      _items = newItems;
                  }
                  else
                  {
                      _items = new object;
                  }
            }
         }
    }

    public int Count =&gt; _size;


    public ArrayExpandable()
    {
      _items = new object?;
    }

    public ArrayExpandable(int capacity)
    {
      _items = new object?;
    }

    public void Add(object? value)
    {
      //数组元素为0或者数组元素容量满
      if (_size == _items.Length) EnsuresCapacity(_size + 1);
      _items = value;
      _size++;
    }

    private void EnsuresCapacity(int size)
    {
      if (_items.Length &lt; size)
      {
            int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;
            if (newCapacity &lt; size) newCapacity = size;
            Capacity = newCapacity;
      }
   }
</code></pre>
<p>然后我们来验证下:</p>
<pre><code class="language-c#">var arrayStr = new ArrayExpandable();
var strs = new string[] { "ryzen", "reed", "wymen" };
for (int i = 0; i &lt; strs.Length; i++)
{
   arrayStr.Add(strs);
   string value = (string)arrayStr;//改为int value = (int)arrayStr 运行时报错
   Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");

var array = new ArrayExpandable();
for (int i = 0; i &lt; 5; i++)
{
   array.Add(i);
   int value = (int)array;
   Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");
</code></pre>
<p>输出:</p>
<pre><code>ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
</code></pre>
<p> 貌似输出结果是正确的,能够动态进行扩容,同样的支持值类型<code>Struct</code>的<code>int32</code>和引用类型的字符串,但是其实这里会发现一些问题,那就是</p>
<ol>
<li>引用类型<code>string</code>进行了类型转换的验证</li>
<li>值类型<code>int32</code>进行了装箱和拆箱操作,同时进行类型转换类型的检验</li>
<li>发生的这一切都是在运行时的,假如类型转换错误,得在运行时才能报错</li>
</ol>
<p>大致执行模型如下:</p>
<p>引用类型:</p>
<p><img src="https://img2020.cnblogs.com/blog/1294271/202103/1294271-20210304143715936-50764338.jpg" alt="" loading="lazy"></p>
<p>值类型:</p>
<p><img src="https://img2020.cnblogs.com/blog/1294271/202103/1294271-20210304143723711-458683671.jpg" alt="" loading="lazy"></p>
<p> 那么有没有一种方法能够避免上面遇到的三种问题呢?在借鉴了cpp的模板和java的泛型经验,在C#2.0的时候推出了更适合.NET体系下的泛型</p>
<h2 id="二用泛型实现">二.用泛型实现</h2>
<pre><code class="language-c#">public class ArrayExpandable&lt;T&gt;
{
   private T[] _items;

   private int _defaultCapacity = 4;

   private int _size;

   public T this
   {
         get
         {
             if (index &lt; 0 || index &gt;= _size)
               throw new ArgumentOutOfRangeException(nameof(index));
             return _items;
         }
         set
         {
             if (index &lt; 0 || index &gt;= _size)
               throw new ArgumentOutOfRangeException(nameof(index));
             _items = value;
          }
   }

   public int Capacity
   {
         get =&gt; _items.Length;
         set
         {
             if (value &lt; _size)
             {
               throw new ArgumentOutOfRangeException(nameof(value));
             }
             if (value != _items.Length)
             {
               if (value &gt; 0)
               {
                     T[] newItems = new T;
                     if (_size &gt; 0)
                     {
                         Array.Copy(_items, newItems, _size);
                     }
                     _items = newItems;
               }
               else
               {
                     _items = new T;
               }
             }
          }
   }

   public int Count =&gt; _size;


   public ArrayExpandable()
   {
         _items = new T;
   }

   public ArrayExpandable(int capacity)
   {
         _items = new T;
   }
   public void Add(T value)
   {
         //数组元素为0或者数组元素容量满
         if (_size == _items.Length) EnsuresCapacity(_size + 1);
         _items = value;
         _size++;
   }

   private void EnsuresCapacity(int size)
   {
         if (_items.Length &lt; size)
         {
             int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;
             if (newCapacity &lt; size) newCapacity = size;
             Capacity = newCapacity;
         }
   }
}
</code></pre>
<p>那么测试代码则改写为如下:</p>
<pre><code class="language-c#">var arrayStr = new ArrayExpandable&lt;string&gt;();
var strs = new string[] { "ryzen", "reed", "wymen", "gavin" };
for (int i = 0; i &lt; strs.Length; i++)
{
   arrayStr.Add(strs);
   string value = arrayStr;//改为int value = arrayStr 编译报错
   Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");

var array = new ArrayExpandable&lt;int&gt;();
for (int i = 0; i &lt; 5; i++)
{
   array.Add(i);
   int value = array;
   Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");
</code></pre>
<p>输出:</p>
<pre><code>ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
</code></pre>
<p>我们通过截取部分<code>ArrayExpandable&lt;T&gt;</code>的IL查看其本质是个啥:</p>
<pre><code>//声明类
.class public auto ansi beforefieldinit MetaTest.ArrayExpandable`1&lt;T&gt;
       extends System.Object
{
.custom instance void System.Reflection.DefaultMemberAttribute::.ctor(string) = ( 01 00 04 49 74 65 6D 00 00 )                     
}


//Add方法
.method public hidebysig instance voidAdd(!T 'value') cil managed
{
// 代码大小       69 (0x45)
.maxstack3
.locals init (bool V_0)
IL_0000:nop
IL_0001:ldarg.0
IL_0002:ldfld      int32 class MetaTest.ArrayExpandable`1&lt;!T&gt;::_size
IL_0007:ldarg.0
IL_0008:ldfld      !0[] class MetaTest.ArrayExpandable`1&lt;!T&gt;::_items
IL_000d:ldlen
IL_000e:conv.i4
IL_000f:ceq
IL_0011:stloc.0
IL_0012:ldloc.0
IL_0013:brfalse.sIL_0024
IL_0015:ldarg.0
IL_0016:ldarg.0
IL_0017:ldfld      int32 class MetaTest.ArrayExpandable`1&lt;!T&gt;::_size
IL_001c:ldc.i4.1
IL_001d:add
IL_001e:call       instance void class MetaTest.ArrayExpandable`1&lt;!T&gt;::EnsuresCapacity(int32)
IL_0023:nop
IL_0024:ldarg.0
IL_0025:ldfld      !0[] class MetaTest.ArrayExpandable`1&lt;!T&gt;::_items
IL_002a:ldarg.0
IL_002b:ldfld      int32 class MetaTest.ArrayExpandable`1&lt;!T&gt;::_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&lt;!T&gt;::_size
IL_003d:ldc.i4.1
IL_003e:add
IL_003f:stfld      int32 class MetaTest.ArrayExpandable`1&lt;!T&gt;::_size
IL_0044:ret
} // end of method ArrayExpandable`1::Add



</code></pre>
<p> 原来定义的时候就是用了个<code>T</code>作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符,我们可以在编译期就知道了其类型,且不用在运行时进行类型检测,而我们也可以对比<code>ArrayExpandable</code>和<code>ArrayExpandable&lt;T&gt;</code>在类型为值类型中的IL,查看是否进行拆箱和装箱操作,以下为IL截取部分:</p>
<p>ArrayExpandable:</p>
<pre><code class="language-c#">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.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.anySystem.Int32 //unbox为拆箱操作

</code></pre>
<p>ArrayExpandable&lt;T&gt;:</p>
<pre><code class="language-c#"> IL_007f:newobj   instance void class GenericSample.ArrayExpandable`1&lt;int32&gt;::.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:callvirtinstance void class GenericSample.ArrayExpandable`1&lt;int32&gt;::Add(!0)
IL_0093:nop
IL_0094:ldloc.2
IL_0095:ldloc.s    V_6
IL_0097:callvirt   instance !0 class GenericSample.ArrayExpandable`1&lt;int32&gt;::get_Item(int32)
</code></pre>
<p> 我们从IL也能看的出来,<code>ArrayExpandable&lt;T&gt;</code>的<code>T</code>作为一个类型参数,在编译后在IL已经确定了其类型,因此当然也就不存在装拆箱的情况,在编译期的时候IDE能够检测类型,因此也就不用在运行时进行类型检测,但并不代表不能通过运行时检测类型(可通过is和as),还能通过反射体现出泛型的灵活性,后面会讲到</p>
<p> 其实有了解<code>ArrayList</code>和<code>List</code>的朋友就知道,<code>ArrayExpandable</code>和<code>ArrayExpandable&lt;T&gt;</code>其实现大致就是和它们一样,只是简化了很多的版本,我们这里可以通过 BenchmarkDotNet 来测试其性能对比,代码如下:</p>
<pre><code class="language-c#">   
   
   
    public class TestClass
    {

      
      public void EnumAE_ValueType()
      {
            ArrayExpandable array = new ArrayExpandable();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add(i);//装箱
                int value = (int)array;//拆箱
            }
            array = null;//确保进行垃圾回收
      }

      
      public void EnumAE_RefType()
      {
            ArrayExpandable array = new ArrayExpandable();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add("r");
                string value = (string)array;
            }
            array = null;//确保进行垃圾回收
      }

      
       public void EnumAE_Gen_ValueType()
      {
            ArrayExpandable&lt;int&gt; array = new ArrayExpandable&lt;int&gt;();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add(i);
                int value = array;
            }
            array = null;//确保进行垃圾回收;
      }

      
      public void EnumAE_Gen_RefType()
      {
            ArrayExpandable&lt;string&gt; array = new ArrayExpandable&lt;string&gt;();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add("r");
                string value = array;
            }
            array = null;//确保进行垃圾回收;
      }

      
      public void EnumList_ValueType()
      {
            List&lt;int&gt; array = new List&lt;int&gt;();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add(i);
                int value = array;
            }
            array = null;//确保进行垃圾回收;
      }


      
      public void EnumList_RefType()
      {
            List&lt;string&gt; array = new List&lt;string&gt;();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add("r");
                string value = array;
            }
            array = null;//确保进行垃圾回收;
      }

      
      public void EnumAraayList_valueType()
      {
            ArrayList array = new ArrayList();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add(i);
                int value = (int)array;
            }
            array = null;//确保进行垃圾回收;
      }


      
      public void EnumAraayList_RefType()
      {
            ArrayList array = new ArrayList();
            for (int i = 0; i &lt; 10000; i++)
            {
                array.Add("r");
                string value = (string)array;
            }
            array = null;//确保进行垃圾回收;
      }
    }
</code></pre>
<p> 我还加入了.NETCore3.1和.NET5的对比,且以.NETCore3.1的<code>EnumAraayList_valueType</code>方法为基准,性能测试结果如下:</p>
<p><img src="https://img2020.cnblogs.com/blog/1294271/202103/1294271-20210304143743985-329834922.jpg" alt="" loading="lazy"></p>
<p>用更直观的柱形图来呈现:</p>
<p><img src="https://img2020.cnblogs.com/blog/1294271/202103/1294271-20210304143751771-715849333.jpg" alt="" loading="lazy"></p>
<p> 我们能看到在这里<code>List</code>的性能在引用类型和值类型中都是所以当中是最好的,不管是执行时间、GC次数,分配的内存空间大小,都是最优的,同时.NET5在几乎所有的方法中性能都是优于.NETCore3.1,这里还提一句,我实现的<code>ArrayExpandable</code>和<code>ArrayExpandable&lt;T&gt;</code>性能都差于<code>ArrayList</code>和<code>List</code>,我还没实现<code>IList</code>和各种方法,只能说句dotnet基金会牛逼</p>
<h2 id="三泛型的多态性">三.泛型的多态性</h2>
<h3 id="多态的声明">多态的声明</h3>
<p>类、结构、接口、方法、和委托可以声明一个或者多个类型参数,我们直接看代码:</p>
<pre><code class="language-c#">interface IFoo&lt;InterfaceT&gt;
{
   void InterfaceMenthod(InterfaceT interfaceT);
}

class Foo&lt;ClassT, ClassT1&gt;: IFoo&lt;StringBuilder&gt;
{
   public ClassT1 Field;
   
   public delegate void MyDelegate&lt;DelegateT&gt;(DelegateT delegateT);

   public void DelegateMenthod&lt;DelegateT&gt;(DelegateT delegateT, MyDelegate&lt;DelegateT&gt; myDelegate)
   {
      myDelegate(delegateT);
   }

   public static string operator +(Foo&lt;ClassT, ClassT1&gt; foo,string s)
   {
      return $"{s}:{foo.GetType().Name}";
   }


   public List&lt;ClassT&gt; Property{ get; set; }
   public ClassT1 Property1 { get; set; }

   public ClassT this =&gt; Property;//没判断越界


   public Foo(List&lt;ClassT&gt; classT, ClassT1 classT1)
   {
      Property = classT;
      Property1 = classT1;
      Field = classT1;
      Console.WriteLine($"构造函数:parameter1 type:{Property.GetType().Name},parameter2 type:{Property1.GetType().Name}");
   }

      //方法声明了多个新的类型参数
   public void Method&lt;MenthodT, MenthodT1&gt;(MenthodT menthodT, MenthodT1 menthodT1)
   {
       Console.WriteLine($"Method&lt;MenthodT, MenthodT1&gt;:{(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());
    }
}
</code></pre>
<p>控制台测试代码:</p>
<pre><code class="language-c#">static void Main(string[] args)
{
   Test();
   Console.ReadLine();
}

static void Test()
{
   var list = new List&lt;int&gt;() { 1, 2, 3, 4 };
   var foo = new Foo&lt;int, string&gt;(list, "ryzen");

   var index = 0;
   Console.WriteLine($"索引:索引{index}的值:{foo}");
   
   Console.WriteLine($"Filed:{foo.Field}");

   foo.Method(2333);

   foo.Method&lt;DateTime, long&gt;(DateTime.Now, 2021);

   foo.DelegateMenthod&lt;string&gt;("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}");
}


</code></pre>
<p>输出如下:</p>
<pre><code>构造函数:parameter1 type:List`1,parameter2 type:String
索引:索引0的值:1
Filed:ryzen
Method:Int32:classT?.ToString()
Method&lt;MenthodT, MenthodT1&gt;:DateTime:2021/03/02 11:45:40,Int64:2021
DelegateMenthod:this is a delegate
InterfaceMenthod:this is a interfaceMthod
重载+运算符:Foo`2

</code></pre>
<p>我们通过例子可以看到的是:</p>
<ul>
<li>类(结构也可以),接口,委托,方法都可以声明一个或多个类型参数,体现了声明的多态性</li>
<li>类的函数成员:属性,字段,索引,构造器,运算符只能引入类声明的类型参数,不能够声明,唯有方法这一函数成员具备声明和引用类型参数两种功能,由于具备声明功能,因此可以声明和委托一样的类型参数并且引用它,这也体现了方法的多态性</li>
</ul>
<h3 id="多态的继承">多态的继承</h3>
<p>父类和实现类或接口的接口都可以是实例化类型,直接看代码:</p>
<pre><code class="language-c#">interface IFooBase&lt;IBaseT&gt;{}

interface IFoo&lt;InterfaceT&gt;: IFooBase&lt;string&gt;
{
    void InterfaceMenthod(InterfaceT interfaceT);
}

class FooBase&lt;ClassT&gt;
{

}

class Foo&lt;ClassT, ClassT1&gt;: FooBase&lt;ClassT&gt;,IFoo&lt;StringBuilder&gt;{}

</code></pre>
<p>我们可以通过例子看出:</p>
<ul>
<li>由于<code>Foo</code>的基类<code>FooBase</code>定义的和<code>Foo</code>有着共享的类型参数<code>ClassT</code>,因此可以在继承的时候不实例化类型</li>
<li>而<code>Foo</code>和<code>IFoo</code>接口没定义相同的类型参数,因此可以在继承的时候实例化出接口的类型参数<code>StringBuild</code>出来</li>
<li><code>IFoo</code>和<code>IFooBase</code>没定义相同的类型参数,因此可以在继承的时候实例化出接口的类型参数<code>string</code>出来</li>
<li>上述都体现出继承的多态性</li>
</ul>
<h3 id="多态的递归">多态的递归</h3>
<p>我们定义如下一个类和一个方法,且不会报错:</p>
<pre><code class="language-c#">    class D&lt;T&gt; { }
    class C&lt;T&gt; : D&lt;C&lt;C&lt;T&gt;&gt;&gt;
    {
      void Foo()
      {
            var foo = new C&lt;C&lt;T&gt;&gt;();
            Console.WriteLine(foo.ToString());
      }
    }
</code></pre>
<p>因为<code>T</code>能在实例化的时候确定其类型,因此也支持这种循环套用自己的类和方法的定义</p>
<h2 id="四泛型的约束">四.泛型的约束</h2>
<h3 id="where的约束">where的约束</h3>
<p>我们先上代码:</p>
<pre><code class="language-c#">    class FooBase{ }

    class Foo : FooBase
    {
      
    }
   
    class someClass&lt;T,K&gt; where T:struct where K :FooBase,new()
    {

    }

    static void TestConstraint()
    {
      var someClass = new someClass&lt;int, Foo&gt;();//通过编译
      //var someClass = new someClass&lt;string, Foo&gt;();//编译失败,string不是struct类型
      //var someClass = new someClass&lt;string, long&gt;();//编译失败,long不是FooBase类型
    }

   
</code></pre>
<p>再改动下Foo类:</p>
<pre><code class="language-c#">class Foo : FooBase
{
   public Foo(string str)
   {

   }
}

static void TestConstraint()
{
   var someClass = new someClass&lt;int, Foo&gt;();//编译失败,因为new()约束必须类含有一个无参构造器,可以再给Foo类加上个无参构造器就能编译通过
}

</code></pre>
<p> 我们可以看到,通过<code>where</code>语句,可以对类型参数进行约束,而且一个类型参数支持多个约束条件(例如K),使其在实例化类型参数的时候,必须按照约束的条件对应实例符合条件的类型,而<code>where</code>条件约束的作用就是起在编译期约束类型参数的作用</p>
<h3 id="out和in的约束">out和in的约束</h3>
<p> 说到<code>out</code>和<code>in</code>之前,我们可以说下协变和逆变,在C#中,只有泛型接口和泛型委托可以支持协变和逆变</p>
<h4 id="协变">协变</h4>
<p>我们先看下代码:</p>
<pre><code class="language-c#">class FooBase{ }

class Foo : FooBase
{

}

interface IBar&lt;T&gt;
{
    T GetValue(T t);
}

class Bar&lt;T&gt; : IBar&lt;T&gt;
{
   public T GetValue(T t)
   {
       return t;
   }
}

static void Test()
{
    var foo = new Foo();
    FooBase fooBase = foo;//编译成功

    IBar&lt;Foo&gt; bar = new Bar&lt;Foo&gt;();
    IBar&lt;FooBase&gt; bar1 = bar;//编译失败
}
</code></pre>
<p> 这时候你可能会有点奇怪,为啥那段代码会编译失败,明明<code>Foo</code>类可以隐式转为<code>FooBase</code>,但作为泛型接口类型参数实例化却并不能呢?使用<code>out</code>约束泛型接口<code>IBar</code>的T,那段代码就会编译正常,但是会引出另外一段编译报错:</p>
<pre><code class="language-c#">interface IBar&lt;out T&gt;
{
    T GetValue(string str);//编译成功
    //T GetValue(T t);//编译失败 T不能作为形参输入,用out约束T支持协变,T可以作为返回值输出
   
}

IBar&lt;Foo&gt; bar = new Bar&lt;Foo&gt;();
IBar&lt;FooBase&gt; bar1 = bar;//编译正常
</code></pre>
<p>因此我们可以得出以下结论:</p>
<ul>
<li>由于<code>Foo</code>继承<code>FooBase</code>,本身子类<code>Foo</code>包含着父类允许访问的成员,因此能隐式转换父类,这是类型安全的转换,因此叫协变</li>
<li>在为泛型接口用<code>out</code>标识其类型参数支持协变后,约束其方法的返回值和属性的Get(本质也是个返回值的方法)才能引用所声明的类型参数,也就是作为输出值,用<code>out</code>很明显的突出了这一意思</li>
</ul>
<p>而支持迭代的泛型接口<code>IEnumerable</code>也是这么定义的:</p>
<pre><code class="language-c#">    public interface IEnumerable&lt;out T&gt; : IEnumerable
    {
      new IEnumerator&lt;T&gt; GetEnumerator();
    }
</code></pre>
<h4 id="逆变">逆变</h4>
<p>我们将上面代码改下:</p>
<pre><code class="language-c#">class FooBase{ }

class Foo : FooBase
{

}

interface IBar&lt;T&gt;
{
    T GetValue(T t);
}

class Bar&lt;T&gt; : IBar&lt;T&gt;
{
   public T GetValue(T t)
   {
       return t;
   }
}

static void Test1()
{
    var fooBase = new FooBase();
    Foo foo = (Foo)fooBase;//编译通过,运行时报错

    IBar&lt;FooBase&gt; bar = new Bar&lt;FooBase&gt;();
    IBar&lt;Foo&gt; bar1 = (IBar&lt;Foo&gt;)bar;//编译通过,运行时报错
}
</code></pre>
<p>我们再改动下IBar,发现出现另外一处编译失败</p>
<pre><code class="language-c#">interface IBar&lt;in T&gt;
{
    void GetValue(T t);//编译成功
    //T GetValue(T t);//编译失败 T不能作为返回值输出,用in约束T支持逆变,T可以作为返回值输出
}

IBar&lt;FooBase&gt; bar = new Bar&lt;FooBase&gt;();
IBar&lt;Foo&gt; bar1 = (IBar&lt;Foo&gt;)bar;//编译通过,运行时不报错
IBar&lt;Foo&gt; bar1 = bar;//编译通过,运行时不报错
</code></pre>
<p>因此我们可以得出以下结论:</p>
<ul>
<li>由于<code>FooBase</code>是<code>Foo</code>的父类,并不包含子类的自由的成员,转为为子类<code>Foo</code>是类型不安全的,因此在运行时强式转换的报错了,但编译期是不能够确认的</li>
<li>在为泛型接口用<code>in</code>标识其类型参数支持逆变后,<code>in</code>约束其接口成员不能将其作为返回值(输出值),我们会发现协变和逆变正是一对反义词</li>
<li>这里提一句,值类型是不支持协变和逆变的</li>
</ul>
<p>同样的泛型委托<code>Action</code>就是个逆变的例子:</p>
<pre><code class="language-c#">public delegate void Action&lt;in T&gt;(T obj);
</code></pre>
<h2 id="五泛型的反射">五.泛型的反射</h2>
<p>我们先来看看以下代码:</p>
<pre><code class="language-c#">static void Main(string[] args)
{
    var lsInt = new ArrayExpandable&lt;int&gt;();
    lsInt.Add(1);
    var lsStr = new ArrayExpandable&lt;string&gt;();
    lsStr.Add("ryzen");
    var lsStr1 = new ArrayExpandable&lt;string&gt;();
    lsStr.Add("ryzen");
}
</code></pre>
<p>然后通过ildasm查看其IL,开启视图-》显示标记值,查看Main方法:</p>
<pre><code class="language-c#">voidMain(string[] args) cil managed
{
.entrypoint
// 代码大小       52 (0x34)
.maxstack2
.locals /*11000001*/ init (class MetaTest.ArrayExpandable`1/*02000003*/&lt;int32&gt; V_0,
         class MetaTest.ArrayExpandable`1/*02000003*/&lt;string&gt; V_1,
         class MetaTest.ArrayExpandable`1/*02000003*/&lt;string&gt; V_2)
IL_0000:nop
IL_0001:newobj   instance void class MetaTest.ArrayExpandable`1/*02000003*/&lt;int32&gt;/*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*/&lt;int32&gt;/*1B000001*/::Add(!0) /* 0A00000D */
IL_000e:nop
IL_000f:newobj   instance void class MetaTest.ArrayExpandable`1/*02000003*/&lt;string&gt;/*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*/&lt;string&gt;/*1B000002*/::Add(!0) /* 0A00000F */
IL_0020:nop
IL_0021:newobj   instance void class MetaTest.ArrayExpandable`1/*02000003*/&lt;string&gt;/*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*/&lt;string&gt;/*1B000002*/::Add(!0) /* 0A00000F */
IL_0032:nop
IL_0033:ret
} // end of method Program::Main
</code></pre>
<p>打开元数据表将上面所涉及到的元数据定义表和类型规格表列出:</p>
<p>metainfo:</p>
<pre><code class="language-c#">-----------定义部分
TypeDef #2 (02000003)
-------------------------------------------------------
        TypDefName: MetaTest.ArrayExpandable`1(02000003)
        Flags   : (00100001)
        Extends   : 0100000C System.Object
        1 Generic Parameters
                (0) GenericParamToken : (2a000001) Name : T flags: 00000000 Owner: 02000003
       
        Method #8 (0600000a)
        -------------------------------------------------------
                MethodName: Add (0600000A)
                Flags   : (00000086)
                RVA       : 0x000021f4
                ImplFlags : (00000000)
                CallCnvntn:
                hasThis
                ReturnType: Void
                1 Arguments
                        Argument #1:Var!0
                1 Parameters
                (1) ParamToken : (08000007) Name : value flags: (00000000)
               

------类型规格部分
TypeSpec #1 (1b000001)
-------------------------------------------------------
        TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1&lt; I4&gt; //14代表int32
        MemberRef #1 (0a00000c)
        -------------------------------------------------------
                Member: (0a00000c) .ctor:
                CallCnvntn:
                hasThis
                ReturnType: Void
                No arguments.
        MemberRef #2 (0a00000d)
        -------------------------------------------------------
                Member: (0a00000d) Add:
                CallCnvntn:
                hasThis
                ReturnType: Void
                1 Arguments
                        Argument #1:Var!0

TypeSpec #2 (1b000002)
-------------------------------------------------------
        TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1&lt; String&gt;
        MemberRef #1 (0a00000e)
        -------------------------------------------------------
                Member: (0a00000e) .ctor:
                CallCnvntn:
                hasThis
                ReturnType: Void
                No arguments.
        MemberRef #2 (0a00000f)
        -------------------------------------------------------
                Member: (0a00000f) Add:
                CallCnvntn:
                hasThis
                ReturnType: Void
                1 Arguments
                Argument #1:Var!0
</code></pre>
<p> 这时候我们就可以看出,元数据为泛型类<code>ArrayExpandable&lt;T&gt;</code>定义一份定义表,生成两份规格,也就是当你实例化类型参数为<code>int</code>和<code>string</code>的时候,分别生成了两份规格代码,同时还发现以下的现象:</p>
<pre><code class="language-c#">var lsInt = new ArrayExpandable&lt;int&gt;();//引用的是类型规格1b000001的成员0a00000c .ctor构造
lsInt.Add(1);//引用的是类型规格1b000001的成员0a00000d Add
   
var lsStr = new ArrayExpandable&lt;string&gt;();//引用的是类型规格1b000002的成员0a00000e .ctor构造
lsStr.Add("ryzen");//引用的是类型规格1b000002的成员0a00000f Add
var lsStr1 = new ArrayExpandable&lt;string&gt;();//和lsStr一样
lsStr.Add("ryzen");//和lsStr一样


</code></pre>
<p> 非常妙的是,当你实例化两个一样的类型参数<code>string</code>,是共享一份类型规格的,也就是同享一份本地代码,因此上面的代码在线程堆栈和托管堆的大致是这样的:</p>
<p><img src="https://img2020.cnblogs.com/blog/1294271/202103/1294271-20210304143834780-396008966.jpg" alt="" loading="lazy"></p>
<p>由于泛型也有元数据的存在,因此可以对其做反射:</p>
<pre><code class="language-c#">Console.WriteLine($"-----------{nameof(lsInt)}---------------");
Console.WriteLine($"{nameof(lsInt)} is generic?:{lsInt.GetType().IsGenericType}");
Console.WriteLine($"Generic type:{lsInt.GetType().GetGenericArguments().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().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}");
}
</code></pre>
<p>输出:</p>
<pre><code>-----------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
</code></pre>
<h2 id="六总结">六.总结</h2>
<p> 泛型编程作为.NET体系中一个很重要的编程思想,主要有以下亮点:</p>
<ul>
<li>编译期确定类型,避免值类型的拆装箱和不必要的运行时类型检验,同样运行时也能通过<code>is</code>和<code>as</code>进行类型检验</li>
<li>通过约束进行对类型参数实例化的范围</li>
<li>同时在IL层面,实例化相同类型参数的时候共享一份本地代码</li>
<li>由于元数据的存在,也能在运行时进行反射,增强其灵活性</li>
</ul>
<h2 id="参考">参考</h2>
<p>Design and Implementation of Generics for the .NET Common Language Runtime</p>
<p>https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/</p>
<p>《CLR Via C# 第四版》</p>
<p>《你必须知道的.NET(第二版)》</p><br><br>
来源:https://www.cnblogs.com/ryzen/p/14480171.html
頁: [1]
查看完整版本: 理解C#泛型运作原理