• 深入了解C#泛型


    现代程序员写代码没有人敢说自己没用过泛型,这个泛型模板T可以被任何你想要的类型替代,确实很魔法很神奇,很多人也习以为常了,但就是这么有趣的泛型T底层到底是怎么帮你实现的,不知道有多少人清楚底层玩法。

    一:没有泛型前

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

    1. public class ArrayList
    2. {
    3. private object[] items;
    4. private int index = 0;
    5. public ArrayList()
    6. {
    7. items = new object[10];
    8. }
    9. public void Add(object item)
    10. {
    11. items[index++] = item;
    12. }
    13. }

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

    1. 装箱拆箱

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

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

    <1> 占用更大的空间

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

    原始代码和IL代码如下:

    1. public static void Main(string[] args)
    2. {
    3. var num = 10;
    4. var obj = (object)num;
    5. Console.Read();
    6. }
    7. IL_0000: nop
    8. IL_0001: ldc.i4.s 10
    9. IL_0003: stloc.0
    10. IL_0004: ldloc.0
    11. IL_0005: box [mscorlib]System.Int32
    12. IL_000a: stloc.1
    13. IL_000b: call int32 [mscorlib]System.Console::Read()
    14. IL_0010: pop
    15. IL_0011: ret

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

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

    1. 0:000> ~0s
    2. ntdll!ZwReadFile+0x14:
    3. 00007ff9`fc7baa64 c3 ret
    4. 0:000> !clrstack -l
    5. OS Thread Id: 0xfc (0)
    6. Child SP IP Call Site
    7. 0000002c397fedf0 00007ff985c808f3 ConsoleApp2.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp2\Program.cs @ 28]
    8. LOCALS:
    9. 0x0000002c397fee2c = 0x000000000000000a
    10. 0x0000002c397fee20 = 0x0000018300002d48
    11. 0000002c397ff038 00007ff9e51b6c93 [GCFrame: 0000002c397ff038]
    12. 0:000> !do 0x0000018300002d48
    13. Name: System.Int32
    14. MethodTable: 00007ff9e33285a0
    15. EEClass: 00007ff9e34958a8
    16. Size: 24(0x18) bytes
    17. File: C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    18. Fields:
    19. MT Field Offset Type VT Attr Value Name
    20. 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,所以无法避免程序员使用乱七八糟的类型,当然这可能是无意的,但是编译器确无法规避,代码如下:

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

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

    二:泛型的出现

    1. 救世主

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

    1. public static void Main(string[] args)
    2. {
    3. List<double> list1 = new List<double>();
    4. List<string> list3 = new List<string>();
    5. ...
    6. }

    三:泛型原理探究

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

     

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

    1. public static void Main(string[] args)
    2. {
    3. List<double> list1 = new List<double>();
    4. List<int> list2 = new List<int>();
    5. List<string> list3 = new List<string>();
    6. List<int[]> list4 = new List<int[]>();
    7. Console.ReadLine();
    8. }

    1. 在第一阶段探究

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

    1. IL_0000: nop
    2. IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<float64>::.ctor()
    3. IL_0006: stloc.0
    4. IL_0007: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
    5. IL_000c: stloc.1
    6. IL_000d: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
    7. IL_0012: stloc.2
    8. IL_0013: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32[]>::.ctor()
    9. IL_0018: stloc.3
    10. IL_0019: call string [mscorlib]System.Console::ReadLine()
    11. IL_001e: pop
    12. IL_001f: ret
    13. .class public auto ansi serializable beforefieldinit System.Collections.Generic.List`1<T>
    14. extends System.Object
    15. implements class System.Collections.Generic.IList`1<!T>,
    16. class System.Collections.Generic.ICollection`1<!T>,
    17. class System.Collections.Generic.IEnumerable`1<!T>,
    18. System.Collections.IEnumerable,
    19. System.Collections.IList,
    20. System.Collections.ICollection,
    21. class System.Collections.Generic.IReadOnlyList`1<!T>,
    22. class System.Collections.Generic.IReadOnlyCollection`1<!T>

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

    2. 在第二阶段探究

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

     

     

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

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

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

    !dumpmt -md 00007ff9e333a068

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

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

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

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

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

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

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

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

    1. MethodDesc Table
    2. Entry MethodDesc JIT Name
    3. 00007ff9e38a3650 00007ff9e34dc6e8 PreJIT System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
    4. 00007ff9e4428730 00007ff9e34e4170 PreJIT System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double)
    5. 0:000> !u 00007ff9e38a3650
    6. preJIT generated code
    7. System.Collections.Generic.List`1[[System.Int32, mscorlib]].Add(Int32)
    8. Begin 00007ff9e38a3650, size 50
    9. >>> 00007ff9`e38a3650 57 push rdi
    10. 00007ff9`e38a3651 56 push rsi
    11. 00007ff9`e38a3652 4883ec28 sub rsp,28h
    12. 00007ff9`e38a3656 488bf1 mov rsi,rcx
    13. 00007ff9`e38a3659 8bfa mov edi,edx
    14. 00007ff9`e38a365b 8b5618 mov edx,dword ptr [rsi+18h]
    15. 00007ff9`e38a365e 488b4e08 mov rcx,qword ptr [rsi+8]
    16. 00007ff9`e38a3662 3b5108 cmp edx,dword ptr [rcx+8]
    17. 00007ff9`e38a3665 7423 je mscorlib_ni+0x5a368a (00007ff9`e38a368a)
    18. 00007ff9`e38a3667 488b5608 mov rdx,qword ptr [rsi+8]
    19. 00007ff9`e38a366b 8b4e18 mov ecx,dword ptr [rsi+18h]
    20. 00007ff9`e38a366e 8d4101 lea eax,[rcx+1]
    21. 00007ff9`e38a3671 894618 mov dword ptr [rsi+18h],eax
    22. 00007ff9`e38a3674 3b4a08 cmp ecx,dword ptr [rdx+8]
    23. 00007ff9`e38a3677 7321 jae mscorlib_ni+0x5a369a (00007ff9`e38a369a)
    24. 00007ff9`e38a3679 4863c9 movsxd rcx,ecx
    25. 00007ff9`e38a367c 897c8a10 mov dword ptr [rdx+rcx*4+10h],edi
    26. 00007ff9`e38a3680 ff461c inc dword ptr [rsi+1Ch]
    27. 00007ff9`e38a3683 4883c428 add rsp,28h
    28. 00007ff9`e38a3687 5e pop rsi
    29. 00007ff9`e38a3688 5f pop rdi
    30. 00007ff9`e38a3689 c3 ret
    31. 00007ff9`e38a368a 8b5618 mov edx,dword ptr [rsi+18h]
    32. 00007ff9`e38a368d ffc2 inc edx
    33. 00007ff9`e38a368f 488bce mov rcx,rsi
    34. 00007ff9`e38a3692 90 nop
    35. 00007ff9`e38a3693 e8a8e60700 call mscorlib_ni+0x621d40 (00007ff9`e3921d40) (System.Collections.Generic.List`1[[System.Int32, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5)
    36. 00007ff9`e38a3698 ebcd jmp mscorlib_ni+0x5a3667 (00007ff9`e38a3667)
    37. 00007ff9`e38a369a e8bf60f9ff call mscorlib_ni+0x53975e (00007ff9`e383975e) (mscorlib_ni)
    38. 00007ff9`e38a369f cc int 3
    39. 0:000> !u 00007ff9e4428730
    40. preJIT generated code
    41. System.Collections.Generic.List`1[[System.Double, mscorlib]].Add(Double)
    42. Begin 00007ff9e4428730, size 5a
    43. >>> 00007ff9`e4428730 56 push rsi
    44. 00007ff9`e4428731 4883ec20 sub rsp,20h
    45. 00007ff9`e4428735 488bf1 mov rsi,rcx
    46. 00007ff9`e4428738 8b5618 mov edx,dword ptr [rsi+18h]
    47. 00007ff9`e442873b 488b4e08 mov rcx,qword ptr [rsi+8]
    48. 00007ff9`e442873f 3b5108 cmp edx,dword ptr [rcx+8]
    49. 00007ff9`e4428742 7424 je mscorlib_ni+0x1128768 (00007ff9`e4428768)
    50. 00007ff9`e4428744 488b5608 mov rdx,qword ptr [rsi+8]
    51. 00007ff9`e4428748 8b4e18 mov ecx,dword ptr [rsi+18h]
    52. 00007ff9`e442874b 8d4101 lea eax,[rcx+1]
    53. 00007ff9`e442874e 894618 mov dword ptr [rsi+18h],eax
    54. 00007ff9`e4428751 3b4a08 cmp ecx,dword ptr [rdx+8]
    55. 00007ff9`e4428754 732e jae mscorlib_ni+0x1128784 (00007ff9`e4428784)
    56. 00007ff9`e4428756 4863c9 movsxd rcx,ecx
    57. 00007ff9`e4428759 f20f114cca10 movsd mmword ptr [rdx+rcx*8+10h],xmm1
    58. 00007ff9`e442875f ff461c inc dword ptr [rsi+1Ch]
    59. 00007ff9`e4428762 4883c420 add rsp,20h
    60. 00007ff9`e4428766 5e pop rsi
    61. 00007ff9`e4428767 c3 ret
    62. 00007ff9`e4428768 f20f114c2438 movsd mmword ptr [rsp+38h],xmm1
    63. 00007ff9`e442876e 8b5618 mov edx,dword ptr [rsi+18h]
    64. 00007ff9`e4428771 ffc2 inc edx
    65. 00007ff9`e4428773 488bce mov rcx,rsi
    66. 00007ff9`e4428776 90 nop
    67. 00007ff9`e4428777 e854fbffff call mscorlib_ni+0x11282d0 (00007ff9`e44282d0) (System.Collections.Generic.List`1[[System.Double, mscorlib]].EnsureCapacity(Int32), mdToken: 00000000060039e5)
    68. 00007ff9`e442877c f20f104c2438 movsd xmm1,mmword ptr [rsp+38h]
    69. 00007ff9`e4428782 ebc0 jmp mscorlib_ni+0x1128744 (00007ff9`e4428744)
    70. 00007ff9`e4428784 e8d50f41ff call mscorlib_ni+0x53975e (00007ff9`e383975e) (mscorlib_ni)
    71. 00007ff9`e4428789 cc int 3

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

     

     

    四:总结

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

    文章来源于【黑哥聊dotnet】和【dotner一线码农】

  • 相关阅读:
    Docker 部署SpringBoot项目,使用外部配置文件启动项目
    vue3的基本使用(超详细)
    心理测评测试h5公众号字节微信小程序app开源版开发
    轮询与中断
    免杀技术实际演示
    H265码流RTP封装方式详解
    使用Python操作SQLite
    1010 Radix
    微服务到底该怎么样部署呢?
    KVM Cloud云平台
  • 原文地址:https://blog.csdn.net/weixin_67336587/article/details/125508151