带着问题读CLR via C#(三)基元类型,引用类型和值类型(上)
Q1: 什么是基元类型?C#中有哪些基元类型?
A1: 编译器能直接支持的数据类型称为基元类型,基元类型直接映射到FCL中存在的类型,比如C#中int映射到System.Int32类型。
C#中的基元类型:
我们可以定义这样定义一个字符串:
String str = "abc";
也可以这样定义一个字符串:
string str = "abc";
它们生成的IL是完全相同的,string可以被C#编译器直接映射到FCL中的System.String类型,换言之,C#编译器自动假定所有的源代码中都添加了这个命令:
using string = System.String;
类似的int和Int32, double和Double也都是一样的。
Q2: 什么是值类型,什么是引用类型,它们的区别是什么?
A3: 派生自System.ValueType的类型称为值类型,值类型可以实现一个或多个接口,但不能作为其他类型的基类。值类型的内存空间分配在线程栈中,在代表值类型的一个实例的变量中,并不包含指针,而是包含了实例本身的字段,值类型的实例不受垃圾回收器的控制。引用类型的内存空间分配在托管堆中,它的实例由垃圾回收器进行回收,在线程栈中存储的实例变量只包含一个指向托管堆中特定位置的一个指针。
Q3: 分析以下代码。
1 namespace Test 2 { 3 4 // 引用类型 5 class SomeRef 6 { 7 public Int32 x; 8 } 9 10 // 值类型 11 struct SomeVal 12 { 13 public Int32 x; 14 } 15 16 class Program 17 { 18 static void Main(string[] args) 19 { 20 SomeRef r1 = new SomeRef(); // 在托管堆上分配 21 SomeVal v1 = new SomeVal(); // 在线程栈上分配 22 r1.x = 5; // x储存在托管堆中,将其值设为5,线程栈中存储着一个r1变量指向托管堆中的SomeRef实例 23 v1.x = 5; // 在线程栈中将变量值改为5; 24 Console.WriteLine(r1.x); // 打印出5 25 Console.WriteLine(v1.x); // 打印出5 26 27 SomeRef r2 = r1; // 在线程栈中分配一个新的变量r2,同样指向r1指向的托管堆中的实例 28 SomeVal v2 = v1; // 在线程栈中分配一个新的变量v2, 将v1的值复制到v2中 29 r1.x = 8; // 修改存储在托管堆中的x的值为8 30 v1.x = 9; // 修改线程栈中v1.x的值为9 31 32 Console.WriteLine(r1.x); // 打印出8 33 Console.WriteLine(r2.x); // r2指向和r1的同一个实例,故也打印出8 34 Console.WriteLine(v1.x); // 打印出9 35 Console.WriteLine(v2.x); // v2.x在线程栈中的值并没有改变,故打印出5 36 } 37 } 38 }
A3:
Q4: 何时使用struct, 何时使用class?
A4: 同时满足以下三个条件:1)类型中没有成员会会修改类型的实例字段;2)类型不需要从其它任何类型继承;3)类型不会派生出其他任何类型;并满足以下两个条件中的一个:1)类型的实例小于16字节;2)类型的实例大于16字节,但不作为方法的实参传递,也不作为方法的返回值。此时,可以把这个类型定义为struct, 否则定义为class.
Q5: 定义一个值类型应该注意什么?
A5: 1)由于System.ValueType重写了Equals方法和GetHashCode方法,在定义自己的值类型时,也要重写这两个方法并提供它们的显式实现;2)所有方法都不能是虚方法。
Q6: SomeVal为一个struct,包含一个实例字段x, 以下代码有何不同?
1 SomeVal v = new SomeVal(); 2 SomeVal v;
A6: 两行代码都会在线程栈上分配内存空间,唯一的不同在于C#会认为new操作符对v进行了初始化。如果用第一行代码定义v, 打印v.x时会打印出默认值0, 如果使用第二行代码定义v, 打印v.x时会出错。
Q7: 什么是装箱和拆箱,装箱和拆箱的过程是什么?
A7: 值类型转换为引用类型会发生装箱。装箱的时候,首先会在托管堆中分配内存空间,这包括值类型各个字段需要的内存空间加上托管堆上所有对象都有的两个成员(类型对象指针和同步块索引);然后会将值类型的字段复制到托管堆上;最后返回对象的地址。引用类型转换为值类型会发生拆箱,拆箱操作并不是装箱操作的逆操作,拆箱其实是获取一个指针的过程,该指针指向已装箱实例的未装箱部分,紧接着会发生一次复制操作。
Q8: 分析以下代码,判断三次打印各发生了多少次装箱和拆箱。
1 static void Main(string[] args) 2 { 3 Int32 v = 5; 4 object o = v; 5 v = 123; 6 Console.WriteLine(v + "," + (Int32)o); // 打印一 7 Console.WriteLine(v + "," + o); // 打印二 8 Console.WriteLine(v.ToString() + "," + o); // 打印三 9 }
A8: 第一次打印发生了三次装箱(Int32类型的v转换为Object类型的o, Int32类型的v转换为字符串,o被转换为Int32发生拆箱,再被转换为字符串,发生装箱);第二次打印发生两次装箱(Int32类型的v转换为Object类型的o, Int32类型的v转换为字符串);第三次打印只发生了一次装箱(Int32类型的v转换为Object类型的o)。
Q9: 以下代码是否正确?
1 static void Main(string[] args) 2 { 3 Int32 x = 5; 4 object o = x; 5 Int16 y = (Int16)o; 6 }
A9: 不对。当一个对象进行拆箱操作时,只能转换为它原先未装箱时的值类型,故应该为:
Int16 y = (Int16)(Int32)o;