Nuss 阅读(4) 评论(0)

前几天微信好友突然发给我一个截图打开一看是一段代码,题目如下,问装箱和拆箱的次数以及最终的输出

namespace DotNetCore
{
    class Program
    {
        static void Main(string[] args)
        {
            var p = new Point(1,1);
            Console.WriteLine(p);
            p.Change(2,2);
            Console.WriteLine(p);
            object o = p;
            ((Point)o).Change(3,3);
            Console.WriteLine(o);    
        }       
    }

    struct Point
    {
        private int a,b;
        public Point(int a,int b)
        {
            this.a = a;
           this.b = b;
        }

        public void Change(int a,int b)
        {
            this.a = a;
           this.b = b;
        }

        public override string ToString()
        {
            return $"({a},{b})";
        }
    }
}

看了看很简单啊,不就是考值类型和引用类型吗?so easy啊,甩手一个答案:
装箱1次,拆箱1次,
返回结果:
(1,1)
(2,2)
(2,2)

事实证明这个答案只能算答对了一半,题目套路深啊!!!

图说执行流程

最直观,最简单,最有效的当然是直接上IL了,IL是啥?IL就是代码编译后的中间语言(Intermediate Language)。上面Main方法代码的IL详见下图。

相信上图已经很详细的说明的代码的一个执行流程,即使对IL并不熟悉的人应该也是可以看懂整个代码的执行的流程的。这个题目的坑点我认为主要有两个:

  • ((Point)o).Change(3,3); 可能有部分人会认为输出是(3,3),通过上图IL_0044和IL_0045两行,我们可以很清晰的知道 ((Point)o)拆箱后的变量我们后续并没有使用。输出的还是o变量本身的值;
  • Console.WriteLine(); 语句,该方法并没有struct类型的重载,所以调用的是使用Object类型参数的方法,所以每次输出都要进行装箱操作,通过IL我们也可以很清晰的看到。

所以,本题正确答案如下:
装箱3次,拆箱1次,
返回结果:
(1,1)
(2,2)
(2,2)

一些思考

值类型的装箱和拆箱是会带来性能损耗的,因为数据需要不断的在线程栈(stack)和托管堆(manager Heap)上来回移动,有时候我们不经意使用的一些方法或者类型,就包含了隐式装箱,例如:
Hashtable ht = new Hashtable();
ht.Add(a,b);

当a和b分别是值类型或引用类型,执行效率是不一样的,如果是大批量数据处理问题可想而知。
技术来不得半点马虎。So,我觉得无论是自己写程序,还是使用第三方的一些API,都应该更深入了解一些内部的实现,拿来即用虽然省事,但不知哪一天可能就入坑了。如有问题欢迎讨论。