以下问题由同事提问引发,或者在实际项目中遇到,这里总结一下。
问: C#中装箱和拆箱是指什么
答:
要明白装箱和拆箱,必须先明白其他一些东西。
C#纯面向对象,所有的东西都是对象;所有的数据类型和类等都是由基类System.Object继承而来的(只支持单一继承)(想想那张巨大的类关系图)。
根据在内存中如何被分配,C#中的类型一共分为两类,一类是值类型(Value Type),一类是引用类型(Reference Type)。值类型隐式地继承自System.ValueType类型,System.ValueType和所有的引用类型都继承自 System.Object基类。
值类型是在栈中分配内存,超出作用范围系统自动释放内存,跟C/C++中栈上一样。值类型主要包括整型(Sbyte、Byte、Char、Short、Ushort、Int、Uint、Long、Ulong), 浮点型(Float、Double), decimal, bool(前述值类型有固定大小) ,用户定义的结构(struct),枚举(enum)。它们都对应了类库中的类,如int是System. Int32的别名或者说是简写形式。
引用类型在托管堆中分配内存,初始化时默认为null,由垃圾回收机制回收。包括类、接口、委托、数组以及内置引用类型object与string。
由于C#中所有的值类型和引用类型都继承自System.Object,所以两者可以通过显式(或隐式)操作相互转换。栈上值类型的数据转换成托管堆上引用类型的数据,这一过程叫装箱(boxing);相反,托管堆上的引用类型的数据转换成栈上值类型的数据,这一过程叫拆箱(unboxing),被装过箱的对象才能被拆箱,且拆箱之后值类型必须跟装箱时的值类型匹配。
装箱(值类型到引用类型,栈到托管堆):用于在垃圾回收堆(即托管堆)中存储值类型。装箱是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。拆箱(引用类型到值类型,托管堆到栈):从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。
问:为什么会有装箱和拆箱,什么时候用到?
答:
性能,栈上的性能明显优于托管堆上的性能。装箱时,堆上申请空间耗资源,拷贝耗资源;拆箱,主要是拷贝耗资源。装箱比拆箱更耗资源,所以要尽量避免装箱。
栈上的值类型和装箱到托管堆上后的数据究竟有什么区别呢?托管堆上会多出一个类似于C++虚函数表指针的东西(方法表指针),通过它可以调用到基类的方法。
导致装箱的情况有:值类型转换成Object类型, 值类型转换成该值类型已实现的接口类型时, 当值类型调用Object类中未重写的方法时。代码示例如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CSharp
{
//define a value type A
struct A : ICloneable
{
public Int32 x;
public override String ToString()
{
return String.Format("{0}", x); //boxing, value type "Int32" to referrence type "object"
}
public object Clone()
{
return MemberwiseClone();
}
}
class Program
{
static void Main(string[] args)
{
int i = 10;
object ob = i;//boxing,这时会在托管堆上创建一块内存空间,并把a的值复制到该空间,并且创建System.Int类型的元数据信息相应的方法表等信息
int i2 = (int)ob;// unboxing, 这句分两步,(int)ob是拆箱操作,返回一个ob的地址引用.(只有这步叫拆箱); 当赋值给i2时,会把地址引用的值复制给i2.(这步不是拆箱操作,但拆箱后往往跟随赋值,所以有些地方把这步包含在拆箱中.)
A a = new A(); // stack for value type "struct A"
a.x = 100;
Object o = a; //boxing implicitly, to referrence type object
Console.WriteLine(a.ToString()); //no-boxing, as <a> overrides the function ToString()
Console.WriteLine(a.GetType()); //boxing implicitly, as it invokes GetType() of class object
ICloneable c = a; //boxing implicitly, to referrence type Interface <ICloneable>
}
}
}
问: C#程序的编译和执行过程
答:
C#的源程序并不是被编译成二进制可执行形式,而是一种中间语言(IL),类似于JAVA字节码,称为中间代码(Microsoft Intermediate Language),然后通过 .NET Framework 的虚拟机(被称之为通用语言执行层(Common Language Runtime, CLR))执行。
Java的字节代码(Byte Code)和MSIL都是中间的汇编形式的语言,它们在运行时或其它的时候被编译成机器代码。在程序执行时,.Net Framework将中间代码翻译成为二进制机器码,从而使它得到正确的运行。最终的二进制代码被存储在一个缓冲区中。所以一旦程序使用了相同的代码,那么将会调用缓冲区中的版本。如果一个.Net程序第二次被运行,不需要中间码到二进制的翻译,速度很快。
当然C/C++进程中也可以调用C#实现的东西,windows debugger也可以对C#的程序进行调试,基本都是通过.NET CLR Core实现的;C#中也可以调用C/C++的函数。
问:C#编译时,如何查看中间代码MSIL
答:
用Ildasm.exe 打开C#编译生成的.exe或.DLL文件查看中间代码。从中可以看到装箱等信息,对于分析性能等问题也是很有帮助的。
C#的编译器生成程序集包括中间语言模块和元数据,不能生成想过去C++编写的那样的本机代码,而中间语言模块的执行需要.NET Framework的支持,所以在目标机器上需要安装.NET Framework的。
csc.exe可执行文件通常位于系统目录下的 Microsoft.NET\Framework\Version 文件夹中。用于将.cs源代码文件编译生成MSIL中间代码。
Ildasm.exe 可以分析任何 .NET Framework .exe 或 .dll 程序集,并以可读的格式显示信息。Ildasm.exe 不只是显示 Microsoft 中间语言 (MSIL) 代码,它还显示命名空间和类型,包括其接口。如下图所示:
问: string使用的注意事项
答:
String是引用类型,其对象在托管堆上分配内存,由系统根据初始化字符串的大小自动分配内存。 使用不当,可能带来性能问题。当频繁地进行字符串操作(替换,添加,删除)时,优先使用System.Text.StringBuilder 类。如果下面的代码放到一个循环里面,可能带来严重的性能问题:
name += "xxx";
因为执行这行代码时,.NET CLR会在堆中创建一个新的字符串实例,给他分配足够的内存,然后存储合并的所有文本。然后更新存储在 name 变量中的内存地址,使变量正确的指向新的字符串对象。旧的字符串对象被销毁了引用,并等待系统回收。如果在循环中执行,每次都会申请新的空间,很耗资源。在真实的项目中,我们就曾犯过这个错误。
附录: 参考链接
C# 装箱和拆箱[整理]
http://www.cnblogs.com/huashanlin/archive/2007/05/16/749359.html
C# 指南之装箱与拆箱
http://www.cnblogs.com/hunts/archive/2007/01/19/boxing_unboxing.html
<