从CLR角度来看值类型与引用类型

时间:2021-11-05 04:49:12

前言

  本文中大部分示例代码来自于《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
v1.X = ; // 修改值类型的x值为5 Console.WriteLine(r1.X); // 结果显示“5”
Console.WriteLine(v1.X); // 结果显示“5”
}

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

  从CLR角度来看值类型与引用类型

Sample Two:

  

        public void SmapleTwo()
{
SomeClass r1 = new SomeClass(); // 引用类型,在托管堆上被分配。
SomeStruct v1 = new SomeStruct(); // 值类型,在线程栈上被分配。 r1.X = ; // 修改引用类型的值为5
v1.X = ; // 修改值类型的x值为5 SomeClass r2 = r1; // 只复制引用(r1指针)
SomeStruct v2 = v1; // 复制v1生成栈上的新对象v2 r1.X = ; // r1.X与r2.X均改变,因为其指向的地址内容相同
v1.X = ; // 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。

从CLR角度来看值类型与引用类型

装箱和拆箱

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

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

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

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

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

从CLR角度来看值类型与引用类型

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

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

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

  用IL来给假设证明

Sample One:

        public int RunSample1()
{
var v = ;
object o = v; //Box
v = ;
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(, );
Console.WriteLine(p); //Box, show 1,1 p.Offset(, ); //Change Point -> 3,3
Console.WriteLine(p); //Box, show 3,3 object o = p; //Box
Console.WriteLine(o); //Show 3,3 ((Point) o).Offset(, ); //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(, ); // Define struct type
object o = p; Console.WriteLine(p.ToString()); // Not Box ((IMyPoint) p).Change(, ); // When turn to interface it should boxed and copy a instance.
Console.WriteLine(p); // Show 1,1 ((IMyPoint) o).Change(, ); // '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

示例代码下载

从CLR角度来看值类型与引用类型的更多相关文章

  1. CLR:基元类型、引用类型和值类型

    最新更新请访问: http://denghejun.github.io   前言 今天重新看了下关于CLR基元类型的东西,觉得还是有必要将其记录下来,毕竟这是理解CLR成功 之路上的重要一步,希望你也 ...

  2. CLR值类型和引用类型

    知识点:引用类型.值类型.装箱.拆箱 CLR支持两种类型:引用类型和值类型.引用类型在堆上分配内存,值类型在线程栈上分配内存.值类型与引用类型如下所示: 值类型对象有两种表示形式:未装箱和已装箱.将一 ...

  3. [No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1

    引言 本文之初的目的是讲述设计模式中的 Prototype(原型)模式,但是如果想较清楚地弄明白这个模式,需要了解对象克隆(Object Clone),Clone其实也就是对象复制.复制又分为了浅度复 ...

  4. C# - 值类型、引用类型&走出误区,容易错误的说法

    1. 值类型与引用类型小总结 1)对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象. 2)引用就像URL,是允许你访问真实信息的一小片数据. 3)对于值类型的表达式,它的值是实际的数据. ...

  5. 图解C#的值类型,引用类型,栈,堆,ref,out

    C# 的类型系统可分为两种类型,一是值类型,一是引用类型,这个每个C#程序员都了解.还有托管堆,栈,ref,out等等概念也是每个C#程序员都会接触到的概念,也是C#程序员面试经常考到的知识,随便搜搜 ...

  6. .NET面试题解析(01)-值类型与引用类型

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 常见面试题目: 1. 值类型和引用类型的区别? 2. 结构和类的区别? 3. delegate是引用类型还 ...

  7. c#数据类型之值类型和引用类型

    C#数据类型分隔为值类型和引用类型.而所用数据类型都继承自Object. 1. 值类型继承自System.ValueType,引用类型继承自System.Object.ValueType也直接继承自O ...

  8. Windows Phone 开发起步之旅之二 C#中的值类型和引用类型

    今天和大家分享下本人也说不清楚的一个C#基础知识,我说不清楚,所以我才想把它总结一下,以帮助我自己理解这个知识上的盲点,顺便也和同我一样不是很清楚的人一起学习下.  一说起来C#中的数据类型有哪些,大 ...

  9. C++ : 从栈和堆来理解C#中的值类型和引用类型

    C++中并没有值类型和引用类型之说,标准变量或者自定义对象的存取默认是没有区别的.但如果深入地来看,就要了解C++中,管理数据的两大内存区域:栈和堆. 栈(stack)是类似于一个先进后出的抽屉.它的 ...

随机推荐

  1. Oracle Tuning 基础概述01 - Oracle 常见等待事件

    对Oracle数据库整体性能的优化,首先要关注的是在有性能问题时数据库排名前几位等待事件是哪些.Oracle等待事件众多,随着版本的升级,数量还在不断增加,可以通过v$event_name查到当前数据 ...

  2. HTML5探秘:用requestAnimationFrame优化Web动画

    本文转载自: HTML5探秘:用requestAnimationFrame优化Web动画

  3. 如何比较两个SQL数据库的字段差别。

    程序好几个版本了,数据也弄出好好几版本,这下好了,原程序要升级,当然数据库也要升,可是里面已经有了大量的数据了,这时候怎么办.写了个存储过程来解决,一目了然. 因为2005及以上的数据库已经没有表sy ...

  4. lnmp下启动mysql报错 The server quit without updating PID file

    启动时候错误代码:Starting MySQL[FAIL.] The server quit without updating PID file (/var/run/mysqld/mysqld.pid ...

  5. 基于对话框的MFC应用程序基本结构

    新建一个基于对话框的MFC应用程序,假设命名为 Test:则该应用程序在刚创建的时候,有4个非常重要的文件和3个类: 4个非常重要的文件 1.Test.h 2.Test.cpp (应用程序类头文件) ...

  6. CentOS 7.x下安装部署MySQL 8.0实施手册

    MySQL 8 正式版 8.0.11 已发布,官方表示 MySQL 8 要比 MySQL 5.7 快 2 倍,还带来了大量的改进和更快的性能! 一.  Mysql8.0版本相比之前版本的一些特性 1) ...

  7. Mac下安装mongdb

    使用 homebrew 安装 MongoDB :brew install mongodb 这时 MongoDB 将被安装在 /usr/local/Cellar/mongodb/4.0.3_1 (我的 ...

  8. android HTTP镜像

    mirrors.neusoft.edu.cn 80

  9. 20171104xlVBA各人各科进退

    Sub 各班个人各科进步幅度() Dim dRank As Object Set dRank = CreateObject("Scripting.Dictionary") Dim ...

  10. Cmd下修改文件访问控制权限

    保证自己的磁盘分区格式是NTFS.FAT32是不行的. 一.Cacls.exe命令的使用 这是一个在Windows 2000/XP/Server 2003操作系统下都可以使用的命令,作用是显示或者修改 ...