C#泛型

Wesley13
• 阅读 999

现在的netcore 3.1和最新的.netframework8早已经没有当初那个被人诟病的ArrayList了,但很巧这玩意不得不说,因为它决定了C#团队痛改前非,抛弃过往重新上路,上一段ArrayList案例代码。

    public class ArrayList
    {
        private object[] items; private int index = 0; public ArrayList() { items = new object[10]; } public void Add(object item) { items[index++] = item; } } 

上面这段代码,为了保证在Add中可以塞入各种类型 eg: int,double,class, 就想到了一个绝招用祖宗类object接收,这就引入了两大问题,装箱拆箱和类型安全。

1. 装箱拆箱

这个很好理解,因为你使用了祖宗类,所以当你 Add 的时候塞入的是值类型的话,自然就有装箱操作,比如下面代码:

            ArrayList arrayList = new ArrayList();
            arrayList.Add(3);

<1> 占用更大的空间

这个问题我准备用windbg看一下,相信大家知道一个int类型占用4个字节,那装箱到堆上是几个字节呢,好奇吧😄。

原始代码和IL代码如下:

        public static void Main(string[] args) { var num = 10; var obj = (object)num; Console.Read(); } IL_0000: nop IL_0001: ldc.i4.s 10 IL_0003: stloc.0 IL_0004: ldloc.0 IL_0005: box [mscorlib]System.Int32 IL_000a: stloc.1 IL_000b: call int32 [mscorlib]System.Console::Read() IL_0010: pop IL_0011: ret 

可以清楚的看到IL_0005 中有一个box指令,装箱没有问题,然后抓一下dump文件。

~0s -> !clrstack -l -> !do 0x0000018300002d48

