zoukankan      html  css  js  c++  java
  • 从CLR角度来看值类型与引用类型

    前言

      本文中大部分示例代码来自于《CLR via C# Edition3》,并在此之上加以总结和简化,文中只是重点介绍几个比较有共性的问题,对一些细节不会做过深入的讲解。

    前几天一直忙着翻译有关内存中堆和栈的问题博文《C#堆vs栈》,正是在写作本文的过程中对有些地方还是产生了很多的Why,所以就先翻译并学习了一些C/C++方面的知识,这样有助于解决CLR之外的一些困惑,希望多大家有所帮助。

      对知识的理解上难免有偏差或不正确,如有疑问以及错误,还请大家回复~~~

    值类型和引用类型的不同

      C#中的类型划分为值类型(Value Type)和引用类型(Reference Type)。

    Sample One:

            public void SampleOne()
            {
                SomeClass r1 = new SomeClass(); // 引用类型,在托管堆上被分配。
                SomeStruct v1 = new SomeStruct(); // 值类型,在线程栈上被分配。
    
                r1.X = 5; // 修改引用类型的值为5
                v1.X = 5; // 修改值类型的x值为5
    
                Console.WriteLine(r1.X); // 结果显示“5”
                Console.WriteLine(v1.X); // 结果显示“5”
            }

      SampleOne中仅仅是将值类型、引用类型的内容简单地更改了,并没有什么特别之处,对应的下图是程序在线程栈(Thread Stack)和托管堆(Managed Heap)上的示意图,唯一要说明的是下图中的“r1”本身代表了“对托管堆中一个对象SomeClass的引用(指针==值类型)”,如果理解起来有难度,请参考文章《C#堆vs栈》 。

      

    Sample Two:

      

            public void SmapleTwo()
            {
                SomeClass r1 = new SomeClass(); // 引用类型,在托管堆上被分配。
                SomeStruct v1 = new SomeStruct(); // 值类型,在线程栈上被分配。
    
                r1.X = 5; // 修改引用类型的值为5
                v1.X = 5; // 修改值类型的x值为5
    
                SomeClass r2 = r1; // 只复制引用(r1指针)
                SomeStruct v2 = v1; // 复制v1生成栈上的新对象v2
    
                r1.X = 8; // r1.X与r2.X均改变,因为其指向的地址内容相同
                v1.X = 9; // v1.X改变,而v2.X不改变
    
                Console.WriteLine(r1.X); // 结果显示“8”
                Console.WriteLine(r2.X); // 结果显示“8”
                Console.WriteLine(v1.X); // 结果显示“9”
                Console.WriteLine(v2.X); // 结果显示“5”
            }

      SampleTwo中想说明的结果是:值类型是拷贝行为,新对象在栈上,新旧对象之间没有影响;引用类型拷贝的是“指向对象的指针”,指针指向的地址内容还是同一个,所以改变r1.X将影响r2.X。

    装箱和拆箱

      上面一节讲述了C#中值类型和引用类型在行为上是有区别的,值类型的“Box”操作而引发的一系列效率上的讨论。

        为什么会有装箱和拆箱操作

      Framework中很多函数的原型被设计成参数为Object,这样就导致了很多值类型需要转换成Object引用类型,从而导致了装箱以及稍后的拆箱操作(当然也有很多原型实现了值类型的参数重载或泛型方法,从而避免装箱/拆箱操作)。

      举一个例子:System.Collection.ArrayList的Add方法原型为public virtual int Add(object value);

      首先将线程栈中p逐字段的复制到托管堆中,并且产生了类型对象指针和同步索引快两个对象,然后将类型对象指针p’返回给Add方法,如下图:

      而新产生的托管堆中的对象完全不依赖于线程栈中原有的对象p,并且两者的生命周期也没有任何关联。

      最后,当我们使用var p =(Point)a[0]的时候,将object转换成Point类型,进行拆箱。进行拆箱的过程与装箱相反:由托管堆中的引用类型复制到线程栈中作为值类型,再使用。

    显然,这里的Box和UnBox操作都会对程序的性能产生不利的影响,我们要避免此类问题的发生。

      用IL来给假设证明

             Sample One:

            public int RunSample1()
            {
                var v = 5;
                object o = v; //Box
                v = 123;
                Console.WriteLine(v + ", " + (Int32) o); //Fisrt 'v' boxed, and 'o' boxed.
                return v;
    
                #region IL Generate Code
                //.method public hidebysig instance void  RunSample1() cil managed
                //{
                //  // 代码大小       47 (0x2f)
                //  .maxstack  3
                //  .locals init ([0] int32 v,
                //           [1] object o)
                //  IL_0000:  nop
                //  IL_0001:  ldc.i4.5
                //  IL_0002:  stloc.0
                //  IL_0003:  ldloc.0
                //  IL_0004:  box        [mscorlib]System.Int32
                //  IL_0009:  stloc.1
                //  IL_000a:  ldc.i4.s   123
                //  IL_000c:  stloc.0
                //  IL_000d:  ldloc.0
                //  IL_000e:  box        [mscorlib]System.Int32
                //  IL_0013:  ldstr      ", "
                //  IL_0018:  ldloc.1
                //  IL_0019:  unbox.any  [mscorlib]System.Int32
                //  IL_001e:  box        [mscorlib]System.Int32
                //  IL_0023:  call       string [mscorlib]System.String::Concat(object,
                //                                                              object,
                //                                                              object)
                //  IL_0028:  call       void [mscorlib]System.Console::WriteLine(string)
                //  IL_002d:  nop
                //  IL_002e:  ret
                //} // end of method BoxAndUnBox::Run
                #endregion
            }

      从SampleOne中我们得到如下结论:

    1. 值类型v赋值给引用类型o的时候发生了装箱操作,这就是我们所说的当值类型向引用类型转换时产生装箱操作(IL_0004),这是明显的。
    2. System.String.Concat方法在本段代码中是使用了需要三个Object的重载,所以v发生了装箱操作(IL_0023)。
    3. 执行(Int32)o则强制执行了拆箱方法(IL_0019),而最终Concat重载还是需要Object类型,所以会再次对o进行装箱操作(IL_001e)

      Sample Two:

            public Point RunSample2()
            {
                var p = new Point(1, 1);
                Console.WriteLine(p); //Box, show 1,1
    
                p.Offset(2, 2); //Change Point -> 3,3
                Console.WriteLine(p); //Box, show 3,3
    
                object o = p; //Box
                Console.WriteLine(o); //Show 3,3
    
                ((Point) o).Offset(3, 3); //UnBox and Change Point -> 6,6
                Console.WriteLine(o); //Show 3,3
    
                return (Point) o; // UnBox and Copy a instance for return
              
                #region IL Generate Code
                //.method public hidebysig instance valuetype [System.Drawing]System.Drawing.Point 
                //        RunSample2() cil managed
                //{
                //  // 代码大小       94 (0x5e)
                //  .maxstack  3
                //  .locals init ([0] valuetype [System.Drawing]System.Drawing.Point p,
                //           [1] object o,
                //           [2] valuetype [System.Drawing]System.Drawing.Point CS$1$0000,
                //           [3] valuetype [System.Drawing]System.Drawing.Point CS$0$0001)
                //  IL_0000:  nop
                //  IL_0001:  ldloca.s   p
                //  IL_0003:  ldc.i4.1
                //  IL_0004:  ldc.i4.1
                //  IL_0005:  call       instance void [System.Drawing]System.Drawing.Point::.ctor(int32,
                //                                                                                 int32)
                //  IL_000a:  nop
                //  IL_000b:  ldloc.0
                //  IL_000c:  box        [System.Drawing]System.Drawing.Point
                //  IL_0011:  call       void [mscorlib]System.Console::WriteLine(object)
                //  IL_0016:  nop
                //  IL_0017:  ldloca.s   p
                //  IL_0019:  ldc.i4.2
                //  IL_001a:  ldc.i4.2
                //  IL_001b:  call       instance void [System.Drawing]System.Drawing.Point::Offset(int32,
                //                                                                                  int32)
                //  IL_0020:  nop
                //  IL_0021:  ldloc.0
                //  IL_0022:  box        [System.Drawing]System.Drawing.Point
                //  IL_0027:  call       void [mscorlib]System.Console::WriteLine(object)
                //  IL_002c:  nop
                //  IL_002d:  ldloc.0
                //  IL_002e:  box        [System.Drawing]System.Drawing.Point
                //  IL_0033:  stloc.1
                //  IL_0034:  ldloc.1
                //  IL_0035:  call       void [mscorlib]System.Console::WriteLine(object)
                //  IL_003a:  nop
                //  IL_003b:  ldloc.1
                //  IL_003c:  unbox.any  [System.Drawing]System.Drawing.Point
                //  IL_0041:  stloc.3
                //  IL_0042:  ldloca.s   CS$0$0001
                //  IL_0044:  ldc.i4.3
                //  IL_0045:  ldc.i4.3
                //  IL_0046:  call       instance void [System.Drawing]System.Drawing.Point::Offset(int32,
                //                                                                                  int32)
                //  IL_004b:  nop
                //  IL_004c:  ldloc.1
                //  IL_004d:  call       void [mscorlib]System.Console::WriteLine(object)
                //  IL_0052:  nop
                //  IL_0053:  ldloc.1
                //  IL_0054:  unbox.any  [System.Drawing]System.Drawing.Point
                //  IL_0059:  stloc.2
                //  IL_005a:  br.s       IL_005c
                //  IL_005c:  ldloc.2
                //  IL_005d:  ret
                //} // end of method BoxAndUnBox::RunSample2
                #endregion
            }

      结论如下:

    1. 因为Point是值类型,所以进行值拷贝,新对象与旧对象没有任何关系。
    2. Offset方法的原型是Offset(int,int)所以要对引用类型o进行拆箱操作。
    3. 返回值是复制出来的,值类型复制本身,引用类型返回复制后的指针。

      SampleThree:

            public bool RunSample3()
            {
                var p = new MyPoint(1, 1); // Define struct type
                object o = p;
    
                Console.WriteLine(p.ToString()); // Not Box
    
                ((IMyPoint) p).Change(2, 2); // When turn to interface it should boxed and copy a instance.
                Console.WriteLine(p); // Show 1,1 
    
                ((IMyPoint) o).Change(2, 2); // 'o' is a reference type, it not boxed.
                Console.WriteLine(o); // Show 2,2
    
                return ((MyPoint) o).x.Equals(p.x);
    
                #region IL Generate Code
                //.method public hidebysig instance bool  RunSample3() cil managed
                //{
                //  // 代码大小       115 (0x73)
                //  .maxstack  3
                //  .locals init ([0] valuetype Demo_CLRVIACSHARP.MyPoint p,
                //           [1] object o,
                //           [2] bool CS$1$0000,
                //           [3] int32 CS$0$0001)
                //  IL_0000:  nop
                //  IL_0001:  ldloca.s   p
                //  IL_0003:  ldc.i4.1
                //  IL_0004:  ldc.i4.1
                //  IL_0005:  call       instance void Demo_CLRVIACSHARP.MyPoint::.ctor(int32,
                //                                                                      int32)
                //  IL_000a:  nop
                //  IL_000b:  ldloc.0
                //  IL_000c:  box        Demo_CLRVIACSHARP.MyPoint
                //  IL_0011:  stloc.1
                //  IL_0012:  ldloca.s   p
                //  IL_0014:  constrained. Demo_CLRVIACSHARP.MyPoint
                //  IL_001a:  callvirt   instance string [mscorlib]System.Object::ToString()
                //  IL_001f:  call       void [mscorlib]System.Console::WriteLine(string)
                //  IL_0024:  nop
                //  IL_0025:  ldloc.0
                //  IL_0026:  box        Demo_CLRVIACSHARP.MyPoint
                //  IL_002b:  ldc.i4.2
                //  IL_002c:  ldc.i4.2
                //  IL_002d:  callvirt   instance void Demo_CLRVIACSHARP.IMyPoint::Change(int32,
                //                                                                        int32)
                //  IL_0032:  nop
                //  IL_0033:  ldloc.0
                //  IL_0034:  box        Demo_CLRVIACSHARP.MyPoint
                //  IL_0039:  call       void [mscorlib]System.Console::WriteLine(object)
                //  IL_003e:  nop
                //  IL_003f:  ldloc.1
                //  IL_0040:  castclass  Demo_CLRVIACSHARP.IMyPoint
                //  IL_0045:  ldc.i4.2
                //  IL_0046:  ldc.i4.2
                //  IL_0047:  callvirt   instance void Demo_CLRVIACSHARP.IMyPoint::Change(int32,
                //                                                                        int32)
                //  IL_004c:  nop
                //  IL_004d:  ldloc.1
                //  IL_004e:  call       void [mscorlib]System.Console::WriteLine(object)
                //  IL_0053:  nop
                //  IL_0054:  ldloc.1
                //  IL_0055:  unbox.any  Demo_CLRVIACSHARP.MyPoint
                //  IL_005a:  ldfld      int32 Demo_CLRVIACSHARP.MyPoint::x
                //  IL_005f:  stloc.3
                //  IL_0060:  ldloca.s   CS$0$0001
                //  IL_0062:  ldloca.s   p
                //  IL_0064:  ldfld      int32 Demo_CLRVIACSHARP.MyPoint::x
                //  IL_0069:  call       instance bool [mscorlib]System.Int32::Equals(int32)
                //  IL_006e:  stloc.2
                //  IL_006f:  br.s       IL_0071
                //  IL_0071:  ldloc.2
                //  IL_0072:  ret
                //} // end of method BoxAndUnBox::RunSample3
                #endregion
            }

      结论如下:

    1. 转换成接口后会导致装箱。
    2. 值类型的ToString方法不发生装箱,效率更高。

    总结

    1. 只有值类型会发生装箱和拆箱操作。
    2. 当值类型作为参数时,要看方法的原型是否需求的是Object,如果是则装箱,否则不装箱。
    3. 系统的值类型实现了ToString的重载,所以不装箱,效率高。
    4. 值类型转换成接口后发生装箱操作。

    欢迎各位看官拍砖~~~

    Update:

      2015.04.16 13:23 看了评论后我崩溃了~~~这里也有我的原因,因为写了之前的《C#堆vs栈》系列,所以在本文中,没有过多的解释过程,而是直接以结论和总结代替之,本文最终想表达的观点是:要从对象的存储位置来分析值类型和引用类型的行为,便于我们掌握值类型和引用类型。哪些说我将值类型在栈上,引用类型在堆上的观点,请你们好好读一读文章,不要望文生义。

      2015.04.16 15:41 最终我觉得是我范了以偏概全的错误,将堆上被封装为引用类型(Box操作)的对象表现为引用行为,就得出“存储位置不同,其表现行为也不同,最终更好的理解值类型与引用类型”的结论,而忽略了在堆上直接被引用类型所包含的值类型的行为。

      2015.04.16 16:13 最后以Eric Lippert的解释来做最后的总结吧

               英文:http://blogs.msdn.com/b/ericlippert/archive/2010/09/30/the-truth-about-value-types.aspx

                 中文:http://www.cnblogs.com/kirinboy/archive/2012/06/15/value-and-reference-in-csharp-2.html

    示例代码下载

  • 相关阅读:
    Vue 异步组件按需加载
    Net core 连接 Minio 出现 Access Denied 错误
    vue 脚手架打包 生成时间戳版本号
    vue tab 切换动画
    想尽办法不出错的语言太可怕了
    .NET Core:处理全局异常
    C#获取当前路径的方法
    [C#]解决生成的缩略图模糊的问题
    C# 正则表达式 —— 中文/英文空格(全角/半角空格)处理
    用C# 7.0的switch...case模式匹配取代一堆if语句
  • 原文地址:https://www.cnblogs.com/cuiyansong/p/4431242.html
Copyright © 2011-2022 走看看