本章要讨论的是.net的各种类型。这章开始,我想摒弃以前的抄书模式,尝试自己阅读后先行总结,然后再写博客。
基元类型
所谓基元类型,指的是编译器直接支持的数据类型。基元类型直接映射到Framework类库中存在的类型。下面四行代码可以生成完全相同的IL:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace Program1 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 int a = 0; 14 Int32 b = 0; 15 int c = new int(); 16 Int32 d = new Int32(); 17 } 18 } 19 }
再看他们的IL代码:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 10 (0xa) .maxstack 1 .locals init (int32 V_0, int32 V_1, int32 V_2, int32 V_3) IL_0000: nop IL_0001: ldc.i4.0 IL_0002: stloc.0 IL_0003: ldc.i4.0 IL_0004: stloc.1 IL_0005: ldc.i4.0 IL_0006: stloc.2 IL_0007: ldc.i4.0 IL_0008: stloc.3 IL_0009: ret } // end of method Program::Main
由此可知,这四个写法是完全等价的。
在本书中,坚持使用FCL名称,主要有以下原因:
1.很多人纠结于使用string还是System.String,其实这两者没有区别。类似的,还有int和Int32:C#的int永远映射到Int32.C#的long固定映射到Int64.
2.FLC的许多方法都将类型名作为方法名的一部分。
3.方便些其他面向CLR的代码(代码风格一致)。
在高精度基元类型隐式转换到低精度基元类型的时候,往往会进行截断处理(区别于向上取整)。
C#自带checked操作符来在特定的区域控制溢出检查:
Byte b = 100; b = checked((Byte)(b + 200));
会抛出异常:
还可以使用checked语句:
1 static void Main(string[] args) 2 { 3 checked { 4 Byte b = 100; 5 b = (Byte)(b + 200); 6 } 7 8 }
结果是一样的。如果使用了checked语句块,还可以将+=应用于Byte:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 checked { 6 Byte b = 100; 7 b += 200; 8 } 9 10 } 11 }
在日常编程时,给予诸位如下建议:
1.尽量使用有符号数值类型Int32之类而不是UInt32,这样编译器会检查更多的上溢下溢。此外,类库中的很多方法的返回值都是有符号的,这样子可以减少强制类型转换。以及,无符号数值类型不符合CLS。
2.如果代码可能发生溢出,请放到checked语句块中。
3.将允许溢出的代码放到unchecked中。
4.对于没有使用checked和unchecked的代码,溢出默认会抛出异常,
引用类型和值类型
首先,要认清楚四个事实:
1.内存必须从托管堆中分配;
2.堆上的每一个对象都有额外成员,这些成员必须初始化;
3.对象的其他字节总是为零;
4.从托管堆分配对象时,可能强制执行一次GC。
因此,使用引用类型而非值类型的时候,性能会下降。在设计自己的类型时,要考虑是否应该定义成值类型而不是引用类型。除非满足以下全部条件,否则不应该声明为值类型:
1.类型具有基元类型的行为,是不可变类型(没有提供会更改其字段的成员);
2.不需要从其他任何类型继承;
3.没有派生类型;
4.类型实例较小(小于等于16字节);
5.实例类型较大,但不作为方法传递实参,也不从方法返回。
列出值类型和引用类型的一些区别:
1.值类型有两种形式:已装箱和未装箱。引用类型总是处于已装箱;
2.值类型从ValueType派生
#region 程序集 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 // C:Program Files (x86)Reference AssembliesMicrosoftFramework.NETFrameworkv4.6.1mscorlib.dll #endregion using System.Runtime.InteropServices; using System.Security; namespace System { // // 摘要: // 提供值类型的基类。 [ComVisible(true)] public abstract class ValueType { // // 摘要: // 初始化 System.ValueType 类的新实例。 protected ValueType(); // // 摘要: // 指示此实例与指定对象是否相等。 // // 参数: // obj: // 要与当前实例进行比较的对象。 // // 返回结果: // 如果 obj 和该实例具有相同的类型并表示相同的值,则为 true;否则为 false。 [SecuritySafeCritical] public override bool Equals(object obj); // // 摘要: // 返回此实例的哈希代码。 // // 返回结果: // 一个 32 位有符号整数,它是该实例的哈希代码。 [SecuritySafeCritical] public override int GetHashCode(); // // 摘要: // 返回该实例的完全限定类型名。 // // 返回结果: // 包含完全限定类型名的 System.String。 public override string ToString(); } }
而ValueType继承自System.Object;
3.不能在值类型中加入虚方法,所有的方法都不能抽象,不可重写;
4.引用类型包含了堆中对象的地址。引用类型变量在创建的时候默认初始化为NULL,而值类型总是0。null引用类型会抛出异常。值类型可以添加可空标识;
5.值类型复制是完全拷贝,而引用类型只拷贝地址;
6.修改引用类型,会导致其引用也受到影响;
7.因为值类型是没有被装箱的,所以一旦一个实例不再活动,为它分配的存储就会被释放,而不是等待GC。
拆箱和装箱
这部分是这一章的重中之重之重中之重中重。
很多时候,要获取值类型的实例引用。这也是“什么时候会进行装箱”的答案。
先例举一个简单的例子:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Collections; 7 8 namespace Program4 9 { 10 class Program 11 { 12 internal struct Point { 13 private Int32 m_x, m_y; 14 public Point(Int32 x, Int32 y) { 15 m_x = x; 16 m_y = y; 17 } 18 public void Change(Int32 x, Int32 y) 19 { 20 m_x = x; 21 m_y = y; 22 } 23 public override String ToString() 24 { 25 return String.Format("{0}, {1}", m_x.ToString(), m_y.ToString()); 26 } 27 } 28 static void Main(string[] args) 29 { 30 ArrayList a = new ArrayList(); 31 Point p = new Point(0, 0); 32 for (Int32 i = 0; i < 5; i++) { 33 p.Change(i, i); 34 a.Add(p); 35 } 36 } 37 } 38 }
本例中的Add方法原型如下 :
// // 摘要: // 将对象添加到 System.Collections.ArrayList 的结尾处。 // // 参数: // value: // 要添加到 System.Collections.ArrayList 末尾的 System.Object。该值可以为 null。 // // 返回结果: // value 已添加的 System.Collections.ArrayList 索引。 // // 异常: // T:System.NotSupportedException: // The System.Collections.ArrayList is read-only.-or- The System.Collections.ArrayList // has a fixed size. public virtual int Add(object value);
可以看出来,Add要获取的是一个Object,是一个引用类型,但是Point p是一个值类型。为了使代码正确工作,需要将p转换成在堆中托管的对象,以获取对该对象的引用。
这时,就要使用装箱机制。对值类型装箱时发生了如下事情:
1.在托管堆中分配内存。除了值类型各字段所需的内存量,还需要为类型对象指针和同步块索引分配内存空间;
2.值类型的字段拷贝到新分配的内存;
3.返回对象地址。
可以看一下IL代码:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // 代码大小 59 (0x3b) .maxstack 3 .locals init (class [mscorlib]System.Collections.ArrayList V_0, valuetype Program4.Program/Point V_1, int32 V_2, bool V_3) IL_0000: nop IL_0001: newobj instance void [mscorlib]System.Collections.ArrayList::.ctor() IL_0006: stloc.0 IL_0007: ldloca.s V_1 IL_0009: ldc.i4.0 IL_000a: ldc.i4.0 IL_000b: call instance void Program4.Program/Point::.ctor(int32, int32) IL_0010: nop IL_0011: ldc.i4.0 IL_0012: stloc.2 IL_0013: br.s IL_0032 IL_0015: nop IL_0016: ldloca.s V_1 IL_0018: ldloc.2 IL_0019: ldloc.2 IL_001a: call instance void Program4.Program/Point::Change(int32, int32) IL_001f: nop IL_0020: ldloc.0 IL_0021: ldloc.1 IL_0022: box Program4.Program/Point IL_0027: callvirt instance int32 [mscorlib]System.Collections.ArrayList::Add(object) IL_002c: pop IL_002d: nop IL_002e: ldloc.2 IL_002f: ldc.i4.1 IL_0030: add IL_0031: stloc.2 IL_0032: ldloc.2 IL_0033: ldc.i4.5 IL_0034: clt IL_0036: stloc.3 IL_0037: ldloc.3 IL_0038: brtrue.s IL_0015 IL_003a: ret } // end of method Program::Main
会发现其中有装箱操作。
这里稍微扩展一个,关于for循环在IL中的知识:在IL中,for循环通过两个指令:br.s(无条件地将控制转移到目标指令)和brtrue.s(如果 value 为 true、非空或非零,则将控制转移到目标指令)两个指令实现循环,clt(比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上)来控制是否继续循环的那个值。
下面来看拆箱。假定我们要获取ArrayList的第一个元素:
Point p2 = (Point)a[0];
它获取了ArrayList的元素0包含的引用,试图将其放到Point值类型的实例p中。为此,已装箱Point对象中的所有字段都必须复制到值类型变量p2中。为此,已装箱Point对象中的所有字段都必须复制到值类型变量p2中,后者在线程栈上。CLR分两步完成复制:第一步获取已装箱Point对象中哥哥Point字段的地址,这个过程被称为拆箱。第二步就是将字段包含的值从堆复制到基于栈的值类型实例中。
拆箱不是将装箱的过程倒过来。拆箱只是获取指针的过程,该指针指向包含在一个对象中的原始值类型。指针指的是已装箱实例中的未装箱部分。
已装箱值类型的实例在拆箱时,会发生下面的事情:
1.如果包含“对已装箱值类实例的引用”的变量为null,抛出异常;
2.如果引用的对象不是所需值类型以装箱的实例,抛出异常。
第二条的具体情况举例:
Int32 x = 5; Object o = x; Int16 y = (Int16)o;
正确的写法应该是:
Int32 x = 5; Object o = x; Int16 y = (Int16)(Int32)o;
再来看一个例子:
Int32 x = 5; Object o = x; x = 123; Console.WriteLine(x + "," + (Int32)o);
请问在这里总共执行了多少次装箱?
答案是3次。
第一次装箱发生在Object o = x,第二次是WriteLine的x(在WriteLine需要一个String对象,而String是个引用类型。为了将Int32转换成String,需要进行一次装箱操作),第三次是在o进行了一次拆箱操作后,为了获取String,又进行了一次装箱。
可以用下面的写法来避免第二次拆箱和第三次装箱:
Console.WriteLine(x + "," + o);
还可以避免第一次的装箱操作:
Console.WriteLine(x.ToString + "," + o);
虽然未装箱对象没有类型对象指针,但仍可调用由类型继承或重写的虚方法。如果值类型重写了虚方法,那么CLR可以非虚的调用该方法,因为值类型隐式密封,不会有类型派生,而且调用虚方法的值类型没有封装。然而。如果重写的虚方法要调用在基类中的实现的时候,值类型就会装箱,以便通过一个this指针将对一个堆对象的引用传给基方法。将值类型的未装箱实例转型为类型的某个接口时要对实例进行装箱,这是因为接口变量必须包含对堆对象的引用。可以看下面的代码,结合其IL:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Collections; 7 8 namespace Program4 9 { 10 class Program 11 { 12 internal struct Point 13 { 14 private Int32 m_x, m_y; 15 public Point(Int32 x, Int32 y) 16 { 17 m_x = x; 18 m_y = y; 19 } 20 public void Change(Int32 x, Int32 y) 21 { 22 m_x = x; 23 m_y = y; 24 } 25 public override String ToString() 26 { 27 return String.Format("{0}, {1}", m_x.ToString(), m_y.ToString()); 28 } 29 } 30 static void Main(string[] args) 31 { 32 Point p = new Point(0, 0); 33 Console.WriteLine(p); 34 p.Change(1, 2); 35 Console.WriteLine(p); 36 object o = p; 37 Console.WriteLine(o); 38 ((Point)o).Change(3, 3); 39 Console.WriteLine(o); 40 } 41 } 42 }
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 84 (0x54) .maxstack 3 .locals init (valuetype Program4.Program/Point V_0, object V_1, valuetype Program4.Program/Point V_2) IL_0000: nop IL_0001: ldloca.s V_0 IL_0003: ldc.i4.0 IL_0004: ldc.i4.0 IL_0005: call instance void Program4.Program/Point::.ctor(int32, int32) IL_000a: nop IL_000b: ldloc.0 IL_000c: box Program4.Program/Point IL_0011: call void [mscorlib]System.Console::WriteLine(object) IL_0016: nop IL_0017: ldloca.s V_0 IL_0019: ldc.i4.1 IL_001a: ldc.i4.2 IL_001b: call instance void Program4.Program/Point::Change(int32, int32) IL_0020: nop IL_0021: ldloc.0 IL_0022: box Program4.Program/Point IL_0027: call void [mscorlib]System.Console::WriteLine(object) IL_002c: nop IL_002d: ldloc.0 IL_002e: box Program4.Program/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 Program4.Program/Point IL_0041: stloc.2 IL_0042: ldloca.s V_2 IL_0044: ldc.i4.3 IL_0045: ldc.i4.3 IL_0046: call instance void Program4.Program/Point::Change(int32, int32) IL_004b: nop IL_004c: ldloc.1 IL_004d: call void [mscorlib]System.Console::WriteLine(object) IL_0052: nop IL_0053: ret } // end of method Program::Main
对象哈希码
FLC的设计者认为,如果能将对象的任何实例放到哈希表集合中,能带来很多好处。为此,System.Object提供了虚方法GetHashCode,能获取任意对象的Int32的哈希码。所以,如果重写了Equals方法,一定要重写GetHashCode方法。