理解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,一经查实,立即删除!

相关文章

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

最近有多位网友在“电脑百事网”微信公众号中留言问到“显示器多大尺寸合适”、“玩游戏用多大显示器好”类似的相关问题。今天小编就来抽空解答一下&#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;以内容为中心而不是…

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

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

BeetleX使用bootstrap5开发SPA应用

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

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…

机器学习三部曲

随着科技的发展&#xff0c;计算机对人类的生产活动和社会活动产生了极为重要的影响&#xff0c;同时以强大的生命力飞速发展着。目前计算机正广泛用于社会各个领域&#xff0c;并朝着微型化、网络化、智能化和巨型化的方向前进。说到智能化&#xff0c;大家最先想到的应该就是…

AntDesign Pro + .NET Core 实现基于JWT的登录认证

很多同学说AgileConfig的UI实在是太丑了。我想想也是的&#xff0c;本来这个项目是我自己使用的&#xff0c;一开始甚至连UI都没有&#xff0c;全靠手动在数据库里修改数据。后来加上了UI也是使用了老掉牙的bootstrap3做为基础样式。前台框架也是使用了angularjs&#xff0c;同…

武汉大学计算机学院2019考研复试,2019年武汉大学硕士研究生复试及录取名单汇总...

原标题&#xff1a;2019年武汉大学硕士研究生复试及录取名单汇总考生可以通过录取名单了解到很多重要的信息&#xff0c;例如复试比例&#xff0c;进复试最低分&#xff0c;复试录取成绩&#xff0c;录取总评成绩等重要信息。以下是我们整理收集到的各学院复试录取名单汇总&…

ugui unity 取消选择_UGUI中几种不规则按钮的实现方式

前言UGUI中的按钮默认是矩形的&#xff0c;若要实现非矩形按钮该怎么做呢&#xff1f;比如这样的按钮&#xff1a;本文将介绍两种实现方式供大家选择。使用alphaHitTestMinimumThresholdImage类的alphaHitTestMinimumThreshold是一个浮点值&#xff0c;Raycast检测时只有图片中…

你的专业 VS 你妈口中你的专业

亲妈认证★英语语言文学我妈&#xff1a;她就是一个学英语的~我同学&#xff1a;你学英语的啊&#xff1f;那你看美剧不用看字幕的吧&#xff1f;你听英文歌都听得懂的吧&#xff1f;这个怎么翻译啊&#xff1f;这上面写的什么&#xff1f;你不是专八吗&#xff1f;哈喽~ 在吗&…

反射 + 抽象工厂模式切换不同的实现方法

概述工厂模式&#xff08;Abstract Factory&#xff09;定义 &#xff1a;提供一个创建一系列相关或相互依赖对象的接口&#xff0c;而无需指定它们具体的类。抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是围绕一个超级工厂创建其他工厂。该超级工厂又称为其…

3 年工作经验程序员应有的技能

前言因为和同事有约定再加上LZ自己也喜欢做完一件事之后进行总结&#xff0c;因此有了这篇文章。这篇文章大部分内容都是面向整个程序员群体的&#xff0c;当然因为LZ本身是做Java开发的&#xff0c;因此有一部分内容也是专门面向咱们Java程序员的。第二阶段&#xff1a;五年五…

应急响应中的溯源方法

在发现有入侵者后&#xff0c;快速由守转攻&#xff0c;进行精准地溯源反制&#xff0c;收集攻击路径和攻击者身份信息&#xff0c;勾勒出完整的攻击者画像。 对内溯源与对内溯源 对内溯源&#xff1a;确认攻击者的行为 &#xff0c;分析日志 数据包等&#xff1b; 对外溯源&…

POP3口令扫描案例

通过本案例可以学到&#xff1a; (1)了解POP3有关知识(2)利用Hscan工具软件来破解POP3账号和口令现在很多邮箱服务器都支持POP3功能&#xff0c;通过POP3来收取信件&#xff0c;收取信件时仅仅需要提供用户名和密码。目前有很多工具可以扫描POP3邮件账号和口令&#xff0c;本案…

中connect怎么用_烘焙中的各种酒,到底该怎么用?

​在烘焙食谱中&#xff0c;经常会出现“酒”这样材料。烘焙中的酒&#xff0c;仿佛是个神秘的存在&#xff0c;品种也繁多得让人一脸懵逼&#xff0c;朗姆酒是干嘛用的&#xff1f;怎么还有分白朗姆和金朗姆&#xff1f;和利口酒有什么区别&#xff1f;利口酒和力娇酒是同一个…

QQ旋风爆缓冲区溢出漏洞

据金山毒霸安全实验室5月7日透露&#xff0c;金山毒霸安全实验室研究人员进行例行漏洞检查过程中&#xff0c;发现QQ旋风存在一鲜为人知的缓冲区溢出0day漏洞&#xff0c;***者可以利用该漏洞制造恶意URL&#xff0c;使用IE6&#xff0c;7内核的各种浏览器均会受此漏洞的影响。…

w10计算机无法打印,win10升级后惠普无法打印怎么解决_win10升级后惠普打印不了的处理办法...

使用电脑工作或学习时&#xff0c;我们经常会需要用到打印机。可是最近有一些网友却反映说&#xff0c;自己的win10电脑在升级后出现了惠普无法打印的情况&#xff0c;这是怎么一回事呢&#xff1f;我们又该怎么解决呢&#xff1f;用户不是很清楚&#xff0c;所以对此今天本文为…