理解C#泛型运作原理

前言

 我们都知道泛型在C#的重要性,泛型是OOP语言中三大特征的多态的最重要的体现,几乎泛型撑起了整个.NET框架,在讲泛型之前,我们可以抛出一个问题,我们现在需要一个可扩容的数组类,且满足所有类型,不管是值类型还是引用类型,那么在没有用泛型方法实现,如何实现?

一.泛型之前的故事

 我们肯定会想到用object来作为类型参数,因为在C#中,所有类型都是基于Object类型的。因此Object是所有类型的最基类,那么我们的可扩容数组类如下:

Copy public class ArrayExpandable{private object?[] _items = null;private int _defaultCapacity = 4;private int _size;public object? this[int index]{get{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));return _items[index];}set{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));_items[index] = value;}}public int Capacity{get => _items.Length;set{if (value < _size){throw new ArgumentOutOfRangeException(nameof(value));}if (value != _items.Length){if (value > 0){object[] newItems = new object[value];if (_size > 0){Array.Copy(_items, newItems, _size);}_items = newItems;}else{_items = new object[_defaultCapacity];}}}}public int Count => _size;public ArrayExpandable(){_items = new object?[0];}public ArrayExpandable(int capacity){_items = new object?[capacity];}public void Add(object? value){//数组元素为0或者数组元素容量满if (_size == _items.Length) EnsuresCapacity(_size + 1);_items[_size] = value;_size++;}private void EnsuresCapacity(int size){if (_items.Length < size){int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;if (newCapacity < size) newCapacity = size;Capacity = newCapacity;}}

然后我们来验证下:

Copyvar arrayStr = new ArrayExpandable();
var strs = new string[] { "ryzen", "reed", "wymen" };
for (int i = 0; i < strs.Length; i++)
{arrayStr.Add(strs[i]);string value = (string)arrayStr[i];//改为int value = (int)arrayStr[i] 运行时报错Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");var array = new ArrayExpandable();
for (int i = 0; i < 5; i++)
{array.Add(i);int value = (int)array[i];Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");

输出:

Copyryzen
reed
wymen
gavin
Now arrayStr Capacity:4
Now array Capacity:8

 貌似输出结果是正确的,能够动态进行扩容,同样的支持值类型Structint32和引用类型的字符串,但是其实这里会发现一些问题,那就是

  1. 引用类型string进行了类型转换的验证

  2. 值类型int32进行了装箱和拆箱操作,同时进行类型转换类型的检验

  3. 发生的这一切都是在运行时的,假如类型转换错误,得在运行时才能报错

大致执行模型如下:

引用类型:

值类型:

 那么有没有一种方法能够避免上面遇到的三种问题呢?在借鉴了cpp的模板和java的泛型经验,在C#2.0的时候推出了更适合.NET体系下的泛型

二.用泛型实现

Copypublic class ArrayExpandable<T>
{private T[] _items;private int _defaultCapacity = 4;private int _size;public T this[int index]{get{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));return _items[index];}set{if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException(nameof(index));_items[index] = value;}}public int Capacity{get => _items.Length;set{if (value < _size){throw new ArgumentOutOfRangeException(nameof(value));}if (value != _items.Length){if (value > 0){T[] newItems = new T[value];if (_size > 0){Array.Copy(_items, newItems, _size);}_items = newItems;}else{_items = new T[_defaultCapacity];}}}}public int Count => _size;public ArrayExpandable(){_items = new T[0];}public ArrayExpandable(int capacity){_items = new T[capacity];}public void Add(T value){//数组元素为0或者数组元素容量满if (_size == _items.Length) EnsuresCapacity(_size + 1);_items[_size] = value;_size++;}private void EnsuresCapacity(int size){if (_items.Length < size){int newCapacity = _items.Length == 0 ? _defaultCapacity : _items.Length * 2;if (newCapacity < size) newCapacity = size;Capacity = newCapacity;}}}

那么测试代码则改写为如下:

Copyvar arrayStr = new ArrayExpandable<string>();
var strs = new string[] { "ryzen", "reed", "wymen", "gavin" };
for (int i = 0; i < strs.Length; i++)
{arrayStr.Add(strs[i]);string value = arrayStr[i];//改为int value = arrayStr[i] 编译报错Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(arrayStr)} Capacity:{arrayStr.Capacity}");var array = new ArrayExpandable<int>();
for (int i = 0; i < 5; i++)
{array.Add(i);int value = array[i];Console.WriteLine(value);
}
Console.WriteLine($"Now {nameof(array)} Capacity:{array.Capacity}");

输出:

Copyryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8

我们通过截取部分ArrayExpandable<T>的IL查看其本质是个啥:

Copy//声明类
.class public auto ansi beforefieldinit MetaTest.ArrayExpandable`1<T>extends [System.Runtime]System.Object
{.custom instance void [System.Runtime]System.Reflection.DefaultMemberAttribute::.ctor(string) = ( 01 00 04 49 74 65 6D 00 00 )                      
} //Add方法
.method public hidebysig instance void  Add(!T 'value') cil managed
{// 代码大小       69 (0x45).maxstack  3.locals init (bool V_0)IL_0000:  nopIL_0001:  ldarg.0IL_0002:  ldfld      int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_0007:  ldarg.0IL_0008:  ldfld      !0[] class MetaTest.ArrayExpandable`1<!T>::_itemsIL_000d:  ldlenIL_000e:  conv.i4IL_000f:  ceqIL_0011:  stloc.0IL_0012:  ldloc.0IL_0013:  brfalse.s  IL_0024IL_0015:  ldarg.0IL_0016:  ldarg.0IL_0017:  ldfld      int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_001c:  ldc.i4.1IL_001d:  addIL_001e:  call       instance void class MetaTest.ArrayExpandable`1<!T>::EnsuresCapacity(int32)IL_0023:  nopIL_0024:  ldarg.0IL_0025:  ldfld      !0[] class MetaTest.ArrayExpandable`1<!T>::_itemsIL_002a:  ldarg.0IL_002b:  ldfld      int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_0030:  ldarg.1IL_0031:  stelem     !TIL_0036:  ldarg.0IL_0037:  ldarg.0IL_0038:  ldfld      int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_003d:  ldc.i4.1IL_003e:  addIL_003f:  stfld      int32 class MetaTest.ArrayExpandable`1<!T>::_sizeIL_0044:  ret
} // end of method ArrayExpandable`1::Add

 原来定义的时候就是用了个T作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符,我们可以在编译期就知道了其类型,且不用在运行时进行类型检测,而我们也可以对比ArrayExpandableArrayExpandable<T>在类型为值类型中的IL,查看是否进行拆箱和装箱操作,以下为IL截取部分:

ArrayExpandable:

Copy  IL_0084:  newobj     instance void GenericSample.ArrayExpandable::.ctor()IL_0089:  stloc.2IL_008a:  ldc.i4.0IL_008b:  stloc.s    V_6IL_008d:  br.s       IL_00bcIL_008f:  nopIL_0090:  ldloc.2IL_0091:  ldloc.s    V_6IL_0093:  box        [System.Runtime]System.Int32 //box为装箱操作IL_0098:  callvirt   instance void GenericSample.ArrayExpandable::Add(object)IL_009d:  nopIL_009e:  ldloc.2IL_009f:  ldloc.s    V_6IL_00a1:  callvirt   instance object GenericSample.ArrayExpandable::get_Item(int32)IL_00a6:  unbox.any  [System.Runtime]System.Int32 //unbox为拆箱操作

ArrayExpandable:

Copy IL_007f:  newobj     instance void class GenericSample.ArrayExpandable`1<int32>::.ctor()IL_0084:  stloc.2IL_0085:  ldc.i4.0IL_0086:  stloc.s    V_6IL_0088:  br.s       IL_00adIL_008a:  nopIL_008b:  ldloc.2IL_008c:  ldloc.s    V_6IL_008e:  callvirt  instance void class GenericSample.ArrayExpandable`1<int32>::Add(!0)IL_0093:  nopIL_0094:  ldloc.2IL_0095:  ldloc.s    V_6IL_0097:  callvirt   instance !0 class GenericSample.ArrayExpandable`1<int32>::get_Item(int32)

 我们从IL也能看的出来,ArrayExpandable<T>T作为一个类型参数,在编译后在IL已经确定了其类型,因此当然也就不存在装拆箱的情况,在编译期的时候IDE能够检测类型,因此也就不用在运行时进行类型检测,但并不代表不能通过运行时检测类型(可通过is和as),还能通过反射体现出泛型的灵活性,后面会讲到

 其实有了解ArrayListList的朋友就知道,ArrayExpandableArrayExpandable<T>其实现大致就是和它们一样,只是简化了很多的版本,我们这里可以通过 BenchmarkDotNet 来测试其性能对比,代码如下:

Copy    [SimpleJob(RuntimeMoniker.NetCoreApp31,baseline:true)][SimpleJob(RuntimeMoniker.NetCoreApp50)][MemoryDiagnoser]public class TestClass{[Benchmark]public void EnumAE_ValueType(){ArrayExpandable array = new ArrayExpandable();for (int i = 0; i < 10000; i++){array.Add(i);//装箱int value = (int)array[i];//拆箱}array = null;//确保进行垃圾回收}[Benchmark]public void EnumAE_RefType(){ArrayExpandable array = new ArrayExpandable();for (int i = 0; i < 10000; i++){array.Add("r");string value = (string)array[i];}array = null;//确保进行垃圾回收}[Benchmark]public void EnumAE_Gen_ValueType(){ArrayExpandable<int> array = new ArrayExpandable<int>();for (int i = 0; i < 10000; i++){array.Add(i);int value = array[i];}array = null;//确保进行垃圾回收;}[Benchmark]public void EnumAE_Gen_RefType(){ArrayExpandable<string> array = new ArrayExpandable<string>();for (int i = 0; i < 10000; i++){array.Add("r");string value = array[i];}array = null;//确保进行垃圾回收;}[Benchmark]public void EnumList_ValueType(){List<int> array = new List<int>();for (int i = 0; i < 10000; i++){array.Add(i);int value = array[i];}array = null;//确保进行垃圾回收;}[Benchmark]public void EnumList_RefType(){List<string> array = new List<string>();for (int i = 0; i < 10000; i++){array.Add("r");string value = array[i];}array = null;//确保进行垃圾回收;}[Benchmark(Baseline =true)]public void EnumAraayList_valueType(){ArrayList array = new ArrayList();for (int i = 0; i < 10000; i++){array.Add(i);int value = (int)array[i];}array = null;//确保进行垃圾回收;}[Benchmark]public void EnumAraayList_RefType(){ArrayList array = new ArrayList();for (int i = 0; i < 10000; i++){array.Add("r");string value = (string)array[i];}array = null;//确保进行垃圾回收;}}

 我还加入了.NETCore3.1和.NET5的对比,且以.NETCore3.1的EnumAraayList_valueType方法为基准,性能测试结果如下:

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

 我们能看到在这里List的性能在引用类型和值类型中都是所以当中是最好的,不管是执行时间、GC次数,分配的内存空间大小,都是最优的,同时.NET5在几乎所有的方法中性能都是优于.NETCore3.1,这里还提一句,我实现的ArrayExpandableArrayExpandable<T>性能都差于ArrayListList,我还没实现IList和各种方法,只能说句dotnet基金会牛逼

三.泛型的多态性

多态的声明

类、结构、接口、方法、和委托可以声明一个或者多个类型参数,我们直接看代码:

Copyinterface IFoo<InterfaceT>
{void InterfaceMenthod(InterfaceT interfaceT);
}class Foo<ClassT, ClassT1>: IFoo<StringBuilder>
{public ClassT1 Field;public delegate void MyDelegate<DelegateT>(DelegateT delegateT);public void DelegateMenthod<DelegateT>(DelegateT delegateT, MyDelegate<DelegateT> myDelegate){myDelegate(delegateT);}public static string operator +(Foo<ClassT, ClassT1> foo,string s){return $"{s}:{foo.GetType().Name}";}public List<ClassT> Property{ get; set; }public ClassT1 Property1 { get; set; }public ClassT this[int index] => Property[index];//没判断越界public Foo(List<ClassT> classT, ClassT1 classT1){Property = classT;Property1 = classT1;Field = classT1;Console.WriteLine($"构造函数:parameter1 type:{Property.GetType().Name},parameter2 type:{Property1.GetType().Name}");}//方法声明了多个新的类型参数public void Method<MenthodT, MenthodT1>(MenthodT menthodT, MenthodT1 menthodT1){Console.WriteLine($"Method<MenthodT, MenthodT1>:{(menthodT.GetType().Name)}:{menthodT.ToString()}," +$"{menthodT1.GetType().Name}:{menthodT1.ToString()}");}public void Method(ClassT classT){Console.WriteLine($"{nameof(Method)}:{classT.GetType().Name}:classT?.ToString()");}public void InterfaceMenthod(StringBuilder interfaceT){Console.WriteLine(interfaceT.ToString());}
}

控制台测试代码:

Copystatic void Main(string[] args)
{Test();Console.ReadLine();
}static void Test()
{var list = new List<int>() { 1, 2, 3, 4 };var foo = new Foo<int, string>(list, "ryzen");var index = 0;Console.WriteLine($"索引:索引{index}的值:{foo[index]}");Console.WriteLine($"Filed:{foo.Field}");foo.Method(2333);foo.Method<DateTime, long>(DateTime.Now, 2021);foo.DelegateMenthod<string>("this is a delegate", DelegateMenthod);foo.InterfaceMenthod(new StringBuilder().Append("InterfaceMenthod:this is a interfaceMthod"));Console.WriteLine(foo+"重载+运算符");
}static void DelegateMenthod(string str)
{Console.WriteLine($"{nameof(DelegateMenthod)}:{str}");
}

输出如下:

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

我们通过例子可以看到的是:

  • 类(结构也可以),接口,委托,方法都可以声明一个或多个类型参数,体现了声明的多态性

  • 类的函数成员:属性,字段,索引,构造器,运算符只能引入类声明的类型参数,不能够声明,唯有方法这一函数成员具备声明和引用类型参数两种功能,由于具备声明功能,因此可以声明和委托一样的类型参数并且引用它,这也体现了方法的多态性

多态的继承

父类和实现类或接口的接口都可以是实例化类型,直接看代码:

Copyinterface IFooBase<IBaseT>{}interface IFoo<InterfaceT>: IFooBase<string>
{void InterfaceMenthod(InterfaceT interfaceT);
}class FooBase<ClassT>
{}class Foo<ClassT, ClassT1>: FooBase<ClassT>,IFoo<StringBuilder>{}

我们可以通过例子看出:

  • 由于Foo的基类FooBase定义的和Foo有着共享的类型参数ClassT,因此可以在继承的时候不实例化类型

  • FooIFoo接口没定义相同的类型参数,因此可以在继承的时候实例化出接口的类型参数StringBuild出来

  • IFooIFooBase没定义相同的类型参数,因此可以在继承的时候实例化出接口的类型参数string出来

  • 上述都体现出继承的多态性

多态的递归

我们定义如下一个类和一个方法,且不会报错:

Copy    class D<T> { }class C<T> : D<C<C<T>>> { void Foo(){var foo = new C<C<T>>();Console.WriteLine(foo.ToString());}}

因为T能在实例化的时候确定其类型,因此也支持这种循环套用自己的类和方法的定义

四.泛型的约束

where的约束

我们先上代码:

Copy    class FooBase{ }class Foo : FooBase {}class someClass<T,K> where T:struct where K :FooBase,new(){}static void TestConstraint(){var someClass = new someClass<int, Foo>();//通过编译//var someClass = new someClass<string, Foo>();//编译失败,string不是struct类型//var someClass = new someClass<string, long>();//编译失败,long不是FooBase类型}

再改动下Foo类:

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

 我们可以看到,通过where语句,可以对类型参数进行约束,而且一个类型参数支持多个约束条件(例如K),使其在实例化类型参数的时候,必须按照约束的条件对应实例符合条件的类型,而where条件约束的作用就是起在编译期约束类型参数的作用

out和in的约束

 说到outin之前,我们可以说下协变和逆变,在C#中,只有泛型接口和泛型委托可以支持协变和逆变

协变

我们先看下代码:

Copyclass FooBase{ }class Foo : FooBase 
{}interface IBar<T> 
{T GetValue(T t);
}class Bar<T> : IBar<T>
{public T GetValue(T t){return t;}
}static void Test()
{var foo = new Foo();FooBase fooBase = foo;//编译成功IBar<Foo> bar = new Bar<Foo>();IBar<FooBase> bar1 = bar;//编译失败}

 这时候你可能会有点奇怪,为啥那段代码会编译失败,明明Foo类可以隐式转为FooBase,但作为泛型接口类型参数实例化却并不能呢?使用out约束泛型接口IBar的T,那段代码就会编译正常,但是会引出另外一段编译报错:

Copyinterface IBar<out T> 
{T GetValue(string str);//编译成功//T GetValue(T t);//编译失败 T不能作为形参输入,用out约束T支持协变,T可以作为返回值输出}IBar<Foo> bar = new Bar<Foo>();
IBar<FooBase> bar1 = bar;//编译正常

因此我们可以得出以下结论:

  • 由于Foo继承FooBase,本身子类Foo包含着父类允许访问的成员,因此能隐式转换父类,这是类型安全的转换,因此叫协变

  • 在为泛型接口用out标识其类型参数支持协变后,约束其方法的返回值和属性的Get(本质也是个返回值的方法)才能引用所声明的类型参数,也就是作为输出值,用out很明显的突出了这一意思

而支持迭代的泛型接口IEnumerable也是这么定义的:

Copy    public interface IEnumerable<out T> : IEnumerable{new IEnumerator<T> GetEnumerator();}

逆变

我们将上面代码改下:

Copyclass FooBase{ }class Foo : FooBase 
{}interface IBar<T> 
{T GetValue(T t);
}class Bar<T> : IBar<T>
{public T GetValue(T t){return t;}
}static void Test1()
{var fooBase = new FooBase();Foo foo = (Foo)fooBase;//编译通过,运行时报错IBar<FooBase> bar = new Bar<FooBase>();IBar<Foo> bar1 = (IBar<Foo>)bar;//编译通过,运行时报错
}

我们再改动下IBar,发现出现另外一处编译失败

Copyinterface IBar<in T> 
{void GetValue(T t);//编译成功//T GetValue(T t);//编译失败 T不能作为返回值输出,用in约束T支持逆变,T可以作为返回值输出
}IBar<FooBase> bar = new Bar<FooBase>();IBar<Foo> bar1 = (IBar<Foo>)bar;//编译通过,运行时不报错IBar<Foo> bar1 = bar;//编译通过,运行时不报错

因此我们可以得出以下结论:

  • 由于FooBaseFoo的父类,并不包含子类的自由的成员,转为为子类Foo是类型不安全的,因此在运行时强式转换的报错了,但编译期是不能够确认的

  • 在为泛型接口用in标识其类型参数支持逆变后,in约束其接口成员不能将其作为返回值(输出值),我们会发现协变和逆变正是一对反义词

  • 这里提一句,值类型是不支持协变和逆变的

同样的泛型委托Action就是个逆变的例子:

Copypublic delegate void Action<in T>(T obj);

五.泛型的反射

我们先来看看以下代码:

Copystatic void Main(string[] args)
{var lsInt = new ArrayExpandable<int>();lsInt.Add(1);var lsStr = new ArrayExpandable<string>();lsStr.Add("ryzen");var lsStr1 = new ArrayExpandable<string>();lsStr.Add("ryzen");
}

然后通过ildasm查看其IL,开启视图-》显示标记值,查看Main方法:

Copyvoid  Main(string[] args) cil managed
{.entrypoint// 代码大小       52 (0x34).maxstack  2.locals /*11000001*/ init (class MetaTest.ArrayExpandable`1/*02000003*/<int32> V_0,class MetaTest.ArrayExpandable`1/*02000003*/<string> V_1,class MetaTest.ArrayExpandable`1/*02000003*/<string> V_2)IL_0000:  nopIL_0001:  newobj     instance void class MetaTest.ArrayExpandable`1/*02000003*/<int32>/*1B000001*/::.ctor() /* 0A00000C */IL_0006:  stloc.0IL_0007:  ldloc.0IL_0008:  ldc.i4.1IL_0009:  callvirt   instance void class MetaTest.ArrayExpandable`1/*02000003*/<int32>/*1B000001*/::Add(!0) /* 0A00000D */IL_000e:  nopIL_000f:  newobj     instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::.ctor() /* 0A00000E */IL_0014:  stloc.1IL_0015:  ldloc.1IL_0016:  ldstr      "ryzen" /* 70000001 */IL_001b:  callvirt   instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::Add(!0) /* 0A00000F */IL_0020:  nopIL_0021:  newobj     instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::.ctor() /* 0A00000E */IL_0026:  stloc.2IL_0027:  ldloc.1IL_0028:  ldstr      "ryzen" /* 70000001 */IL_002d:  callvirt   instance void class MetaTest.ArrayExpandable`1/*02000003*/<string>/*1B000002*/::Add(!0) /* 0A00000F */IL_0032:  nopIL_0033:  ret
} // end of method Program::Main

打开元数据表将上面所涉及到的元数据定义表和类型规格表列出:

metainfo:

Copy-----------定义部分
TypeDef #2 (02000003)
-------------------------------------------------------TypDefName: MetaTest.ArrayExpandable`1  (02000003)Flags     : [Public] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100001)Extends   : 0100000C [TypeRef] System.Object1 Generic Parameters(0) GenericParamToken : (2a000001) Name : T flags: 00000000 Owner: 02000003Method #8 (0600000a) -------------------------------------------------------MethodName: Add (0600000A)Flags     : [Public] [HideBySig] [ReuseSlot]  (00000086)RVA       : 0x000021f4ImplFlags : [IL] [Managed]  (00000000)CallCnvntn: [DEFAULT]hasThis ReturnType: Void1 ArgumentsArgument #1:  Var!01 Parameters(1) ParamToken : (08000007) Name : value flags: [none] (00000000)------类型规格部分
TypeSpec #1 (1b000001)
-------------------------------------------------------TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< I4> //14代表int32MemberRef #1 (0a00000c)-------------------------------------------------------Member: (0a00000c) .ctor: CallCnvntn: [DEFAULT]hasThis ReturnType: VoidNo arguments.MemberRef #2 (0a00000d)-------------------------------------------------------Member: (0a00000d) Add: CallCnvntn: [DEFAULT]hasThis ReturnType: Void1 ArgumentsArgument #1:  Var!0TypeSpec #2 (1b000002)
-------------------------------------------------------TypeSpec : GenericInst Class MetaTest.ArrayExpandable`1< String>MemberRef #1 (0a00000e)-------------------------------------------------------Member: (0a00000e) .ctor: CallCnvntn: [DEFAULT]hasThis ReturnType: VoidNo arguments.MemberRef #2 (0a00000f)-------------------------------------------------------Member: (0a00000f) Add: CallCnvntn: [DEFAULT]hasThis ReturnType: Void1 ArgumentsArgument #1:  Var!0

 这时候我们就可以看出,元数据为泛型类ArrayExpandable<T>定义一份定义表,生成两份规格,也就是当你实例化类型参数为intstring的时候,分别生成了两份规格代码,同时还发现以下的现象:

Copyvar lsInt = new ArrayExpandable<int>();//引用的是类型规格1b000001的成员0a00000c .ctor构造
lsInt.Add(1);//引用的是类型规格1b000001的成员0a00000d Addvar lsStr = new ArrayExpandable<string>();//引用的是类型规格1b000002的成员0a00000e .ctor构造
lsStr.Add("ryzen");//引用的是类型规格1b000002的成员0a00000f Add
var lsStr1 = new ArrayExpandable<string>();//和lsStr一样
lsStr.Add("ryzen");//和lsStr一样

 非常妙的是,当你实例化两个一样的类型参数string,是共享一份类型规格的,也就是同享一份本地代码,因此上面的代码在线程堆栈和托管堆的大致是这样的:

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

CopyConsole.WriteLine($"-----------{nameof(lsInt)}---------------");
Console.WriteLine($"{nameof(lsInt)} is generic?:{lsInt.GetType().IsGenericType}");
Console.WriteLine($"Generic type:{lsInt.GetType().GetGenericArguments()[0].Name}");
Console.WriteLine("---------Menthods:");
foreach (var method in lsInt.GetType().GetMethods())
{Console.WriteLine(method.Name);
}
Console.WriteLine("---------Properties:");
foreach (var property in lsInt.GetType().GetProperties())
{Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}");
}Console.WriteLine($"\n-----------{nameof(lsStr)}---------------");
Console.WriteLine($"{nameof(lsStr)} is generic?:{lsStr.GetType().IsGenericType}");
Console.WriteLine($"Generic type:{lsStr.GetType().GetGenericArguments()[0].Name}");
Console.WriteLine("---------Menthods:");
foreach (var method in lsStr.GetType().GetMethods())
{Console.WriteLine(method.Name);
}
Console.WriteLine("---------Properties:");
foreach (var property in lsStr.GetType().GetProperties())
{Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}");
}

输出:

Copy-----------lsInt---------------
lsInt is generic?:True
Generic type:Int32
---------Menthods:
get_Item
set_Item
get_Capacity
set_Capacity
get_Count
Add
GetType
ToString
Equals
GetHashCode
---------Properties:
System.Int32:Item
System.Int32:Capacity
System.Int32:Count-----------lsStr---------------
lsStr is generic?:True
Generic type:String
---------Menthods:
get_Item
set_Item
get_Capacity
set_Capacity
get_Count
Add
GetType
ToString
Equals
GetHashCode
---------Properties:
System.String:Item
System.Int32:Capacity
System.Int32:Count

六.总结

 泛型编程作为.NET体系中一个很重要的编程思想,主要有以下亮点:

  • 编译期确定类型,避免值类型的拆装箱和不必要的运行时类型检验,同样运行时也能通过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(第二版)》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/304247.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

visio软件接口流程图_Microsoft Office Visio绘制系统框图以及流程图的操作步骤

最近很多朋友咨询关于Microsoft OfficeVisio如何绘制系统框图以及流程图的问题&#xff0c;今天的这篇教程就来聊一聊这个话题&#xff0c;希望可以帮助到有需要的朋友。Microsoft Office Visio绘制系统框图以及流程图的操作步骤我们先在桌面空白处或文件夹下&#xff0c;点击鼠…

游戏计算机的显示器,玩游戏用多大显示器好?聊聊电脑显示器多大尺寸合适

最近有多位网友在“电脑百事网”微信公众号中留言问到“显示器多大尺寸合适”、“玩游戏用多大显示器好”类似的相关问题。今天小编就来抽空解答一下&#xff0c;希望对有类似问题的网络朋友有所参考。电脑显示器多大尺寸合适Q&#xff1a;玩游戏用多大显示器好&#xff1f;A&a…

搭建基于域名的虚拟web主机

创建两个目录&#xff0c;分别存放accp和benet两个网站的网页文件<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />创建两个页面用以以后的测试因为我的主页写的是index.htm&#xff0c;在配置文件httpd.conf中没有这个引导页面&…

百度地图大数据告诉你一线城市真相

01 城市人口吸引力大PK&#xff01;2017年度城市人口吸引力指数排名▼划重点&#xff1a;1、第三列里的省会城市南昌、长春、乌鲁木齐、兰州、海口、呼和浩特、西宁是对人口的吸引力较弱。2、第二列里的贵阳、沈阳、哈尔滨、石家庄、福州、合肥、南宁、昆明对人口的吸引力尚可。…

微软开源Power Fx,基于Excel的低代码编程语言

喜欢就关注我们吧&#xff01;微软宣布推出新的开源编程语言 Power Fx&#xff0c;一种基于 Microsoft Excel 的低代码公式语言&#xff1b;将可以在整个 Microsoft Power Platform 中进行使用。该语言背后的动机是开发一些 Excel 用户熟悉的东西&#xff0c;以内容为中心而不是…

python浅拷贝_Python中的浅拷贝和深拷贝

本文翻译自copy in Python (Deep Copy and Shallow Copy)&#xff0c;讲述了在Python语言中浅拷贝与深拷贝的不同用法。全文系作者原创&#xff0c;仅供学习参考使用&#xff0c;转载授权请私信联系&#xff0c;否则将视为侵权行为。码字不易&#xff0c;感谢支持。以下为全文内…

常用计算机二级函数,计算机二级MS office常用函数

计算机二级MS office常用函数1、DAVERAGE用途&#xff1a;返回数据库或数据清单中满足指定条件的列中数值的平均值。语法&#xff1a;DAVERAGE(database&#xff0c;field&#xff0c;criteria)参数&#xff1a;Database 构成列表或数据库的单元格区域;Field指定函数所使用的数…

错误删除linux分区致Win7引导失败的修复方法

以前在Winxp和linux双启动时&#xff0c;若完全 删除了linux分区&#xff0c;重启进不了Winxp时&#xff0c;只需要用Winxp的光盘引导系统到命令行或故障恢复控制台&#xff0c;输入fixmbr和fixboot即可解决。现在是win7系统&#xff0c;命令行已经被修改。昨天我就遇到这问题&…

如何通过自学找到一份开发的工作?

01学习过程比较仔细的学习了《cprimer》&#xff0c;并对每个习题都自己写代码实现了一遍&#xff0c;包括稍微复杂一点的例子。认真读完了《effective c》&#xff0c;《effective stl》。比较仔细的学完了《数据结构与算法分析》&#xff0c;并把其中的每种数据结构和算法都用…

BeetleX使用bootstrap5开发SPA应用

在早期版本BeetleX.WebFamily只提供了vuejselement的集成&#xff0c;由于element只适合PC管理应用开发相对于移动应用适配则没这么方便。在新版本组件集成了bootstrap5可以更好地适配移动Web应用&#xff1b;同时也集成了Fontawesome和bootstrapIcons,这样在开发过程中使用字体…

计算机基础知识菜鸟教程,机器学习基础知识整理归纳

关于机器学习的一些基本概念的整理1.前言1.机器学习是一门致力于研究如何通过计算的手段&#xff0c;利用经验来改善系统自身的性能的学科。1997年Mitchell给出一个更形式化的定义&#xff0c;假设用P来评估计算机程序在某任务类T上的性能&#xff0c;若一个程序通过利用经验E在…

python echo函数_python如何调用php文件中的函数详解

前言python调用php代码实现思路&#xff1a;php文件可通过在terminal中使用php命令行进行调用&#xff0c;因此可使用python开启子进程执行命令行代码。函数所需的参数可通过命令行传递。测试环境1、操作系统&#xff1a;macos10.13.22、php版本&#xff1a;PHP 7.1.7(mac自带)…

今天换了ubuntu10.04

今天换成了ubuntu10.04&#xff0c;开机很快&#xff0c;17秒到登录界面&#xff0c;在我这台dell vostro 1500上很快很快了。快是ubuntu10.04给我的第一印象。至于界面什么的&#xff0c;我真觉得没什么区别&#xff0c;反正我也不开特效&#xff0c;管它呢&#xff01;反正我…

Jupyter 常见可视化框架的选择

文末有福利&#xff01;对于以Python作为技术栈的数据科学工作者&#xff0c;Jupyter是不得不提的数据报告工具。可能对于R社区而言&#xff0c;鼎鼎大名的ggplot2是常见的可视化框架&#xff0c;而大家对于Python&#xff0c;以及Jupyter为核心的交互式报告的可个视化方案就并…

AOP(面向切面编程)大概了解一下

前言上一篇在聊MemoryCache的时候&#xff0c;用到了Autofac提供的拦截器进行面向切面编程&#xff0c;很明显能体会到其优势&#xff0c;既然涉及到了&#xff0c;那就趁热打铁&#xff0c;一起来探探面向切面编程。正文1. 概述在软件业&#xff0c;AOP为Aspect Oriented Prog…

es6添加删除class_es6中class类的使用

在es5中我们是使用构造函数实例化出来一个对象&#xff0c;那么构造函数与普通的函数有什么区别呢&#xff1f;其实没有区别&#xff0c;无非就是函数名称用首字母大写来加以区分&#xff0c;这个不用对说对es5有了解的朋友都应该知道。但是es5的这种方式给人的感觉还是不够严谨…

搞定WordPress的日志自动截断

WordPress默认首页显示日志全文&#xff0c;这个很让人受不了&#xff0c;在IC之前就想搞定这个&#xff0c;可是一直没有时间&#xff0c;现在比赛过后&#xff0c;作为休闲&#xff0c;总算是搞定&#xff0c;很多人说用more标签&#xff0c;最后还是用了某位仁兄做的很好的插…

如何选择 WebClient HttpWebRequest HttpClient ?

当我们在用 .NET 调用 RestAPI 时通常有三种选择&#xff0c;分别为&#xff1a;WebClient, HttpWebRequest&#xff0c;HttpClient&#xff0c;这篇文章我们将会讨论如何使用这三种方式去调用 RestAPI&#xff0c;我还会提供相应的代码案例来帮助你更好的理解这三者的概念和使…

gre考试能用计算机么,新GRE考试必须知道的九大考场问题

参加GRE考试须知GRE考场问题1、计算机化GRE考试考场的环境怎样?计算机化考场按照全球统一标准建设&#xff0c;温度适宜&#xff0c;光线充足&#xff0c;隔音效果良好。各个考位由屏风分隔&#xff0c;相对独立。GRE考试须知GRE考场问题2、参加计算机化GRE考试需要携带什么文…

python property setter_Python:动态属性 property setter 以及 __getattr__ 属性

1. property引言&#xff1a;-- 假设有这样一个需求&#xff0c;我们需要创建一个 User 类&#xff0c;并初始化 birthday 参数&#xff0c;之后根据 birthday 计算得到年龄&#xff1b;-- 我们设计下面的代码实现该需求&#xff1a;>>> from datetime import date, d…