由c# dynamic是否装箱引发的思考

前言

    前几天在技术群里看到有同学在讨论关于dynamic是否会存在装箱拆箱的问题,我当时第一想法是"会"。至于为啥会有很多人有这种疑问,主要是因为觉得dynamic可能是因为有点特殊,因为它被称为动态类型,可能是因为这里的动态对大家造成的误解,认为这里的动态可以推断出具体的类型,所以可以避免装箱拆箱。但是事实并不是这样,今天就一起就这个问题讨论一下。.

装箱拆箱

首先咱们先来看下何为装箱拆箱,这个可以在微软官方文档中Boxing and Unboxing文档中看到答案,咱们就简单的摘要一下相关的描述

装箱是将值类型转换为类型对象或此值类型实现的任何接口类型的过程。当公共语言运行时 (CLR) 将值类型装箱时,它会将值包装在 System.Object 实例中并将其存储在托管堆上。拆箱从对象中提取值类型。拳击是隐含的;拆箱是明确的。装箱和拆箱的概念是 C# 类型系统统一视图的基础,其中任何类型的值都可以视为对象。

翻译起来会比较抽象,理解起来就是利用装箱和拆箱功能,可通过允许值类型的任何值与Object 类型的值相互转换,将值类型与引用类型链接起来。也就是值类型和引用类型相互转换的一座桥梁,但是问题也很明显那就是实例会存在在堆栈之前相互copy的问题,会存在一定的性能问题,所以这也一直是一个诟病。

虽然说是这样但是也没必要一直扣死角,毕竟很多时候程序还没有纠结到这种程度,因为任何语言存在的各种方法中或者操作中都会有一定这种问题,所以本质不是语言存在各种问题,而是在什么场景如何使用的问题。比如避免出现装箱和拆箱的办法也就是如概念所说的,那就是避免值类型和和引用类型之间相互转换,但是很多时候还是避免不了的,所以也不必纠结。

探究本质

上面讲解了关于装箱拆箱的概念,接下来咱们就来定义一段代码看看效果,为了方便对比咱们直接对比着看一下

dynamic num = 123;
dynamic str = "a string";

想要看清本质还是要反编译一下生成的结果看一下的,这里我们可以借助ILSpydnSpy来看下,首先看一下反编译回来的效果

private static void <Main>$(string[] args)
{
    object num = 123;
    object str = "a string";
    Console.ReadKey();
}

因为我是使用的是.net6的顶级声明方式所以会生成<Main>$方法。不过从反编译的结果就可以看出来dynamic的本质是object,如果还有点怀疑的话可以直接查看生成的IL代码,还是使用ILSpy工具

.method private hidebysig static 
    void '<Main>$' (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2094
    // Header size: 12
    // Code size: 30 (0x1e)
    .maxstack 1
    .entrypoint
    .locals init (
        // 这里可以看出声明的num和str变量都是object类型的
        [0] object num,
        [1] object str
    )

    // object obj = 123;
    IL_0000: ldc.i4.s 123
    // 这里的box说明存在装箱操作
    IL_0002: box [System.Runtime]System.Int32
    IL_0007: stloc.0
    // object obj2 = "a string";
    IL_0008: ldstr "a string"
    IL_000d: stloc.1
    // Console.ReadKey();
    IL_000e: call valuetype [System.Console]System.ConsoleKeyInfo [System.Console]System.Console::ReadKey()
    IL_0013: pop
    // (no C# code)
    IL_0014: nop
    IL_0015: nop
    IL_0016: nop
    IL_0017: nop
    IL_0018: nop
    IL_0019: nop
    IL_001a: nop
    IL_001b: nop
    // }
    IL_001c: nop
    IL_001d: ret
} // end of method Program::'<Main>$'

通过这里可以看出dynamic的本质确实是object,既然是object那就可以证实确实是存在装箱操作。这个其实在微软官方文档Using type dynamic上有说明,大致描述是这样的

dynamic类型是一种静态类型,但类型为dynamic的对象会跳过静态类型检查。大多数情况下,该对象就像具有类型object一样。在编译时,将假定类型化为dynamic的元素支持任何操作。因此,不必考虑对象是从 COM API、从动态语言(例如 IronPython)、从 HTML 文档对象模型 (DOM)、从反射还是从程序中的其他位置获取自己的值。但是,如果代码无效,则在运行时会捕获到错误。

从这里可以看出dynamic表现出来的就是object,只是dynamic会跳过静态类型检查,所以编译的时候不会报错,有错误的话会在运行的时候报错,也就是我们说的是在运行时确定具体操作。这涉及到动态语言运行时,动态语言运行时(DLR)是一种运行时环境,可以将一组动态语言服务添加到公共语言运行时(CLR)。使用DLR可以轻松开发在.NET上运行的动态语言,并为静态类型语言添加动态特征。

匿名类型

总会有人拿dynamicvar进行比较,但是本质上来说,这两者描述的不是一个层面的东西。var叫隐式类型,本质是一种语法糖,也就是说在编译的时候就可以确定类型的具体类型,也就是说var本质是提供了一种更简单的编程体验,不会影响变量本身的行为。这也就解释了为啥同一个var变量多次赋值不能赋不同类型的值,比如以下操作编译器会直接报错

var num = 123;
num = "123"; //报错

如果你是用的集成开发环境的话其实很容易发现,把鼠标放到var类型上就会显示变量对应的真实类型。或者可以直接通过ILSpy看看反编译结果,比如声明了var num = 123编译完成之后就是

private static void <Main>$(string[] args)
{
    int num = 123;
    Console.ReadKey();
}

请注意这里并不是object而是转换成了具体的类型因为123就是int类型的,严谨一点看一下IL代码

.maxstack 1
.entrypoint
//声明的int32
.locals init (
    [0] int32 num
)
// int num = 123;
IL_0000: ldc.i4.s 123
IL_0002: stloc.0

相信这里就可以看出来了dynamicvar确实也不是一个层面的东西。var是隐式类型是语法糖为了简化编程体验用的,dynamic则是动态语言运行时技术,编译时转换成object类型,因为在c#上一切都是object,然后再运行时进行具体的操作。

总结

    本篇文章主要是在技术群里看到有同学在讨论关于dynamic是否会装箱引发的思考,相对来说讲解的比较基础也比较简单。想对一个东西理解的更透彻,就要一步一步的了解它到底是什么,这样的话就可以更好的理解和思考。也印证了那句话,你不会用或者用是因为你对它不够了解,当你对它有足够理解的时候,操作起来也就会游刃有余。