CLR via C#, 4th -- 【设计类型】 -- 第5章 基元类型、引用类型和值类型

5.1 编程语言的基元类型

某些数据类型如此常用,以至于许多编译器允许代码以简化语法来操纵它们。

System.Int32 a = new System.Int32();
int a = 0;

这种语法不仅增强了代码可读性,生成的IL代码还与使用System.Int32生成的1L代码完全一致。

基元类型(primitive type)

编译器直接支持的数据类型称为基元类型(primitive type)。基元类型直接映射到Framework类库(FCL)中存在的类型。例如,C#的int直接映射到System.Int32类型。

Primitive Type

FCL Type

CLS-Compliant

Description

sbyte

System.SByte

No

Signed 8-bit value

byte

System.Byte

Yes

Unsigned 8-bit value

short

System.Int16

Yes

Signed 16-bit value

ushort

System.UInt16

No

Unsigned 16-bit value

int

System.Int32

Yes

Signed 32-bit value

uint

System.UInt32

No

Unsigned 32-bit value

long

System.Int64

Yes

Signed 64-bit value

ulong

System.UInt64

No

Unsigned 64-bit value

char

System.Char

Yes

16-bit Unicode  character (char never represents an 8-bit value as it would in unmanaged C++.)

float

System.Single

Yes

IEEE 32-bit floating point value

double

System.Double

Yes

IEEE 64-bit floating point value

bool

System.Boolean

Yes

A true/false value

decimal

System.Decimal

Yes

A 128-bit high-precision floating-point value com-monly used for financial calculations in which round-ing errors can’t be tolerated. Of the 128 bits, 1 bit

represents the sign of the value, 96 bits represent the value itself, and 8 bits represent the power of 10 to divide the 96-bit value by (can be anywhere from 0

to 28). The remaining bits are unused.

string

System.String

Yes

An array of characters

object

System.Object

Yes

Base type of all types

dynamic

System.Object

Yes

To the common language runtime (CLR), dynamic is identical to object . However, the C# compiler allows dynamic variables to participate in dynamic dispatch by using a simplified syntax. For more infor-mation, see “The dynamic Primitive Type” section at the end of this chapter.

希望编译器根本不提供基元类型名称,而是强迫开发人员使用FCL类型名称,理由如下:

  1. 许多开发人员纠结于是用string还是String。由于C#的string(一个关键字)直接映射到System.String(一个FCL类型),所以两者没有区别,都可使用。
  2. C#的long映射到System.Int64,但在其他编程语言中,long可能映射到Int16或Int32。
  3. FCL的许多方法都将类型名作为方法名的一部分。例如,BinaryReader类型的方法包括ReadBoolean,ReadInt32,ReadSingle等;
  4. 平时只用C#的许多程序员逐渐忘了还可以用其他语言写面向CLR的代码,"C主义

首先,编译器能执行基元类型之间的隐式或显式转型

Int32  i = 5;         // Implicit cast from Int32  to Int32  
Int64  l = i;         // Implicit cast from Int32  to Int64  
Single s = i;         // Implicit cast from Int32  to Single   
Byte   b = (Byte) i;  // Explicit cast from Int32  to Byte  
Int16  v = (Int16) s; // Explicit cast from Single to Int16

只有在转换“安全”的时候,C#才允许隐式转型。所谓“安全”,是指不会发生数据丢失的情况,比如从Int32转换为Int64,但如果可能不安全,C#就要求显式转型。对于数值类型,“不安全”意味着转换后可能丢失精度或数量级。
注意,不同编译器可能生成不同代码来处理这些转型。例如,将值为6.8的Single转型为Int32,有的编译器可能生成代码对其进行截断(向下取整),最终将6放到一个Int32中;其他编译器则可能将结果向上取整为7,顺便说一句,C#总是对结果进行截断。
除了转型,基本类型还能写成字面值(literal),字面值可被看成是类型本身的实例(123456)

Console.WriteLine(123.ToString() + 456.ToString());  // "123456"

