CLR via C#, 4th -- 【设计类型】 -- 第8章方 法

8.1 实例构造器和类(引用类型)

构造器是将类型的实例初始化为良好状态的特殊方法。构造器方法在“方法定义元数据表”中始终叫做.ctor(constructor的简称),创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。
构造引用类型的对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零没有被构造器显式重写的所有字段都保证获得0null值。
和其他方法不同,实例构造器永远不能被继承。也就是说,类只有类自己定义的实例构造器。由于永远不能继承实例构造器,所以实例构造器不能使用以下修饰符:virtualnewoverridesealedabstract。如果类没有显式定义任何构造器,C#编译器将定义一个默认(无参)构造器。在它的实现中,只是简单地调用了基类的无参构造器。

public class SomeType {  
}

//it is as though you wrote the code as follows.

public class SomeType {  
   public SomeType() : base() { }  
}

如果类的修饰符为abstract,那么编译器生成的默认构造器的可访问性就为protected;否则,构造器会被赋予public可访问性。如果基类没有提供无参构造器,那么派生类必须显式调用一个基类构造器,否则编译器会报错。如果类的修饰符为staticsealedabstract)",编译器根本不会在类的定义中生成默认构造器。

为了使代码“可验证”(verifiable),类的实例构造器在访问从基类继承的任何字段之前,必须先调用基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认的基类构造器的调用。
极少数时候可以在不调用实例构造器的前提下创建类型的实例。一个典型的例子是Objec的MemberwiseClone方法。该方法的作用是分配内存,初始化对象的附加字段(类型对象指针和同步块索引),然后将源对象的字节数据复制到新对象中。另外,用运行时序列化器(runtime serializer)反序列化对象时,通常也不需要调用构造器。反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializedObject或者GetSafeUninitializedObject方法为对象分配内存,期间不会调用一个构造器。
重要提示

不要在构造器中调用虚方法。原因是假如被实例化的类型重写了虚方法,就会执行派生类型对虚方法的实现。但在这个时候,尚未完成对继承层次结构中的所有字段的初始化(被实例化的类型的构造器还没有运行呢)。所以,调用虚方法会导致无法预测的行为。归根到底,这是由于调用虚方法时,直到运行时之前都不会选择执行该方法的实际类型。

internal sealed class SomeType {   
   private Int32 m_x = 5;   
}
.method public hidebysig specialname rtspecialname   
        instance void  .ctor() cil managed 
{ 
  // Code size       14 (0xe) 
  .maxstack  8 
  IL_0000:  ldarg.0 
  IL_0001:  ldc.i4.5 
  IL_0002:  stfld      int32 SomeType::m_x 
  IL_0007:  ldarg.0 
  IL_0008:  call       instance void [mscorlib]System.Object::.ctor() 
  IL_000d:  ret 
} // end of method SomeType::.ctor

可以看出,SomeType的构造器把值5存储到字段m-x,再调用基类的构造器。换句话说,C#编译器提供了一个简化的语法,允许以“内联”(其实就是嵌入)方式初始化实例字段。
但在幕后,它会将这种语法转换成构造器方法中的代码来执行初始化。

internal sealed class SomeType {   
   private Int32  m_x = 5;  
   private String m_s = "Hi there";   
   private Double m_d = 3.14159;   
   private Byte   m_b;   
  
   // Here are some constructors.   
   public SomeType()         { ... }  
   public SomeType(Int32 x)  { ... }  
   public SomeType(String s) { ...; m_d = 10; }   
}

编译器为这三个构造器方法生成代码时,在每个方法的开始位置,都会包含用于初始化m_x,m_s和m_d的代码。在这些初始化代码之后,编译器会插入对基类构造器的调用。
再然后,会插入构造器自己的代码。例如,对于获取一个String参数的构造器,编译器生成的代码首先初始化m_x,m_s和m_d,再调用基类(Object)的构造器,再执行自己的代码(最后是用值10覆盖m_d原先的值),注意,即使没有代码显式初始化m_b,m_b也保始化为0.

由于有三个构造器,所以编译器生成三次初始化m_x,m_s和m_d的代码-每个构造器一次。如果有几个已初始化的实例字段和许多重载的构造器方法,可考虑不是在定义字段时初始化,而是创建单个构造器来执行这些公共的初始化。然后,让其他构造器都显式调用这个公共初始化构造器。这样能减少生成的代码。

