目前在看CLR via C#,把总结的记下来,索性就把他写成一个系列吧。
1.【.Net基础一】 类型、对象、线程栈、托管堆运行时的相互关系
2.【.Net基础二】浅谈引用类型、值类型和装箱、拆箱
引用类型和值类型
引用类型:分配在托管堆中。引用类型直接继承自System.Object。引用类型可以被继承。
常用的引用类型有:数组、 类、 接口、 委托、 字符串
值类型:可分配在线程栈和托管堆中。所有值类型都分为结构或枚举。值类型直接继承自System.ValueType。值类型不可以被继承。
例如:System.Int32结构、System.Boolean结构、System.Decimal结构、System.Nullable结构、System.DayOfWeek枚举、System.IO.FileAttributes枚举等都是值类型。
枚举属于值类型,枚举继承自System.Enum,但System.Enum由于特殊性,并不属于值类型,System.Enum属于引用类型。System.Enum继承自System.ValueType。System.ValueType和System.Enum一样,都是属于引用类型,只是CLR对继承自System.ValueType和System.Enum做了特殊处理,内存分配使他具有值类型的特性。编译器也做了特殊处理,例如不能直接继承自这些类。
结构可能我们使用的不多,这里我们可以把它当作一个特殊的“类”。详细的可以参照园子里C#之结构
还有要说明的就是int,double等这些关键字是基元类型,编译器负责将基元类型翻译成对应的FCl(Framework Class Library)中的类型。
来看个例子
class SomeReference { public Int32 x;}//应用类型
struct SomeValue { public Int32 x;}//值类型
static void Main(string[] args)
{
/**第一部分**/
SomeReference re1 = new SomeReference();//在堆上分配
SomeValue va1 = new SomeValue();//在栈上分配
re1.x = 5;
va1.x = 5;
Console.WriteLine(re1.x);//输出5
Console.WriteLine(va1.x);//输出5
/**第二部分**/
SomeReference re2 = re1;//只复制引用(指针)
SomeValue va2 = va1;//在栈上分配并复制成员
re1.x = 8;//re1和re2都会更改
va1.x = 9;//va1会更改,va2不会
Console.WriteLine(re1.x);//输出8
Console.WriteLine(re2.x);//输出8
Console.WriteLine(va1.x);//输出9
Console.WriteLine(va2.x);//输出5
}
第一部分执行完(上一节讲到变量是在序幕代码中进行初始化到栈中):
全部执行完:
装箱和拆箱
这里将一个苹果比喻成值类型,见到苹果能知道他就是吃的苹果,苹果有多种品种(结构和枚举)。
把一个纸箱比喻成引用类型,纸箱可以装各种各样的东西(各种类)。
装箱:将值类型转换为一个引用类型
把苹果装进纸箱(在内存中是复制),就是装箱。装箱运输的话,苹果要仔细打包,很小心。
装箱步骤:
- 在托管堆上分配内存,内存大小为值类型所有字段的大小和类型对象指针加上同步块索引的大小。
- 将值类型的所有字段复制到刚分配的内存中。
- 返回刚分配的内存地址
拆箱:将引用类型转换成值类型
拆箱可能就直接用小刀划开胶带,然后看见苹果(这里已经完成了拆箱),接着取出苹果(在内存中是复制)。
拆箱不是直接把装箱过程倒过来,拆箱的代价要比装箱低的多。拆箱其实就是一个获取指针的过程。不要求在内存中复制任何字节。
拆箱步骤:
- 获取引用类型所有字段对应的地址,这里拆箱已经完成了。
- 拆箱操作完成后,会发生一次字段的复制。
来看个例子
static void Main(string[] args)
{
Int32 v = 5;//值类型
Object o = v;//将值类型转换成应用类型,o引用一个复制的v(已装箱);
v = 123; //将未装箱的值修改成123
Console.WriteLine(v + "," + (Int32)o);//显示123,5
//这里发生了几次装箱?可能大家会看成2次。我刚开始也以为是。
Console.ReadLine();
}
这里容易发生错误的地方就是Console.WriteLine()方法。
正确的装箱次数是3次。
1.Object o = v; 将v转换成o。这是一次。
2.(Int32)o 这涉及到一次拆箱,将o转成Int32,然后再装箱成String?(真的是String吗?那第三次装箱又在哪?)
3.让我们通过ildasm.exe打开程序集,查看IL代码(书中解释的比较详细,我只说关键的地方)。
通过box关键字我们就能看到发生了三次装箱,一次拆箱。
多的一次装箱就发生在Contact方法上。这个Contact方法就是为什么多了一次装箱的关键,三个Object参数,所以对o进行拆箱完装箱其实是Int32转成String
多次装箱拆箱会影响程序的性能和内存消耗,这里不明显,如果在循环中就会产生严重的性能问题。
这里我们可以这样调用
Console.WriteLine(v.ToString() + "," + o);//这里同样显示123,5
v.ToString()方法会返回一个String,String对象已经是引用类型,所以直接传递给Contact方法,不需要任何装箱操作。
这里调用的是String.ValueType(引用类型)的ToString()方法,所以不会发生装箱操作。
o已经指向一个Object引用类型,所以直接传递引用地址就可以。同样不需要进行装箱。
基础很重要,只有在切实理解这些概念之后,才能保证自己长期成功。只有深刻理解之后,才能更快、更轻松的构建高效率应用程序。