0:000> ~0s
ntdll!ZwReadFile+0x14:
00007ff9`fc7baa64 c3 ret 0:000> !clrstack -l OS Thread Id: 0xfc (0) Child SP IP Call Site 0000002c397fedf0 00007ff985c808f3 ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 28] LOCALS: 0x0000002c397fee2c = 0x000000000000000a 0x0000002c397fee20 = 0x0000018300002d48 0000002c397ff038 00007ff9e51b6c93 [GCFrame: 0000002c397ff038] 0:000> !do 0x0000018300002d48 Name: System.Int32 MethodTable: 00007ff9e33285a0 EEClass: 00007ff9e34958a8 Size: 24(0x18) bytes File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll Fields: MT Field Offset Type VT Attr Value Name 00007ff9e33285a0 40005a0 8 System.Int32 1 instance 10 m_value 

倒数第5行 Size: 24(0x18) bytes, 可以清楚的看到是24字节。 为什么是24个字节,8(同步块指针) + 8(方法表指针) + 4(对象大小)=20,但因为是x64位,内存是按8对齐,也就是要按8的倍数计算,所以占用是 8+8+8 =24 字节,原来只有4字节的大小因为装箱已被爆到24字节,如果是10000个值类型的装箱那空间占用是不是挺可怕的?

<2> 栈到堆的装箱搬运到运输到售后到无害化处理都需要付出重大的人力和机器成本

2. 类型不安全

很简单,因为是祖宗类型object,所以无法避免程序员使用乱七八糟的类型,当然这可能是无意的,但是编译器确无法规避,代码如下:

            ArrayList arrayList = new ArrayList();
            arrayList.Add(3);
            arrayList.Add(new Action<int>((num) => { }));
            arrayList.Add(new object()); 

面对这两大尴尬的问题,C#团队决定重新设计一个类型,实现一定终身,这就有了泛型。

二:泛型的出现

1. 救世主

首先可以明确的说,泛型就是为了解决这两个问题而生的,你可以在底层提供的List<T>中使用List<int>,List<double>。。。等等你看得上的类型,而这种技术的底层实现原理才是本篇关注的重点。

        public static void Main(string[] args) { List<double> list1 = new List<double>(); List<string> list3 = new List<string>(); ... } 

三:泛型原理探究

这个问题的探索其实就是 List<T> -> List<int>在何处实现了 T -> int 的替换,反观java,它的泛型实现其实在底层还是用object来替换的,C#肯定不是这么做的,不然也没这篇文章啦,要知道在哪个阶段被替换了,你起码要知道C#代码编译的几个阶段,为了理解方便,我画一张图吧。

C#泛型

流程大家也看到了,要么在MSIL中被替换,要么在JIT编译中被替换。。。

        public static void Main(string[] args) { List<double> list1 = new List<double>(); List<int> list2 = new List<int>(); List<string> list3 = new List<string>(); List<int[]> list4 = new List<int[]>(); Console.ReadLine(); } 

1. 在第一阶段探究

因为第一阶段是MSIL代码,所以用ILSpy看一下中间代码即可。

        IL_0000: nop
        IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<float64>::.ctor() IL_0006: stloc.0 IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor() IL_000c: stloc.1 IL_000d: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor() IL_0012: stloc.2 IL_0013: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32[]>::.ctor() IL_0018: stloc.3 IL_0019: call string [mscorlib]System.Console::ReadLine() IL_001e: pop IL_001f: ret .class public auto ansi serializable beforefieldinit System.Collections.Generic.List`1<T> extends System.Object implements class System.Collections.Generic.IList`1<!T>, class System.Collections.Generic.ICollection`1<!T>, class System.Collections.Generic.IEnumerable`1<!T>, System.Collections.IEnumerable, System.Collections.IList, System.Collections.ICollection, class System.Collections.Generic.IReadOnlyList`1<!T>, class System.Collections.Generic.IReadOnlyCollection`1<!T> 

从上面的IL代码中可以看到,最终的类定义还是 System.Collections.Generic.List1\<T>,说明在中间代码阶段还是没有实现 T -> int 的替换。

2. 在第二阶段探究

想看到JIT编译后的代码,这个说难也不难,其实每个对象头上都有一个方法表指针,而这个指针指向的就是方法表,方法表中有该类型的所有最终生成方法,如果不好理解,我就画个图。

C#泛型

!dumpheap -stat 寻找托管堆上的四个List对象。

0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ff9e3314320        1           32 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle 00007ff9e339b4b8 1 40 System.Collections.Generic.List`1[[System.Double, mscorlib]] 00007ff9e333a068 1 40 System.Collections.Generic.List`1[[System.Int32, mscorlib]] 00007ff9e3330d58 1 40 System.Collections.Generic.List`1[[System.String, mscorlib]] 00007ff9e3314a58 1 40 System.IO.Stream+NullStream 00007ff9e3314510 1 40 Microsoft.Win32.Win32Native+InputRecord 00007ff9e3314218 1 40 System.Text.InternalEncoderBestFitFallback 00007ff985b442c0 1 40 System.Collections.Generic.List`1[[System.Int32[], mscorlib]] 00007ff9e338fd28 1 48 System.Text.DBCSCodePageEncoding+DBCSDecoder 00007ff9e3325ef0 1 48 System.SharedStatics 

可以看到从托管堆中找到了4个list对象,现在我就挑一个最简单的 System.Collections.Generic.List1[[System.Int32, mscorlib]] ,前面的 00007ff9e333a068 就是方法表地址。

!dumpmt -md 00007ff9e333a068

0:000> !dumpmt -md 00007ff9e333a068
EEClass:         00007ff9e349b008
Module:          00007ff9e3301000 Name: System.Collections.Generic.List`1[[System.Int32, mscorlib]] mdToken: 00000000020004af File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll BaseSize: 0x28 ComponentSize: 0x0 Slots in VTable: 77 Number of IFaces in IFaceMap: 8 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 00007ff9e3882450 00007ff9e3308de8 PreJIT System.Object.ToString() 00007ff9e389cc60 00007ff9e34cb9b0 PreJIT System.Object.Equals(System.Object) 00007ff9e3882090 00007ff9e34cb9d8 PreJIT System.Object.GetHashCode() 00007ff9e387f420 00007ff9e34cb9e0 PreJIT System.Object.Finalize() 00007ff9e38a3650 00007ff9e34dc6e8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32) 00007ff9e4202dc0 00007ff9e34dc7f8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Insert(Int32, Int32) 

上面方法表中的方法过多,我做了一下删减,可以清楚的看到,此时Add方法已经接受(Int32)类型的数据了,说明在JIT编译之后,终于实现了 T -> int 的替换,然后再把 List<double> 打出来看一下。

0:000> !dumpmt -md 00007ff9e339b4b8
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e3882450 00007ff9e3308de8 PreJIT System.Object.ToString() 00007ff9e389cc60 00007ff9e34cb9b0 PreJIT System.Object.Equals(System.Object) 00007ff9e3882090 00007ff9e34cb9d8 PreJIT System.Object.GetHashCode() 00007ff9e387f420 00007ff9e34cb9e0 PreJIT System.Object.Finalize() 00007ff9e4428730 00007ff9e34e4170 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double) 00007ff9e3867a00 00007ff9e34e4280 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Insert(Int32, Double) 