internal sealed class SomeType {   
   // Do not explicitly initialize the fields here.  
   private Int32  m_x;   
   private String m_s;   
   private Double m_d;   
   private Byte   m_b;   
  
   // This constructor sets all fields to their default.  
   // All of the other constructors explicitly invoke this constructor.  
   public SomeType() {   
      m_x = 5;  
      m_s = "Hi there";  
      m_d = 3.14159;   
      m_b = 0xff;   
   }  
  
   // This constructor sets all fields to their default, then changes m_x.  
   public SomeType(Int32 x) : this() {  
      m_x = x;  
   }  
  
   // This constructor sets all fields to their default, then changes m_s.  
   public SomeType(String s) : this() {  
      m_s = s;  
   }  
  
   // This constructor sets all fields to their default, then changes m_x & m_s.   
   public SomeType(Int32 x, String s) : this() {  
      m_x = x;  
      m_s = s;  
   }  
}

8.2 实例构造器和结构(值类型)

值类型(struct)构造器的工作方式与引用类型(class)的构造器截然不同。CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型其实并不需要定义构造器,C#编译器根本不会为值类型内联(嵌入)默认的无参构造器。
CLR确实允许为值类型定义构造器。但必须显式调用才会执行。

internal struct Point {   
   public Int32 m_x, m_y;   
  
   public Point(Int32 x, Int32 y) {   
      m_x = x;  
      m_y = y;  
   }  
}  
  
internal sealed class Rectangle {   
   public Point m_topLeft, m_bottomRight;    
  
   public Rectangle() {  
      // In C#, new on a value type calls the constructor to   
      // initialize the value type's fields.  
      m_topLeft     = new Point(1, 2);  
      m_bottomRight = new Point(100, 200);   
   }  
}

C#编译器故意不允许值类型定义无参构造器,目的是防止开发人员对这种构造器在什么时候调用产生迷惑。由于不能定义无参构造器,所以编译器永远不会生成自动调用它的代码。没有无参构造器,值类型的字段总是被初始化为0或null.实际上,即便值类型提供了无参构造器,许多编译器也永远不会生成代码来自动调用它。为了执行值类型的无参构造器,开发人员必须增加显式调用值类型构造器的代码。

由于C#不允许为值类型定义无参构造器,所以编译以下类型时,C#编译器将显示消息:error CS0573:"SomeValType.m_x":结构中不能有实例字段初始值设定项。

internal struct SomeValType {  
   // You cannot do inline instance field initialization in a value type.  
   private Int32 m_x = 5;   
}

为了生成“可验证”代码,在访问值类型的任何字段之前,都需要对全部字段进行赋值。所以,值类型的任何构造器都必须初始化值类型的全部字段。以下类型为值类型定义了一个构造器,但没有初始化值类型的全部字段

internal struct SomeValType {  
   private Int32 m_x, m_y;  
  
   // C# allows value types to have constructors that take parameters.   
   public SomeValType(Int32 x) {   
      m_x = x;      
      // Notice that m_y is not initialized here.  
   }  
}

编译上述类型,C#编译器会显示消息:error CS0171:在控制返回到调用方之前,字段"SomeValType.m_y"必须完全赋值。为了修正这个问题,需要在构造器中为y赋一个值(通常是0)

// C# allows value types to have constructors that take parameters. 
public SomeValType(Int32 x) { 
   // Looks strange but compiles fine and initializes all fields to 0/null. 
   this = new SomeValType();  
 
   m_x = x; // Overwrite m_x's 0 with x 
   // Notice that m_y was initialized to 0.  
}

上面是对值类型的全部字段进行赋值的一个替代方案:在值类型的构造器中,this代表值类型本身的一个实例,用new创建的值类型的一个实例可以赋给this。在new的过程中,会将所有字段置为零。而在引用类型的构造器中,this被认为是只读的,所以不能对它进行赋值。

8.3 类型构造器

