理解C#泛型运作原理
2021-05-16 08:29
标签:const some 迭代 pen slot 反射 guid datetime 运行 ?我们都知道泛型在C#的重要性,泛型是OOP语言中三大特征的多态的最重要的体现,几乎泛型撑起了整个.NET框架,在讲泛型之前,我们可以抛出一个问题,我们现在需要一个可扩容的数组类,且满足所有类型,不管是值类型还是引用类型,那么在没有用泛型方法实现,如何实现? ?我们肯定会想到用 然后我们来验证下: 输出: ?貌似输出结果是正确的,能够动态进行扩容,同样的支持值类型 大致执行模型如下: 引用类型: 值类型: ?那么有没有一种方法能够避免上面遇到的三种问题呢?在借鉴了cpp的模板和java的泛型经验,在C#2.0的时候推出了更适合.NET体系下的泛型 那么测试代码则改写为如下: 输出: 我们通过截取部分 ?原来定义的时候就是用了个 ArrayExpandable: ArrayExpandable ?我们从IL也能看的出来, ?其实有了解 ?我还加入了.NETCore3.1和.NET5的对比,且以.NETCore3.1的 用更直观的柱形图来呈现: ?我们能看到在这里 类、结构、接口、方法、和委托可以声明一个或者多个类型参数,我们直接看代码: 控制台测试代码: 输出如下: 我们通过例子可以看到的是: 父类和实现类或接口的接口都可以是实例化类型,直接看代码: 我们可以通过例子看出: 我们定义如下一个类和一个方法,且不会报错: 因为 我们先上代码: 再改动下Foo类: ?我们可以看到,通过 ?说到 我们先看下代码: ?这时候你可能会有点奇怪,为啥那段代码会编译失败,明明 因此我们可以得出以下结论: 而支持迭代的泛型接口 我们将上面代码改下: 我们再改动下IBar,发现出现另外一处编译失败 因此我们可以得出以下结论: 同样的泛型委托 我们先来看看以下代码: 然后通过ildasm查看其IL,开启视图-》显示标记值,查看Main方法: 打开元数据表将上面所涉及到的元数据定义表和类型规格表列出: metainfo: ?这时候我们就可以看出,元数据为泛型类 ?非常妙的是,当你实例化两个一样的类型参数 由于泛型也有元数据的存在,因此可以对其做反射: 输出: ?泛型编程作为.NET体系中一个很重要的编程思想,主要有以下亮点: 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#泛型运作原理 标签:const some 迭代 pen slot 反射 guid datetime 运行 原文地址:https://www.cnblogs.com/ryzen/p/14480171.html前言
一.泛型之前的故事
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 = _size)
throw new ArgumentOutOfRangeException(nameof(index));
return _items[index];
}
set
{
if (index = _size)
throw new ArgumentOutOfRangeException(nameof(index));
_items[index] = value;
}
}
public int Capacity
{
get => _items.Length;
set
{
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
var arrayStr = new ArrayExpandable();
var strs = new string[] { "ryzen", "reed", "wymen" };
for (int i = 0; i
ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
Struct
的int32
和引用类型的字符串,但是其实这里会发现一些问题,那就是
string
进行了类型转换的验证int32
进行了装箱和拆箱操作,同时进行类型转换类型的检验二.用泛型实现
public class ArrayExpandable
var arrayStr = new ArrayExpandable
ryzen
reed
wymen
gavin
Now arrayStr Capacity:4
0
1
2
3
4
Now array Capacity:8
ArrayExpandable
的IL查看其本质是个啥://声明类
.class public auto ansi beforefieldinit MetaTest.ArrayExpandable`1
T
作为占位符,起一个模板的作用,我们对其实例化类型参数的时候,补足那个占位符,我们可以在编译期就知道了其类型,且不用在运行时进行类型检测,而我们也可以对比ArrayExpandable
和ArrayExpandable
在类型为值类型中的IL,查看是否进行拆箱和装箱操作,以下为IL截取部分: 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为拆箱操作
IL_007f: newobj instance void class GenericSample.ArrayExpandable`1
ArrayExpandable
的T
作为一个类型参数,在编译后在IL已经确定了其类型,因此当然也就不存在装拆箱的情况,在编译期的时候IDE能够检测类型,因此也就不用在运行时进行类型检测,但并不代表不能通过运行时检测类型(可通过is和as),还能通过反射体现出泛型的灵活性,后面会讲到ArrayList
和List
的朋友就知道,ArrayExpandable
和ArrayExpandable
其实现大致就是和它们一样,只是简化了很多的版本,我们这里可以通过 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 array = new ArrayExpandable
EnumAraayList_valueType
方法为基准,性能测试结果如下:List
的性能在引用类型和值类型中都是所以当中是最好的,不管是执行时间、GC次数,分配的内存空间大小,都是最优的,同时.NET5在几乎所有的方法中性能都是优于.NETCore3.1,这里还提一句,我实现的ArrayExpandable
和ArrayExpandable
性能都差于ArrayList
和List
,我还没实现IList
和各种方法,只能说句dotnet基金会牛逼三.泛型的多态性
多态的声明
interface IFoo
static void Main(string[] args)
{
Test();
Console.ReadLine();
}
static void Test()
{
var list = new List
构造函数:parameter1 type:List`1,parameter2 type:String
索引:索引0的值:1
Filed:ryzen
Method:Int32:classT?.ToString()
Method
多态的继承
interface IFooBase
Foo
的基类FooBase
定义的和Foo
有着共享的类型参数ClassT
,因此可以在继承的时候不实例化类型Foo
和IFoo
接口没定义相同的类型参数,因此可以在继承的时候实例化出接口的类型参数StringBuild
出来IFoo
和IFooBase
没定义相同的类型参数,因此可以在继承的时候实例化出接口的类型参数string
出来多态的递归
class D
T
能在实例化的时候确定其类型,因此也支持这种循环套用自己的类和方法的定义四.泛型的约束
where的约束
class FooBase{ }
class Foo : FooBase
{
}
class someClass
class Foo : FooBase
{
public Foo(string str)
{
}
}
static void TestConstraint()
{
var someClass = new someClass
where
语句,可以对类型参数进行约束,而且一个类型参数支持多个约束条件(例如K),使其在实例化类型参数的时候,必须按照约束的条件对应实例符合条件的类型,而where
条件约束的作用就是起在编译期约束类型参数的作用out和in的约束
out
和in
之前,我们可以说下协变和逆变,在C#中,只有泛型接口和泛型委托可以支持协变和逆变协变
class FooBase{ }
class Foo : FooBase
{
}
interface IBar
Foo
类可以隐式转为FooBase
,但作为泛型接口类型参数实例化却并不能呢?使用out
约束泛型接口IBar
的T,那段代码就会编译正常,但是会引出另外一段编译报错:interface IBar
Foo
继承FooBase
,本身子类Foo
包含着父类允许访问的成员,因此能隐式转换父类,这是类型安全的转换,因此叫协变out
标识其类型参数支持协变后,约束其方法的返回值和属性的Get(本质也是个返回值的方法)才能引用所声明的类型参数,也就是作为输出值,用out
很明显的突出了这一意思IEnumerable
也是这么定义的: public interface IEnumerable
逆变
class FooBase{ }
class Foo : FooBase
{
}
interface IBar
interface IBar
FooBase
是Foo
的父类,并不包含子类的自由的成员,转为为子类Foo
是类型不安全的,因此在运行时强式转换的报错了,但编译期是不能够确认的in
标识其类型参数支持逆变后,in
约束其接口成员不能将其作为返回值(输出值),我们会发现协变和逆变正是一对反义词Action
就是个逆变的例子:public delegate void Action
五.泛型的反射
static void Main(string[] args)
{
var lsInt = new ArrayExpandable
void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 52 (0x34)
.maxstack 2
.locals /*11000001*/ init (class MetaTest.ArrayExpandable`1/*02000003*/
-----------定义部分
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 //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
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
定义一份定义表,生成两份规格,也就是当你实例化类型参数为int
和string
的时候,分别生成了两份规格代码,同时还发现以下的现象:var lsInt = new ArrayExpandable
string
,是共享一份类型规格的,也就是同享一份本地代码,因此上面的代码在线程堆栈和托管堆的大致是这样的:Console.WriteLine($"-----------{nameof(lsInt)}---------------");
Console.WriteLine($"{nameof(lsInt)} is generic?:{lsInt.GetType().IsGenericType}");
Console.WriteLine($"Generic type:{lsInt.GetType().GetGenericArguments()[0].Name}");
Console.WriteLine("---------Menthods:");
foreach (var method in lsInt.GetType().GetMethods())
{
Console.WriteLine(method.Name);
}
Console.WriteLine("---------Properties:");
foreach (var property in lsInt.GetType().GetProperties())
{
Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}");
}
Console.WriteLine($"\n-----------{nameof(lsStr)}---------------");
Console.WriteLine($"{nameof(lsStr)} is generic?:{lsStr.GetType().IsGenericType}");
Console.WriteLine($"Generic type:{lsStr.GetType().GetGenericArguments()[0].Name}");
Console.WriteLine("---------Menthods:");
foreach (var method in lsStr.GetType().GetMethods())
{
Console.WriteLine(method.Name);
}
Console.WriteLine("---------Properties:");
foreach (var property in lsStr.GetType().GetProperties())
{
Console.WriteLine($"{property.PropertyType.ToString()}:{property.Name}");
}
-----------lsInt---------------
lsInt is generic?:True
Generic type:Int32
---------Menthods:
get_Item
set_Item
get_Capacity
set_Capacity
get_Count
Add
GetType
ToString
Equals
GetHashCode
---------Properties:
System.Int32:Item
System.Int32:Capacity
System.Int32:Count
-----------lsStr---------------
lsStr is generic?:True
Generic type:String
---------Menthods:
get_Item
set_Item
get_Capacity
set_Capacity
get_Count
Add
GetType
ToString
Equals
GetHashCode
---------Properties:
System.String:Item
System.Int32:Capacity
System.Int32:Count
六.总结
is
和as
进行类型检验参考