上面看的都是值类型,接下来再看一下如果 T 是引用类型会是怎么样呢?

0:000> !dumpmt -md 00007ff9e3330d58
MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e3890060 00007ff9e34eb058 PreJIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon) 0:000> !dumpmt -md 00007ff985b442c0 MethodDesc Table Entry MethodDesc JIT Name 00007ff9e3890060 00007ff9e34eb058 PreJIT System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon) 

可以看到当是List<int[]> 和 List<string> 的时候,JIT使用了 System.__Canon 这么一个类型作为替代,有可能人家是摄影爱好者吧,为什么用__Canon替代引用类型,这是因为它想让能共享代码区域的方法都共享来节省空间和内存吧,不信的话可以看看它们的Entry列都是同一个内存地址:00007ff9e3890060, 打印出来就是这么一段汇编。

0:000> !u 00007ff9e3890060
preJIT generated code
System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)
Begin 00007ff9e3890060, size 4a >>> 00007ff9`e3890060 57 push rdi 00007ff9`e3890061 56 push rsi 00007ff9`e3890062 4883ec28 sub rsp,28h 00007ff9`e3890066 488bf1 mov rsi,rcx 00007ff9`e3890069 488bfa mov rdi,rdx 00007ff9`e389006c 8b4e18 mov ecx,dword ptr [rsi+18h] 00007ff9`e389006f 488b5608 mov rdx,qword ptr [rsi+8] 00007ff9`e3890073 3b4a08 cmp ecx,dword ptr [rdx+8] 00007ff9`e3890076 7422 je mscorlib_ni+0x59009a (00007ff9`e389009a) 00007ff9`e3890078 488b4e08 mov rcx,qword ptr [rsi+8] 00007ff9`e389007c 8b5618 mov edx,dword ptr [rsi+18h] 00007ff9`e389007f 448d4201 lea r8d,[rdx+1] 00007ff9`e3890083 44894618 mov dword ptr [rsi+18h],r8d 00007ff9`e3890087 4c8bc7 mov r8,rdi 00007ff9`e389008a ff152088faff call qword ptr [mscorlib_ni+0x5388b0 (00007ff9`e38388b0)] (JitHelp: CORINFO_HELP_ARRADDR_ST) 00007ff9`e3890090 ff461c inc dword ptr [rsi+1Ch] 00007ff9`e3890093 4883c428 add rsp,28h 00007ff9`e3890097 5e pop rsi 00007ff9`e3890098 5f pop rdi 00007ff9`e3890099 c3 ret 00007ff9`e389009a 8b5618 mov edx,dword ptr [rsi+18h] 00007ff9`e389009d ffc2 inc edx 00007ff9`e389009f 488bce mov rcx,rsi 00007ff9`e38900a2 90 nop 00007ff9`e38900a3 e8c877feff call mscorlib_ni+0x577870 (00007ff9`e3877870) (System.Collections.Generic.List`1[[System.__Canon, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5) 00007ff9`e38900a8 ebce jmp mscorlib_ni+0x590078 (00007ff9`e3890078) 

然后再回过头看List<int> 和 List<double> ,从Entry列中看确实不是一个地址,说明List<int> 和 List<double> 是两个完全不一样的Add方法,看得懂汇编的可以自己看一下哈。。。

MethodDesc Table
           Entry       MethodDesc    JIT Name
00007ff9e38a3650 00007ff9e34dc6e8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
00007ff9e4428730 00007ff9e34e4170 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double) 0:000> !u 00007ff9e38a3650 preJIT generated code System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32) Begin 00007ff9e38a3650, size 50 >>> 00007ff9`e38a3650 57 push rdi 00007ff9`e38a3651 56 push rsi 00007ff9`e38a3652 4883ec28 sub rsp,28h 00007ff9`e38a3656 488bf1 mov rsi,rcx 00007ff9`e38a3659 8bfa mov edi,edx 00007ff9`e38a365b 8b5618 mov edx,dword ptr [rsi+18h] 00007ff9`e38a365e 488b4e08 mov rcx,qword ptr [rsi+8] 00007ff9`e38a3662 3b5108 cmp edx,dword ptr [rcx+8] 00007ff9`e38a3665 7423 je mscorlib_ni+0x5a368a (00007ff9`e38a368a) 00007ff9`e38a3667 488b5608 mov rdx,qword ptr [rsi+8] 00007ff9`e38a366b 8b4e18 mov ecx,dword ptr [rsi+18h] 00007ff9`e38a366e 8d4101 lea eax,[rcx+1] 00007ff9`e38a3671 894618 mov dword ptr [rsi+18h],eax 00007ff9`e38a3674 3b4a08 cmp ecx,dword ptr [rdx+8] 00007ff9`e38a3677 7321 jae mscorlib_ni+0x5a369a (00007ff9`e38a369a) 00007ff9`e38a3679 4863c9 movsxd rcx,ecx 00007ff9`e38a367c 897c8a10 mov dword ptr [rdx+rcx*4+10h],edi 00007ff9`e38a3680 ff461c inc dword ptr [rsi+1Ch] 00007ff9`e38a3683 4883c428 add rsp,28h 00007ff9`e38a3687 5e pop rsi 00007ff9`e38a3688 5f pop rdi 00007ff9`e38a3689 c3 ret 00007ff9`e38a368a 8b5618 mov edx,dword ptr [rsi+18h] 00007ff9`e38a368d ffc2 inc edx 00007ff9`e38a368f 488bce mov rcx,rsi 00007ff9`e38a3692 90 nop 00007ff9`e38a3693 e8a8e60700 call mscorlib_ni+0x621d40 (00007ff9`e3921d40) (System.Collections.Generic.List`1[[System.Int32, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5) 00007ff9`e38a3698 ebcd jmp mscorlib_ni+0x5a3667 (00007ff9`e38a3667) 00007ff9`e38a369a e8bf60f9ff call mscorlib_ni+0x53975e (00007ff9`e383975e) (mscorlib_ni) 00007ff9`e38a369f cc int 3 0:000> !u 00007ff9e4428730 preJIT generated code System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double) Begin 00007ff9e4428730, size 5a >>> 00007ff9`e4428730 56 push rsi 00007ff9`e4428731 4883ec20 sub rsp,20h 00007ff9`e4428735 488bf1 mov rsi,rcx 00007ff9`e4428738 8b5618 mov edx,dword ptr [rsi+18h] 00007ff9`e442873b 488b4e08 mov rcx,qword ptr [rsi+8] 00007ff9`e442873f 3b5108 cmp edx,dword ptr [rcx+8] 00007ff9`e4428742 7424 je mscorlib_ni+0x1128768 (00007ff9`e4428768) 00007ff9`e4428744 488b5608 mov rdx,qword ptr [rsi+8] 00007ff9`e4428748 8b4e18 mov ecx,dword ptr [rsi+18h] 00007ff9`e442874b 8d4101 lea eax,[rcx+1] 00007ff9`e442874e 894618 mov dword ptr [rsi+18h],eax 00007ff9`e4428751 3b4a08 cmp ecx,dword ptr [rdx+8] 00007ff9`e4428754 732e jae mscorlib_ni+0x1128784 (00007ff9`e4428784) 00007ff9`e4428756 4863c9 movsxd rcx,ecx 00007ff9`e4428759 f20f114cca10 movsd mmword ptr [rdx+rcx*8+10h],xmm1 00007ff9`e442875f ff461c inc dword ptr [rsi+1Ch] 00007ff9`e4428762 4883c420 add rsp,20h 00007ff9`e4428766 5e pop rsi 00007ff9`e4428767 c3 ret 00007ff9`e4428768 f20f114c2438 movsd mmword ptr [rsp+38h],xmm1 00007ff9`e442876e 8b5618 mov edx,dword ptr [rsi+18h] 00007ff9`e4428771 ffc2 inc edx 00007ff9`e4428773 488bce mov rcx,rsi 00007ff9`e4428776 90 nop 00007ff9`e4428777 e854fbffff call mscorlib_ni+0x11282d0 (00007ff9`e44282d0) (System.Collections.Generic.List`1[[System.Double, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5) 00007ff9`e442877c f20f104c2438 movsd xmm1,mmword ptr [rsp+38h] 00007ff9`e4428782 ebc0 jmp mscorlib_ni+0x1128744 (00007ff9`e4428744) 00007ff9`e4428784 e8d50f41ff call mscorlib_ni+0x53975e (00007ff9`e383975e) (mscorlib_ni) 00007ff9`e4428789 cc int 3 

可能你有点蒙,我画一张图吧。

C#泛型

四: 总结

泛型T真正的被代替是在 JIT编译时才实现的,四个List<T> 会生成四个具有相应具体类型的类对象,所以就不存在拆箱和装箱的问题,而类型的限定visualstudio编译器工具提前就帮我们约束好啦。

https://www.cnblogs.com/huangxincheng/p/12764925.html

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
2年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这