除了实例构造器,CLR还支持类型构造器(type constructor),也称为静态构造器(static constructor)类构造器(class constructor)或者类型初始化器(type initializer),类型构造器可应用于接口(虽然C#编译器不允许)、引用类型和值类型。实例构造器的作用是设置类型的实例的初始状态。对应地,类型构造器的作用是设置类型的初始状态。类型默认没有定义类型构造器。如果定义,也只能定义一个。此外,类型构造器永远没有参数。

internal sealed class SomeRefType {   
   static SomeRefType() {   
      // This executes the first time a SomeRefType is accessed.  
   }  
}  
  
internal struct SomeValType {  
   // C# does allow value types to define parameterless type constructors.  
   static SomeValType() {   
      // This executes the first time a SomeValType is accessed.  
   }  
}

可以看出,定义类型构造器类似于定义无参实例构造器,区别在于必须标记为static。此外,类型构造器总是私有;C#自动把它们标记为private。事实上,如果在源代码中显式将类型构造器标记为private(或其他访问修饰符),C#编译器会显示以下消息:error CS0515:静态构造函数中不允许出现访问修饰符。之所以必须私有,是为了防止任何由开发人员写的代码调用它,对它的调用总是由CLR负责。
类型构造器的调用比较麻烦。JIT编译器在编译一个方法时,会查看代码中都引用了哪些类型。任何一个类型定义了类型构造器,JIT编译器都会检查针对当前AppDomain,是否已经执行了这个类型构造器。如果构造器从未执行,JIT编译器会在它生成的本机(native)代码中添加对类型构造器的调用。如果类型构造器已经执行,JIT编译器就不添加对它的调用,因为它知道类型已经初始化好了。
现在,当方法被JIT编译完毕之后,线程开始执行它,最终会执行到调用类型构造器的代码。事实上,多个线程可能同时执行相同的方法。CLR希望确保在每个AppDomain中,一个类型构造器只执行一次。为了保证这一点,在调用类型构造器时,调用线程要获取一个互斥线程同步锁。这样一来,如果多个线程试图同时调用某个类型的静态构造器,只有一个线程才可以获得锁,其他线程会被阻塞(blocked)。第一个线程会执行静态构造器中的代码。当第一个线程离开构造器后,正在等待的线程将被唤醒,然后发现构造器的代码已被执行过。因此,这些线程不会再次执行代码,将直接从构造器方法返回。除此之外,如果再次调用这样的一个方法,CLR知道类型构造器已被执行过,从而确保构造器不被再次调用。
重要提示

虽然能在值类型中定义类型构造器,但永远都不要真的那么做,因为CLR有时不会调用值类型的静态类型构造器。

internal struct SomeValType {  
   static SomeValType() {   
      Console.WriteLine("This never gets displayed");  
   }  
   public Int32 m_x;   
}  
  
public sealed class Program {  
   public static void Main() {   
      SomeValType[] a = new SomeValType[10];  
      a[0].m_x = 123;  
      Console.WriteLine(a[0].m_x);   // Displays 123   
   }  
}

注意

由于CLR保证一个类型构造器在每个AppDomain中只执行一次,而且(这种执行)是线程安全的,所以非常适合在类型构造器中初始化类型需要的任何单实例(Singleton)对象"。

类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些字段。和实例字段一样,C#提供了一个简单的语法来初始化类型的静态字段:

internal sealed class SomeType {   
   private static Int32 s_x = 5;   
}

生成上述代码时,编译器自动为SomeType生成一个类型构造器,好像源代码本来是这样写的:

internal sealed class SomeType {   
   private static Int32 s_x;  
   static SomeType() { s_x = 5; }  
}

使用ILDasm.exe查看类型构造器的IL,很容易验证编译器实际生成的东西。类型构造器)法总是叫.cctor(代表class constructor

类型构造器不应调用基类型的类型构造器。这种调用之所以没必要,是因为类型不可能有静态字段是从基类型分享或继承的。

8.4 操作符重载方法

有的语言允许类型定义操作符应该如何操作类型的实例。例如,许多类型(比如System.String)都重载了相等(==)和不等(!=)操作符。CLR对操作符重载一无所知,它甚至不知道什么是操作符。是编程语言定义了每个操作符的含义,以及当这些特殊符号出现时,应该生成什么样的代码。
CLR规范要求操作符重载方法必须是publicstatic方法。另外,C#(以及其他许多语言)要求操作符重载方法至少有一个参数的类型与当前定义这个方法的类型相同。之所以要进行这样的限制,是为了使C#编译器能在合理的时间内找到要绑定的操作符方法。

public sealed class Complex {  
   public static Complex operator+(Complex c1, Complex c2) { ... }  
}

编译器为名为op_Addition的方法生成元数据方法定义项;这个方法定义项还设置了specialname标志,表明这是一个“特殊”方法。编程语言的编译器(包括C#编译器)看到源
·代码中出现一个+操作符时,会检查是否有一个操作数的类型定义了名为op_Addition的specialname方法,而且该方法的参数兼容于操作数的类型。如果存在这样的方法,编译器就生成调用它的代码。不存在这样的方法就报告编译错误。

TABLE 8-1   C# Unary  Operators and Their CLS-Compliant Method Names

C# Operator Symbol

Special Method Name

Suggested CLS-Compliant Method Name

+

op_UnaryPlus

Plus

­

op_UnaryNegation

Negate

!

op_LogicalNot

Not

~

op_OnesComplement

OnesComplement

++

op_Increment

Increment

­­

op_Decrement

Decrement

 (none)

op_True

IsTrue { get; }

 (none)

op_False

IsFalse { get; }

TABLE 8-2   C# Binary Operators and Their CLS-Compliant Method Names

C# Operator Symbol

Special Method Name

Suggested CLS-Compliant Method Name

+

op_Addition

Add

­

op_Subtraction

Subtract

*

op_Multiply

Multiply

/

op_Division

Divide

%

op_Modulus

Mod

&

op_BitwiseAnd

BitwiseAnd

|

op_BitwiseOr

BitwiseOr

^

op_ExclusiveOr

Xor

<< 

op_LeftShift

LeftShift

>> 

op_RightShift

RightShift

==

op_Equality

Equals

!=

op_Inequality

Equals

op_LessThan

Compare

op_GreaterThan

Compare

<=

op_LessThanOrEqual

Compare

>=

op_GreaterThanOrEqual

Compare

注意 检查Framework 类库(FCL)的核心数值类型(Int32,Int64和UInt32等),会发现它们没有定义任何操作符重载方法。之所以不定义,是因为编译器会(在代码中)专门查找针对这些基元类型执行的操作(运算),并生成直接操作这些类型的实例的IL指令。如果类型要提供方法,而且编译器要生成代码来调用这些方法,方法调用就会产生额外的运行时开销。另外,方法最终都要执行一些IL指令来完成你希望的操作。这正是核心FCL类型没有定义任何操作符重载方法的原因。对于开发人员,这意味着假如选择的编程语言不支持其中的某个FCL类型,便不能该类型的实例执行任何操个

操作符和编程语言互操作性
操作符重载是很有用的工具,允许开发人员用简洁的代码表达自己的想法。但并不是所有编程语言都支持操作符重载。使用不支持操作符重载的语言时,语言不知道如何解释+操作符(除非类型是该语言的基元类型),编译器会报错。使用不支持操作符重载的编程语言时,语言应该允许你直接调用希望的op_*方法(例如op_Addition
如果语言不支持在类型中定义+操作符重载,这个类型仍然可能提供了一个op-Addition方法。在这种情况下,可不可以在C#中使用+操作符来调用这个opAddition方法呢?答案是否定的。C#编译器检测到操作符+时,会查找关联了specialname元数据标志的op-Addition方法,以确定op_Addition方法是要作为操作符重载方法使用。但由于现在这个 op-Addition方法是由不支持操作符重载的编程语言生成的,所以方法没有关联specialname标记。因此,C#编译器会报告编译错误。当然,用任何编程语言写的代码都可以显式调用碰巧命名为op-Addition的方法,但编译器不会将一个+号的使用翻译成对这个方法的调用。

8.5 转换操作符方法

转换操作符是将对象从一种类型转换成另一种类型的方法。可以使用特殊的语法来定义转换操作符方法。CLR规范要求转换操作符重载方法必须是public和static方法。此外,C#(以及其他许多语言)要求参数类型和返回类型二者必有其一与定义转换方法的类型相同。之所以要进行这个限制,是为了使C#编译器能在一个合理的时间内找到要绑定的操作符方法。

public sealed class Rational {   
   // Constructs a Rational from an Int32  
   public Rational(Int32 num) { ... }   
  
   // Constructs a Rational from a Single  
   public Rational(Single num) { ... }  
  
   // Converts a Rational to an Int32   
   public Int32 ToInt32() { ... }  
  
   // Converts a Rational to a Single   
   public Single ToSingle() { ... } 
   
   // Implicitly constructs and returns a Rational from an Int32 
   public static implicit operator Rational(Int32 num) {    
      return new Rational(num);    
   }  
  
   // Implicitly constructs and returns a Rational from a Single 
   public static implicit operator Rational(Single num) {   
      return new Rational(num);  
   }  
  
   // Explicitly returns an Int32 from a Rational  
   public static explicit operator Int32(Rational r) {    
      return r.ToInt32();   
   }  
  
   // Explicitly returns a Single from a Rational  
   public static explicit operator Single(Rational r) {   
      return r.ToSingle();  
   }  
}

在C#中,implicit关键字告诉编译器为了生成代码来调用方法,不需要在源代码中进行显式转型。相反,explicit关键字告诉编译器只有在发现了显式转型时,才调用方法。

implicitexplicit关键字之后,要指定operator关键字告诉编译器该方法是一个转换操作符。在operator之后,指定对象要转换成什么类型。在圆括号内,则指定要从什么类型转换。

public sealed class Program {  
   public static void Main() {   
      Rational r1 = 5;         // Implicit cast from Int32  to Rational  
      Rational r2 = 2.5F;      // Implicit cast from Single to Rational  
  
      Int32  x = (Int32)  r1;  // Explicit cast from Rational to Int32   
      Single s = (Single) r2;  // Explicit cast from Rational to Single  
   }  
}

在幕后,C#编译器检测到代码中的转型,并内部生成IL代码来调用Rational类型定义的转换操作符方法。现在的问题是,这些方法的名称是什么?编译Rational类型并查看元数据,会发现编译器为定义的每个转换操作符都生成了一个方法。Rational类型的4个转换操作符方法的元数据如下:

public static Rational op_Implicit(Int32 num)   
public static Rational op_Implicit(Single num)  
public static Int32    op_Explicit(Rational r)  
public static Single   op_Explicit(Rational r)

可以看出,将对象从一种类型转换成另一种类型的方法总是叫做op_Implicit或者op_Explicit。只有在转换不损失精度或数量级的前提下(比如将个Int32转换成Rational),才能定义隐式转换操作符。如果转换会造成精度或数量级的损失(比如将Rational转换成Int32),就应该定义一个显式转换操作符。显式转换失败,应该让显式转换操作符方法抛出OverflowException或者InvalidOperationException异常。

C#编译器提供了对转换操作符的完全支持。如果检测到代码中正在使用某个类型的对象,但实际期望的是另一种类型的对象,编译器就会查找能执行这种转换的隐式转换操作符方法,并生成代码来调用该方法。如果存在隐式转换操作符方法,编译器会在结果1L代码中生成对它的调用。如果编译器看到源代码是将对象从一种类型显式转换为另一种类型,就会查找能执行这种转换的隐式或显式转换操作符方法。如果找到一个,编译器就生成1L代码来调用它。如果没有找到合适的转换操作符方法,就报错并停止编译。

为了真正理解操作符重载方法和转换操作符方法,强烈建议将System.Decimal类型作为典型来研究。Decimal定义了几个构造器,允许将对象从各种类型转换为Decimal,还定义了几个Toxxx方法,允许将Decimal转换成其他类型。最后,Decimal类型还定义了几个转换操作符方法和操作符重载方法。

8.6扩展方法

public static class StringBuilderExtensions { 
   public static Int32 IndexOf(StringBuilder sb, Char value) { 
      for (Int32 index = 0; index < sb.Length; index++) 
         if (sb[index] == value) return index;  
      return  ­1; 
   } 
}
StringBuilder sb = new StringBuilder("Hello. My name is Jeff.");   // The initial string  
 
// Change period to exclamation and get # characters in 1st sentence (5).  
Int32 index = StringBuilderExtensions.IndexOf(sb.Replace('.', '!'), '!');
// First, change period to exclamation mark  
sb.Replace('.', '!');  
 
// Now, get # characters in 1st sentence (5) 
Int32 index = StringBuilderExtensions.IndexOf(sb, '!');

但这两个版本都存在另一个不容忽视的问题,它影响了我们对代码行为的理解。使用StringBuilderExtensions显得“小题大做”,造成程序员无法专注于当前要执行的操作:IndexOf,如果StringBuilder类定义了自己的Indexor方法,上述代码就可以重写为:

// Change period to exclamation and get # characters in 1st sentence (5).  
Int32 index = sb.Replace('.', '!').IndexOf('!');

有了这个例子作为铺垫,就很容易理解C扩展方法的意义了。它允许定义一个静态方法,并用实例方法的语法来调用。换言之,现在既能定义自己的IndexOr方法,又能避免上述三个问题。要将Indexor方法转变成扩展方法,只需在第一个参数前添加this关键字:

public static class StringBuilderExtensions { 
   public static Int32 IndexOf(this StringBuilder sb, Char value) { 
      for (Int32 index = 0; index < sb.Length; index++) 
         if (sb[index] == value) return index;  
      return  ­1; 
   } 
}

还没有说第一个问题是如何解决的:程序员怎么知道有这样的一个IndexOf方法,可以用它操作StringBuilder对象呢?这个问题是通过Microsoft Visual Studio的“智能感知”功能来解决的。在编辑器中输入句点符号,会弹出Visual Studio的“智能感知"窗口,列出当前可用的实例方法。现在,这个窗口还会列出可作用于句点左侧表达式类型的扩展方法。Visual Studio的“智能感知”窗口;扩展方法的图标中有一个下箭头,方法旁边的“工具提示”表明该方法实际是一个扩展方法。

8.6.1 规则和原则

  • C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等。
  • 扩展方法(第一个参数前面有this的方法)必须在非泛型的静态类中声明。然而,类名没有限制,可以随便叫什么名字。当然,扩展方法至少要有一个参数,而且只有第一个参数能用this关键字标记。
  • C#编译器在静态类中查找扩展方法时,要求静态类本身必须具有文件作用域(*类要具有整个文件的作用域,而不能嵌套在某个类中而只具有该类的作用域)。如果静态类嵌套在另一个类中,C#编译器显示以下消息:error CS1109:扩展方法必须在顶级静态类中定义;StringBuilderExtensions是嵌套类。
  • 由于静态类可以取任何名字,所以C#编译器要花一定时间来寻找扩展方法,它必须检查文件作用域中的所有静态类,并扫描它们的所有静态方法来查找一个匹配。为增强性能,并避免找到非你所愿的扩展方法,C#编译器要求“导入”扩展方法。例如,如果有人在Wintellect命名空间中定义了一个StringBuilderExtensions类,那么程序员为了访问这个类的扩展方法,必须在他的源代码文件顶部写一条using Wintellect;指令。
  • 多个静态类可以定义相同的扩展方法。如果编译器检测到存在两个或多个扩展方法,就会显示以下消息:error CS0121:在以下方法或属性之间的调用不明确:"StringBuilderExtensions.IndexOf(string,char)"和"AnotherStringBuilderExtensions.IndexOf(string,char)"。修正这个错误必须修改源代码。具体地说,不能再用实例方法语法来调用这个静态方法。相反,必须使用静态方法语法。换言之,必须显式指定静态类的名称,明确告诉编译器要调用哪个方法。
  • 使用这个功能须谨慎,一个原因是并非所有程序员都熟悉它。例如,用一个扩展方法扩展一个类型时,同时也扩展了派生类型。所以,不要将System.Object作为扩展方法的第一个参数,否则这个方法在所有表达式类型上都能调用,造成Visual Studio的“智能感知”窗口被填充太多垃圾信息。
  • 扩展方法可能存在版本控制问题。如果Microsoft未来为他们的StringBuilder类添加了IndexOf实例方法,而且和我的代码调用的原型一样,那么在重新编译我的代码时编译器会绑定到Microsoft的IndexOf实例方法,而不是我的静态IndexOf方法。这样我的程序就会有不同的行为。版本控制问题是使用扩展方法须谨慎的另一个原因。

8.6.2 用扩展方法扩展各种类型

由于扩展方法实际是对一个静态方法的调用,所以CLR不会生成代码对调用方法的表达式的值进行null值检查(不保证它非空):

// sb is null  
StringBuilder sb = null; 

// Calling extension method: NullReferenceException will NOT be thrown when calling IndexOf  
// NullReferenceException will be thrown inside IndexOf’s for loop  
sb.IndexOf('X');  
 
// Calling instance method: NullReferenceException WILL be thrown when calling Replace  
sb.Replace('.', '!');

还要注意,可以为接口类型定义扩展方法,如下所示:

public static void ShowItems<T>(this IEnumerable<T> collection) { 
   foreach (var item in collection)   
      Console.WriteLine(item); 
}

还可为委托类型定义扩展方法,如下所示:

public static void InvokeAndCatch<TException>(this Action<Object> d, Object o)  
   where TException : Exception {  
   try { d(o); }  
   catch (TException) { } 
}

最后,C#编译器允许创建委托(参见第17章“委托”)来引用一个对象上的扩展方法:

public static void Main () {  
   // Create an Action delegate that refers to the static ShowItems extension method 
   // and has the first argument initialized to reference the "Jeff" string.  
   Action a = "Jeff".ShowItems;  
   . 
   . 
   . 
   // Invoke the delegate that calls ShowItems passing it a reference to the "Jeff" string.  
   a(); 
}

在上述代码中,C#编译器生成IL代码来构造一个Action委托。创建委托时,会向构造器传递应调用的方法,同时传递一个对象引用,这个引用应传给方法的隐藏this参数。正常情况下,创建引用静态方法的委托时,对象引用是null,因为静态方法没有this参数。但在这个例子中,C#编译器生成特殊代码创建一个委托来引用静态方法(ShowItems),而静态方法的目标对象是对"Jefr字符串的引用。稍后,当这个委托被调用(invoke)时,CLR会调用(call)静态方法,并向其传递对"Jeff"字符串的引用。这是编译器耍的小“花招”,但效果不错,而且只要你不去细想内部发生的事情,整个过程还是感觉非常自然的。

8.6.3 ExtensionAttribute类

在C#中,一旦用this关键字标记了某个静态方法的第一个参数,编译器就会在内部向该方法应用一个定制特性。该特性会在最终生成的文件的元数据中持久性地存储下来。该特性在System.Core.dl程序集中定义。

// Defined in the System.Runtime.Compile rServic es  namespace 
[Attri buteUsage(AttributeTarget s.Method | AttributeTargets.Class | AttributeTarge ts. Assembly)] 
public sealed class ExtensionAttribute : Attribute { 
}

除此之外,任何静态类只要包含至少一个扩展方法,它的元数据中也会应用这个特性。类似地,任何程序集只要包含了至少一个符合上述特点的静态类,它的元数据中也会应用这个特性。这样一来,如果代码调用了一个不存在的实例方法,编译器就能快速扫描引用的所有程序集,判断它们哪些包含了扩展方法。然后,在这些程序集中,可以只扫描包含了扩展方法的静态类。在每个这样的静态类中,可以只扫描扩展方法来查找匹配。利用这个技术,代码能以最快速度编译完毕。

8.7 分部方法

假定用某个工具生成了包含类型定义的C#源代码文件,工具知道你想在代码的某些位置定制类型的行为。正常情况下,是让工具生成的代码调用虚方法来进行定制。工具生成的代码还必须包含虚方法的定义。另外,这些方法的实现是什么事情都不做,直接返回了事。现在,如果想定制类的行为,就必须从基类派生并定义自己的类,重写虚方法来实现自己想要的行为。

// Tool­produced code in some source code file: 
internal class Base {  
   private String m_name; 
 
   // Called before changing the m_name field 
   protected virtual void OnNameChanging(String value) {  
   } 
 
   public String Name {  
      get { return m_name; }  
      set {  
         OnNameChanging(value.ToUpper());  // Inform class of potential change  
         m_name = value;                   // Change the field 
      } 
   } 
} 
 
 
// Developer­produced code in some other source code file: 
internal class Derived : Base {  
   protected override void OnNameChanging(string value) { 
      if (String.IsNullOrEmpty(value))  
         throw new ArgumentNullException("value"); 
   } 
}

遗憾的是,上述代码存在两个问题。

  • 类型必须是非密封的类。这个技术不能用于密封类,也不能用于值类型(值类型隐式密封)。此外,这个技术不能用于静态方法,因为静态方法不能重写。
  • 效率问题。定义一个类型只是为了重写一个方法,这会浪费少量系统资源。另外,即使不想重写OnNameChanging的行为,基类代码仍需调用一个什么都不做、直接就返回的虚方法。另外,无论OnNameChanging是否访问传给它的实参,编译器都会生成对ToUpper进行调用的IL代码。

利用C#的分部方法功能,可以在解决上述问题的同时覆盖类的行为。以下代码使用分部方法实现和上述代码完全一样的语义:

// Tool­produced code in some source code file: 
internal sealed partial class Base {  
   private String m_name; 
 
   // This defining­partial­method ­declaration is called before changing the m_name field 
   partial void OnNameChanging(String value); 
 
   public String Name {  
      get { return m_name; }  
      set {  
         OnNameChanging(value.ToUpper());  // Inform class of potential change  
         m_name = value;                   // Change the field 
      } 
   } 
} 
 
// Developer­produced code in some other source code file: 
internal sealed partial class Base {  
 
   // This implementing ­partial­method ­declaration is called before m_name is changed  
   partial void OnNameChanging(String value) {  
      if (String.IsNullOrEmpty(value))  
         throw new ArgumentNullException("value"); 
   } 
}

这个新版本要注意以下几个问题。

  • 类现在密封(虽然并非一定如此)。事实上,类可以是静态类,甚至可以是值类型。
  • 工具和开发者所生成的代码真的是一个类型定义的两个部分。要更多地了解分部类型,请参见6.5节“分部类、结构和接口”。
  • 工具生成的代码包含分部方法的声明。要用partial关键字标记,无主体。
  • 开发者生成的代码实现这个声明。该方法也要用partial关键字标记,有主体。

编译上述代码后,可以获得和原始代码一样的效果。现在的好处在于,可以重新运行工具,在新的源代码文件中生成新的代码,但你自己的代码是存储在一个单独的文件中的,不会受到影响。另外,这个技术可用于密封类、静态类以及值类型

但是,分部方法还提供了另一个巨大的提升。如果不想修改工具生成的类型的行为,那么根本不需要提供自己的源代码文件。如果只是对工具生成的代码进行编译,编译器会改变生成的IL代码和元数据,使工具生成的代码看起来变成下面这样:

// Logical equivalent of tool­produced code if there is no  
// implementing partial method declaration:  
internal sealed class Base {  
   private String m_name; 
 
   public String Name {  
      get { return m_name; }  
      set {  
         m_name = value;                  // Change the field 
      } 
   } 
}

也就是说,如果没有实现分部方法,编译器不会生成任何代表分部方法的元数据。此外,编译器不会生成任何调用分部方法的IL指令。而且,编译器不会生成对本该传给分部方法的实参进行求值的IL指令。在这个例子中,编译器不会生成调用ToUpper方法的代码。结果就是更少的元数据/L,运行时的性能得到了提升。

规则和原则

  • 它们只能在分部类或结构中声明
  • 分部方法的返回类型始终是void,任何参数都不能用out修饰符来标记。之所以有这两个限制,是因为方法在运行时可能不存在,所以不能将变量初始化为方法也许会返回的东西。类似地,不允许out参数是因为方法必须初始化它,而方法可能不存在。分部方法可以有ref参数,可以是泛型方法,可以是实例或静态方法,而且可标记为unsafe.
  • 当然,分部方法的声明和实现必须具有完全一致的签名。如果两者都应用了定制特性,编译器会合并两个方法的特性。应用于参数的任何特性也会合并。
  • 如果没有对应的实现部分,便不能在代码中创建一个委托来引用这个分部方法。这同样是由于方法在运行时不存在。编译器报告以下消息:error CS0762:无法通过方法"Base.OnNameChanging(string)"创建委托,因为该方法是没有实现声明的分部方法。
  • 分部方法总是被视为private方法,但C#编译器禁止在分部方法声明之前添加private关键字。
posted @ 2019-12-06 21:54  FH1004322  阅读(154)  评论(0)    收藏  举报