如果表达式由字面值构成,编译器在编译时就能完成表达式求值
最后,编译器知道如何和以什么顺序解析代码中的操作符(比如+,-,*,/,%,&,^,|,==,!=,>,<,>=,<=,<<,>>,~,!,++,--等):

Boolean found = false;    // Generated code sets found to 0  
Int32 x = 100 + 20 + 3;   // Generated code sets x to 123   
String s = "a " + "bc";   // Generated code sets s to "a bc"

checked和unchecked基元类型操作(溢出)

CLR有一个add指令,作用是将两个值相加,但不执行溢出检查。还有一个add.ovf指令,作用也是将两个值相加,但会在发生溢出时抛出System.OverflowException异常。除了用于加法运算的IL指令,CLR还为减、乘和数据转换提供了类似的IL指令,分别是sub/sub.ovfmul/mul.ovfconv/conv.ovf.
让C#编译器控制溢出的一个办法是使用/checked+编译器开关。
除了全局性地打开或关闭溢出检查,程序员还可在代码的特定区域控制溢出检查。C#通过checked和unchecked操作符来提供这种灵活性。

UInt32 invalid = unchecked((UInt32) (­1));  // OK
Byte b = 100;  
b = checked((Byte) (b + 200));    // OverflowException is thrown

除了checkedunchecked操作符,C#还支持checked和unchecked语句,它们造成一个块中的所有表达式都进行或不进行溢出检查:

checked {                         // Start of checked block  
   Byte b = 100;  
   b = (Byte) (b + 200);          // This expression is checked for overflow.   
}                                 // End of checked block

计算相关的建议

  • 尽量使用有符号数值类型(比如Int32和Int64)而不是无符号数值类型(比如UInt32和UInt64),这允许编译器检测更多的上溢/下溢错误。除此之外,类库多个部分(比如Array和String的Length属性)被硬编码为返回有符号的值。
  • 写代码时,如果代码可能发生你不希望的溢出(可能是因为无效的输入,比如要求使用最终用户或客户机提供的数据),就把这些代码放到checked块中。
  • 写代码时,将允许发生溢出的代码显式放到unchecked块中,比如在计算校验和时。
  • 对于没有使用checked或unchecked的任何代码,都假定你希望在发生溢出时抛出一个异常,比如在输入已知的前提下计算一些东西(比如质数),此时的溢出应被计为bug。

开发应用程序时,打开编译器的/checked+开关进行调试性生成。这样系统会对没有显式标记checked或unchecked的代码进行溢出检查,所以应用程序运行起来会慢一些。此时一旦发生异常,就可以轻松检测到,而且能及时修正代码中的bug。但是,为了正式发布而生成应用程序时,应使用编译器的/checked-开关,确保代码能更快运行,不会产生溢出异常。要在Microsoft Visual Studio中更改Checked设置,请打开项目的属性页,点击“生成”标签,单击“高级”,再勾选“检查运算上溢/下溢”。
System.Decimal
重要提示 System.Decimal是非常特殊的类型。虽然许多编程语言(包括C#和Visual Basic)将Decimal视为基元类型,但CLR不然。这意味着CLR没有知道如何处理Decimal值的IL指令。在文档中查看Decimal类型,可以看到它提供了一系列public static方法,包括Add,Subtract,Multiply,Divide等。此外,Decimal类型还为+,-,*,/等提供了操作符重载方法.
编译使用了Decimal值的程序时,编译器会生成代码来调用Decimal的成员,并通过这些成员来执行实际运算,这意味着Decimal值的处理速度慢于CLR基元类型的值。另外,由于没有相应的1L指令来处理Decimal值,所以checked和unchecked操作符、语句以及编译器开关都失去了作用。如果对Decimal值执行的运算是不安全的,肯定会抛出OverflowException异常.
System.Numerics.BigInteger
System.Numerics.BigInteger类型也在内部使用UInt32数组来表示任意大的整数,它的值没有上限和下限。因此,对BigInteger执行的运算永远不会造成OverflowException异常。但如果值太大,没有足够多的内存来改变数组大小,对BigInteger的运算可能抛出OutOMemoryException异常。

5.2 引用类型和值类型

CLR支持两种类型:引用类型和值类型。虽然FCL的大多数类型都是引用类型,但程序员用得最多的还是值类型。引用类型总是从托管堆分配,C#的new操作符返回对象内存地址-即指向对象数据的内存地址。

引用类型

使用引用类型必须留意性能问题。首先要认清楚以下四个事实。
1、内存必须从托管堆分配。
2,堆上分配的每个对象都有一些额外成员,这些成员必须初始化。
3,对象中的其他字节(为字段而设)总是设为零。
4,从托管堆分配对象时,可能强制执行一次垃圾回收。
值类型的实例一般在线程栈上分配(虽然也可作为字段嵌入引用类型的对象中)。在代表值类型实例的变量中不包含指向实例的指针。相反,变量中包含了实例本身的字段。由于变量己包含了实例的字段,所以操作实例中的字段不需要提领指针。值类型的实例不受垃圾回收器的控制。因此,值类型的使用缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。

结构
在文档中查看类型时,任何称为“类”的类型都是引用类型。例如,System.Exception类、System.IO.FileStream类以及System.Random类都是引用类型。相反,所有值类型都称为结构或枚举。例如,System.In32结构。
所有结构都是抽象类型System.ValueType的直接派生类。System.ValueType本身又直接从System.Object派生。根据定义,所有值类型都必须从System.ValueType派生。所有枚举都从System.Enum抽象类型派生,后者又从System.ValueType派生。CLR和所有编程语言都给予枚举特殊待遇。
虽然不能在定义值类型时为它选择基类型,但如果愿意,值类型可实现一个或多个接口。除此之外,所有值类型都隐式密封,目的是防止将值类型用作其他引用类型或值类型的基类型。

The following code and Figure 5-2 demonstrate how reference types and value types differ.

// Reference type (because of 'class')  
class  SomeRef { public Int32 x; }  
 
// Value type (because of 'struct')   
struct SomeVal { public Int32 x; }  
 
static void ValueTypeDemo() {  
   SomeRef r1 = new SomeRef();   // Allocated in heap  
   SomeVal v1 = new SomeVal();   // Allocated on stack  
   r1.x = 5;                     // Pointer dereference   
   v1.x = 5;                     // Changed on stack   
   Console.WriteLine(r1.x);      // Displays "5"  
   Console.WriteLine(v1.x);      // Also displays "5"  
   // The left side of Figure 5 ­2 reflects the situation  
   // after the lines above have executed.   
   
   SomeRef r2 = r1;              // Copies reference (pointer) only   
   SomeVal v2 = v1;              // Allocate on stack & copies members   
   r1.x = 8;                     // Changes r1.x and r2.x   
   v1.x = 9;                     // Changes v1.x, not v2.x  
   Console.WriteLine(r1.x);      // Displays "8"  
   Console.WriteLine(r2.x);      // Displays "8"  
   Console.WriteLine(v1.x);      // Displays "9"  
   Console.WriteLine(v2.x);      // Displays "5"  
   // The right side of Figure 5­2 reflects the situation    
   // after ALL of the lines above have executed.  
}

SomeVal v1 = new SomeVal();   // Allocated on stack

在线程栈上分配一个SomeVal实例。C#还会确保值类型中的所有字段都初始化为零。

SomeVal v1;   // Allocated on stack

这一行生成的IL代码也会在线程栈上分配实例,并将字段初始化为零。唯一的区别在于,如果使用new操作符,C#会认为实例已初始化。

// These two lines compile because C# thinks that    
// v1's fields have been initialized to 0.   
SomeVal v1 = new SomeVal();   
Int32 a = v1.x;   
 
// These two lines don't compile because C# doesn't think that   
// v1's fields have been initialized to 0.   
SomeVal v1;  
Int32 a = v1.x;  // error CS0170: Use of possibly unassigned field 'x'

具体地说,除非满足以下全部条件,否则不应将类型声明为值类型。

  • 类型具有基元类型的行为。也就是说,是十分简单的类型,没有成员会修改类型的任何实例字段。如果类型没有提供会更改其字段的成员,就说该类型是不可变(immutable)类型。事实上,对于许多值类型,我们都建议将全部字段标记为readonly。
  • 类型不需要从其他任何类型继承。
  • 类型也不派生出其他任何类型。
  • 类型的实例较小(16字节或更小)。
  • 类型的实例较大(大于16字节),但不作为方法实参传递,也不从方法返回。

值类型和引用类型的一些区别

  • 值类型对象有两种表示形式:未装箱和已装箱,详情参见下一节。相反,引用类型总是处于已装箱形式。
  • 值类型从System.ValueType派生。该类型提供了与System.Object相同的方法。但System.ValueType重写了Equals方法,能在两个对象的字段值完全匹配的前提下返回true,此外,System.ValueType重写了GetHashCode方法。生成哈希码时,这个重写方法所用的算法会将对象的实例字段中的值考虑在内。由于这个默认实现存在性能问题,所以定义自己的值类型时应重写Equals和GetHashCode方法,并提供它们的显式实现。
  • 由于不能将值类型作为基类型来定义新的值类型或者新的引用类型,所以不应在值类型中引入任何新的虚方法。所有方法都不能是抽象的,所有方法都隐式密封(不可重写)。
  • 引用类型的变量包含堆中对象的地址。引用类型的变量创建时默认初始化为null,表明当前不指向有效对象。试图使用null引用类型变量会抛出NullReferenceException异常。相反,值类型的变量总是包含其基础类型的一个值,而且值类型的所有成员都初始化为0。值类型变量不是指针,访问值类型不可能抛出NullReferenceException异常。CLR确实允许为值类型添加“可空”(nullability)标识。
  • 将值类型变量赋给另一个值类型变量,会执行逐字段的复制。将引用类型的变量赋给另一个引用类型的变量只复制内存地址。
  • 基于上一条,两个或多个引用类型变量能引用堆中同一个对象,所以对一个变量执行的操作可能影响到另一个变量引用的对象。相反,值类型变量自成一体,对值类型变量执行的操作不可能影响另一个值类型变量。
  • 由于未装箱的值类型不在堆上分配,一旦定义了该类型的一个实例的方法不再活动,为它们分配的存储就会被释放,而不是等着进行垃圾回收。

CLR如何控制类型中的字段布局
为了告诉CLR应该怎样做,要为自己定义的类或结构应用System.Runtime.InteropServices.StructLayoutAttribute特性。可向该特性的构造器传递LayoutKind.Auto,让CLR自动排列字段;也可传递LayoutKind.Sequential,iCLR保持你的字段布局;
注意,Microsoft C#编译器默认为引用类型(类)选择LayoutKind.Auto,为值类型(给构)选择Layoutkind.Sequential.

5.3 值类型的装箱和拆箱

值类型比引用类型“轻”,原因是它们不作为对象在托管堆中分配,不被垃圾回收,也不通过指针进行引用。
将值类型转换成引用类型要使用装箱机制。

  1. 在托管堆中分配内存。分配的内存量是值类型各字段所需的内存量,还要加上托管堆所有对象都有的两个额外成员(类型对象指针和同步块索引)所需的内存量。
  2. 值类型的字段复制到新分配的堆内存。
  3. 返回对象地址。现在该地址是对象引用;值类型成了引用类型。

泛型集合类允许开发人员在操作值类型的集合时不需要对集合中的项进行装箱/拆箱。
拆箱不是直接将装箱过程倒过来。拆箱的代价比装箱低得多。拆箱其实就是获取指针的过程,该指针指向包含在一个对象中的原始值类型(数据字段)。其实,指针指向的是已装箱实例中的未装箱部分。所以和装箱不同,拆箱不要求在内存中复制任何字节。
已装箱值类型实例在拆箱时,内部发生下面这些事情。

  1. 如果包含“对已装箱值类型实例的引用”的变量为null,抛出NulReferenceException异常。
  2. 如果引用的对象不是所需值类型的已装箱实例,抛出InvalidCastException异常。
public static void Main() {   
   Int32  x = 5;  
   Object o = x;         // Box x; o refers to the boxed object  
   Int16  y = (Int16) o; // Throws an InvalidCastException  
}

从逻辑上说,完全能获取。引用的已装箱Int32,将其强制转型为Int16,但在对对象进行拆箱时,只能转型为最初未装箱的值类型-本例是Int32,以下是上述代码的正确写法:

public static void Main() {   
   Int32  x = 5;  
   Object o = x;                // Box x; o refers to the boxed object   
   Int16  y = (Int16)(Int32) o; // Unbox to the correct type and cast  
}

关于装箱最后注意一点:如果知道自己的代码会造成编译器反复对一个值类型装箱,请改成用手动方式对值类型进行装箱。

using System;  
 
public sealed class Program {  
   public static void Main() {   
      Int32 v = 5;   // Create an unboxed value type variable.   
 
#if INEFFICIENT   
      // When compiling the following line, v is boxed    
      // three times, wasting time and memory.  
      Console.WriteLine("{0}, {1}, {2}", v, v, v);   
#else   
      // The lines below have the same result, execute  
      // much faster, and use less memory.   
      Object o = v;  // Manually box v (just once).  
 
      // No boxing occurs to compile the following line.  
      Console.WriteLine("{0}, {1}, {2}", o, o, o);   
#endif  
   }  
}

前面说过,未装箱值类型比引用类型更“轻”。这要归结于以下两个原因。

  • 不在托管堆上分配。
  • 没有堆上的每个对象都有的额外成员:“类型对象指针”和“同步块索引”

由于未装箱值类型没有同步块索引,所以不能使用System.Threading.Monitor类型的方法(或者C#lock语句)让多个线程同步对实例的访问。
虽然未装箱值类型没有类型对象指针,但仍可调用由类型继承或重写的虚方法(比如Equals,GetHashCode或者Tostring),如果值类型重写了其中任何虚方法,那么CLR可以非虚地调用该方法,因为值类型隐式密封,不可能有类型从它们派生,而且调用虚方法的值类型实例没有装箱。然而,如果重写的虚方法要调用方法在基类中的实现,那么在调用基类的实现时,值类型实例会装箱,以便能够通过this指针将对一个堆对象的引用传给基方法。
但在调用非虚的、继承的方法时(比如GetType或MemberwiseClone),无论如何都要对值类型进行装箱。因为这些方法由System.Object定义,要求this实参是指向堆对象的指针。

5.3.1 使用接口更改已装箱值类型中的字段(以及为什么不应该这样做)

using System;  
 
// Point is a value type.   
internal struct Point {  
   private Int32 m_x, m_y;  
 
   public Point(Int32 x, Int32 y) {   
      m_x = x;  
      m_y = y;  
   }  
 
   public void Change(Int32 x, Int32 y) {  
      m_x = x; m_y = y;  
   }  
 
   public override String ToString() {  
      return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());  
   }  
}  
 
public sealed class Program {  
   public static void Main() {   
      Point p = new Point(1, 1);   
 
      Console.WriteLine(p);   
 
      p.Change(2, 2);  
      Console.WriteLine(p);   
 
      Object o = p;  
      Console.WriteLine(o);   
 
      ((Point) o).Change(3, 3);  
      Console.WriteLine(o);   
   }  
}

有的语言(比如C++/CLI)允许更改已装箱值类型中的字段,但C#不允许。不过,可以用接口欺骗C#,让它允许这个操作。下面是上例的修改版本:

using System;  
 
// Interface defining a Change method   
internal interface IChangeBoxedPoint {  
   void Change(Int32 x, Int32 y);  
}  
 
 
// Point is a value type.   
internal struct Point : IChangeBoxedPoint {  
   private Int32 m_x, m_y;  
 
   public Point(Int32 x, Int32 y) {   
      m_x = x;  
      m_y = y;  
   }  
 
   public void Change(Int32 x, Int32 y) {  
      m_x = x; m_y = y;  
   }  
 
   public override String ToString() {  
      return String.Format("({0}, {1})", m_x.ToString(), m_y.ToString());  
   }  
}  

 
public sealed class Program {  
   public static void Main() {   
      Point p = new Point(1, 1);   
 
      Console.WriteLine(p);   
 
      p.Change(2, 2);  
      Console.WriteLine(p);   
 
      Object o = p;  
      Console.WriteLine(o);   
 
      ((Point) o).Change(3, 3);  
      Console.WriteLine(o);   
 
      // Boxes p, changes the boxed object and discards it  
      ((IChangeBoxedPoint) p).Change(4, 4);  
      Console.WriteLine(p);   
 
       // Changes the boxed object and shows it  
      ((IChangeBoxedPoint) o).Change(5, 5);  
      Console.WriteLine(o);   
   }  
}

在第一个例子中,未装箱的Pointp转型为一个IChangeBoxedPoint,这个转型造成对p中的值进行装箱。然后在已装箱值上调用Change,这确实会将其m_x和m_y字段分别变成4和4,但在Change返回之后,已装箱对象立即准备好进行垃圾回收。所以,对WriteLine的第5个调用会显示(2,2)。许多开发人员预期的并不是这个结果。
在最后一个例子中,0引用的已装箱Point转型为一个IChangeBoxedPoint。这不需要装箱,因为0本来就是已装箱的Point,然后调用Change,它能正确修改已装箱Point的m_x和m_y字段。接口方法Change使我能够更改已装箱Point对象中的字段!现在调用writeLine,会像预期的那样显示(5,5)。本例旨在演示接口方法如何修改已装箱值类型中的字段。在C#中,不用接口方法便无法做到。

5.3.2对象相等性和同一性

System.Object类型提供了名为Equals的虚方法,作用是在两个对象包含相同值的前提下返回true.

public class Object {  
   public virtual Boolean Equals(Object obj) {  
 
      // If both references point to the same object,   
      // they must have the same value.  
      if (this == obj) return true;   
 
      // Assume that the objects do not have the same value.
      return false;  
   }  
}

乍一看,这似乎就是Equals的合理实现:假如this和obi实参引用同一个对象,就返回true.
似乎合理是因为Equals知道对象肯定包含和它自身一样的值。但假如实参引用不同对象,Equals就不肯定对象是否包含相同的值,所以返回false。换言之,对于Object的Equals方法的默认实现,它实现的实际是同一性(identity),而非相等性(equality).

下面展示了Equals方法应该如何正确地实现
1,如果obj实参为null,就返回false,因为调用非静态Equals方法时,this所标识的当前对象显然不为null.
2,如果this和obj实参引用同一个对象,就返回true。在比较包含大量字段的对象时,这一步有助于提升性能。
3,如果this和obj实参引用不同类型的对象,就返回false,一个String对象显然不等于一个FileStream对象。
4,针对类型定义的每个实例字段,将this对象中的值与obj对象中的值进行比较。任何字段不相等,就返回false.
5,调用基类的Equals方法来比较它定义的任何字段。如果基类的Equals方法返回false.就返回false;否则返回true.
所以,Microsoft本应像下面这样实现Object的Equals方法:

public class Object {  
   public virtual Boolean Equals(Object obj) {  
      // The given object to compare to can't be null  
      if (obj == null) return false;
     // If objects are different types, they can't be equal.   
      if (this.GetType() != obj.GetType()) return false;  
 
      // If objects are same type, return true if all of their fields match   
      // Because System.Object defines no fields, the fields match  
      return true;  
   }  
}

但由于Microsoft没有像这样实现Equals,所以Equals的实现规则远比想像的复杂。类型重写Equals方法时应调用其基类的Equals实现(除非基类就是Object),另外,由于类型能重写Object的Equals方法,所以不能再用它测试同一性。为了解决这个问题,Object提供了静态方法ReferenceEquals,其原型如下:

public class Object {  
   public static Boolean ReferenceEquals(Object objA, Object objB) {  
      return (objA == objB);  
   }  
}

System.ValueType(所有值类型的基类)就重写了Object的Equals方法,并进行了正确的实现来执行值的相等性检查(而不是同一性检查),ValueType的Equals内部是这样实现
1,如果obj实参为null.就返回false.
2,如果this和obj实参引用不同类型的对象,就返回false
3,针对类型定义的每个实例字段,都将this对象中的值与obj对象中的值进行比较(通过调用字段的Equals方法),任何字段不相等,就返回false.
4,返回true,ValueType的Equals方法不调用Object的Equals方法。
在内部,ValueType的Equals方法利用反射完成上述步骤3。由于CLR反射机制慢,定义自己的值类型时应重写Equals方法来提供自己的实现,从而提高用自己类型的实例进行值相等性比较的性能。
定义自己的类型时,你重写的Equals要符合相等性的4个特征

  • Equals必须自反:x.Equals(x)肯定返回true.
  • Equals必须对称;x.Equals(y)和y.Equals(x)返回相同的值。
  • Equals必须可传递:x.Equals(y)返回true,y.Equals(z)返回true,则x.Equals(2)肯定返回true.
  • Equals必须一致。比较的两个值不变,Equals返回值(true或false)也不能变。

重写Equals方法时,可能还需要做下面几件事情。

  • 让类型实现System.IEquatable<T>接口的Equals方法
    这个泛型接口允许定义类型安全的Equals方法。通常,你实现的Equals方法应获取一个Object参数,以便在内部调用类型安全的Equals方法。
  • 重载==和!=操作符方法
    通常应实现这些操作符方法,在内部调用类型安全的Equals.

此外,如果以后要出于排序目的而比较类型的实例,类型还应实现System.IComparable的CompareTo方法和System.IComparable<T>的类型安全的CompareTo方法。如果实现了这些方法,还可考虑重载各种比较操作符方法(<,<-,>,>-),在这些方法内部调用类型安全的CompareTo方法。

5.4 对象哈希码

GetHashCode

如果你定义的类型重写了Equals方法,还应重写GetHashCode方法。事实上,如果类型重写Equals的同时没有重写GetHashCode,Microsoft C#编译器会生成一条警告。
类型定义Equals之所以还要定义GetHashCode,是由于在System.Collections.Hashtable类型、System.Collections.Generic.Dictionary类型以及其他一些集合的实现中,要求两个对象必须具有相同哈希码才被视为相等。所以,重写Equals就必须重写GetHashCode,确保相等性算法和对象哈希码算法一致。
选择算法来计算类型实例的哈希码时,请遵守以下规则。

  • 这个算法要提供良好的随机分布,使哈希表获得最佳性能。
  • 可在算法中调用基类的GtHashCode方法,并包含它的返回值。但一般不要调用Object或ValueType的GetHashCode方法,因为两者的实现都与高性能哈希算法“不沾边
  • 算法至少使用一个实例字段。
  • 理想情况下,算法使用的字段应该不可变(immutable);也就是说,字段应在对象构造时初始化,在对象生存期“永不言变”。
  • 算法执行速度尽量快。
  • 包含相同值的不同对象应返回相同哈希码。例如,包含相同文本的两个String对象应返回相同哈希码。

假如因为某些原因要实现自己的哈希表集合,或者要在实现的代码中调用GetHashCode,记住千万不要对哈希码进行持久化,因为哈希码很容易改变。

5.5 dynamic基元类型

类型安全的语言的优势在于:程序员会犯的许多错误都能在编译时检测到,确保代码在尝试执行前是正确的。此外,能编译出更小、更快的代码,因为能在编译时进行更多预设,并在生成的IL和元数据中落实预设。

dynamic
可将表达式的结果放到变量中,并将变量类型标记为dynamic。然后,可以用这个dynamic表达式/变量调用成员,比如字段、属性/索引器、方法、委托以及一元/二元/转换操作符。代码使用dynamic表达式/变量调用成员时,编译器生成特殊IL代码来描述所需的操作。这种特殊的代码称为payload(有效载荷),在运行时,payload代码根据dynamic表达式/量引用的对象的实际类型来决定具体执行的操作。

internal static class DynamicDemo { 
   public static void Main() { 
      dynamic value; 
      for (Int32 demo = 0; demo < 2; demo++) {  
         value = (demo == 0) ? (dynamic) 5 : (dynamic) "A"; 
         value = value + value;  
         M(value);  
      } 
   } 
 
   private static void M(Int32 n)  { Console.WriteLine("M(Int32): " + n); } 
   private static void M(String s) { Console.WriteLine("M(String): " + s); }  
}

DynamicAttribute

如果字段、方法参数或方法返回值的类型是dynamic,编译器会将该类型转换为System.Object,并在元数据中向字段、参数或返回类型应用System.Runtime.CompilerServices.DynamicAttribute的实例。如果局部变量被指定为dynamic,则变量类型也会成为Object,但不会向局部变量应用DynamicAttribute,因为它限制在方法内部使用。由于dynamic其实就是Object,所以方法签名不能仅靠dynamic和Object的变化来区分。

泛型类(引用类型)、结构(值类型)、接口、委托或方法的泛型类型实参也可以是dynamic类型。编译器将dynamic转换成Object,并向必要的各种元数据应用DynamicAttribute。
所有表达式都能隐式转型为dynamic,因为所有表达式最终都生成从Object派生的类型"。正常情况下,编译器不允许写代码将表达式从Object隐式转型为其他类型;必须显式转型。但是,编译器允许使用隐式转型语法将表达式从dynamic转型为其他类型。

Object o1 = 123;        // OK: Implicit cast from Int32 to Object (boxing) 
Int32 n1 = o;           // Error: No implicit cast from Object to Int32  
Int32 n2 = (Int32) o;   // OK: Explicit cast from Object to Int32 (unboxing)  
 
dynamic d1 = 123;       // OK: Implicit cast from Int32 to dynamic (boxing) 
Int32 n3 = d1;          // OK: Implicit cast from dynamic to Int32 (unboxing)

注意,dynamic表达式的求值结果是一个动态表达式。例如以下代码:

dynamic d = 123;  
var result = M(d);  // Note: 'var result' is the same as 'dynamic result'

不要混淆dynamic和var

用var声明局部变量只是一种简化语法,它要求编译器根据表达式推断具体数据类型。var关键字只能在方法内部声明局部变量,而dynamic关键字可用于局部变量、字段和参数。表达式不能转型为var,但能转型为dynamic.必须显式初始化用var声明的变量,但无需初始化用dynamic声明的变量。

foreach

如果dynamic表达式被指定为foreach语句中的集合,或者被指定为using语句中的资源,编译器会生成代码,分别将表达式转型为非泛型System.IEnumerable接口或System.IDisposable接口。转型成功,就使用表达式,代码正常运行。转型失败,就抛出Microsoft.CSharp.RuntimeBinder.RuntimeBinderException异常。

运行时绑定器(runtime binder)

早先指出C#编译器会生成payload代码,在运行时根据对象实际类型判断要执行什么操1作。这些payload代码使用了称为运行时绑定器(runtime binder)的类。
在运行时,C#的“运行时绑定器”根据对象的运行时类型分析应采取什么动态操作。绑定器首先检查类型是否实现了IDynamicMetaObjectProvider接口。如果是,就调用接口的GetMetaObject方法,它返回DynamicMetaObject的一个派生类型。该类型能处理对象的所有成员、方法和操作符绑定。IDynamicMetaObjectProvider接口和DynamicMetaObject基类都在System.Dynamic命名空间中定义,都在System.Core.dll程序集中。

IDynamicMetaObjectProvider

在运行时,C#的“运行时绑定器”根据对象的运行时类型分析应采取什么动态操作。绑定器首先检查类型是否实现了IDynamicMetaObjectProvider接口。如果是,就调用接口的GetMetaObject方法,它返回DynamicMetaObject的一个派生类型。该类型能处理对象的所有成员、方法和操作符绑定。IDynamicMetaObjectProvider接口和DynamicMetaObject基类都在System.Dynamic命名空间中定义,都在System.Core.dll程序集中。
如果在动态表达式中使用的一个对象的类型未实现IDynamicMetaObjectProvider接口,C#编译器会将对象视为用C#定义的普通类型的实例,利用反射在对象上执行操作。

posted @ 2019-12-02 23:12  FH1004322  阅读(241)  评论(0)    收藏  举报