代码改变世界

CLR via C# 边读边想 08 - 方法

2012-07-04 23:07  richardzhaoxb  阅读(148)  评论(0编辑  收藏  举报

Instance Constructors and Classes (Reference Types)

不像其他的方法,实例构造函数是从来不会被继承的,类只有自己的构造函数。由于它是不能被继承的,所以我们不能用这些关键字来修饰构造函数:virtual, new, override, sealed, abstract。如果你没有为类显示的定义一个构造函数,那编译器就会自动的生成一个无参的构造函数,这个默认构造函数的实现就是很简单的调用基类的构造函数。如果你定义了下面这个类:

public class SomeType {
}

那么编译器会生成类似:

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

默认产生的构造函数是 public 的。如果类型是抽象的,那编译器生成的默认构造函数是带 protected 访问控制符的。

如果基类没有提供无参的构造函数,那派生类必须显示的调用基类的构造函数,否则编译器会报错。

对于静态类,编译器不会生成默认的构造函数。

为了代码的正确性,在一个类的实例构造函数中,在使用继承的成员之前,必须先调用基类的的某个构造函数,就算你没有调用基类的构造函数,编译器也会自动生成一条语句去call基类的默认构造函数。最终,System.Object 的 无参构造函数将会被调用,这个构造函数什么事情都不做,直接 return。因为 Object 类没有定义任何实例成员,所以不需要做什么。

有几种特殊的情况,创建一个实例时不会调用构造函数:

  • 调用 Object 的 MemberwiseClone 方法创建新实例。
  • 通过 runtime serializer 反序列化得到的实例。

注意:避免在构造函数中调用任何虚方法,因为它的实现是在继承树中被重写,runtime 调用时,虚方法还不一定被实现了。

C# 提供了一种简易的初始化引用类型中字段的语法:

internal sealed class SomeType {
    private Int32 m_x = 5;
}

当一个 SomeType 对象别实例化时,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

查看 IL 代码可以发现,在SomeType的构造函数中,有一行代码就是给 m_x 赋值为5,再去调用基类的构造函数。它会在每一个构造函数前都加上这么一句。

虽然有这个语法糖,如果像这种 in-line 初始化的字段较多,产生的 IL 会增大。所以比较好的做法是,把这些字段的初始化放在默认构造函数中,然后在其他构造函数中先调用这个默认构造函数。

 1 internal sealed class SomeType {
 2     // Do not explicitly initialize the fields here
 3     private Int32 m_x;
 4     private String m_s;
 5     private Double m_d;
 6     private Byte m_b;
 7     // This constructor sets all fields to their default.
 8     // All of the other constructors explicitly invoke this constructor.
 9     public SomeType() {
10         m_x = 5;
11         m_s = "Hi there";
12         m_d = 3.14159;
13         m_b = 0xff;
14     }
15     // This constructor sets all fields to their default, then changes m_x.
16     public SomeType(Int32 x) : this() {
17         m_x = x;
18     }
19     // This constructor sets all fields to their default, then changes m_s.
20     public SomeType(String s) : this() {
21         m_s = s;
22     }
23     // This constructor sets all fields to their default, then changes m_x & m_s.
24     public SomeType(Int32 x, String s) : this() {
25         m_x = x;
26         m_s = s;
27     }
28 }

 

Instance Constructors and Structures (Value Types)

值类型的构造函数的工作方式和引用类型的完全不同,值类型的在申明变量时,CLR 已经把他的值设置为0。当然你也可以给他定义构造函数。 

 1 internal struct Point {
 2     public Int32 m_x, m_y;
 3     public Point(Int32 x, Int32 y) {
 4         m_x = x;
 5         m_y = y;
 6     }
 7 }
 8 
 9 internal sealed class Rectangle {
10     public Point m_topLeft, m_bottomRight;
11     public Rectangle() {
12         // In C#, new on a value type calls the constructor to
13         // initialize the value type's fields.
14         m_topLeft = new Point(1, 2);
15         m_bottomRight = new Point(100, 200);
16     }
17 }

只有显示的调用了构造函数,构造函数才会执行,值类型也不会生成默认的无参构造函数,但是 CLR 允许你可自己定义默认的无参构造函数。 可是就算你定义了默认的无参构造函数,也不会被隐式的调用,你必须显示的调用才会执行。

但是C#的编译器有些不同,它根本就不允许给 structure 定义默认的无参构造函数,编译器会报错。 同样也不支持 in-line 的初始化字段的写法:

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

C#编译器还有一个检查,如果你定义了一个构造函数,那么你必须在构造函数中对所有的字段都初始化,比如下面的代码就会出错:

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

 

Type Constructors

除了我们常见的实例构造函数,CLR 也允许我们定义类型构造函数,也称静态构造函数。默认情况下,类型是没有类型构造函数的,而且最多只能有一个类型构造函数,而且是无参构造函数。下面分别是引用类型和值类型的类型构造函数。

 1 internal sealed class SomeRefType {
 2     static SomeRefType() {
 3         // This executes the first time a SomeRefType is accessed.
 4     }
 5 }
 6 
 7 internal struct SomeValType {
 8     // C# does allow value types to define parameterless type constructors.
 9     static SomeValType() {
10         // This executes the first time a SomeValType is accessed.
11     }
12 }

和实例构造函数不同的是,类型构造函数都是 private 的,而且是 C# 编译器自动加上的,如果你自己加上 private 修饰符,反而会报错。

对于值类型来说,就算你定义了类型构造函数,它们也永远不会被执行。

对于引用类型来说,是 CLR 第一次要用到这个类型时,就会先执行这个引用类型的类型构造函数,后面再用到这个引用类型时就不会再调用类型构造函数。

类型构造函数也是线程安全的,如果有多个线程要用到同一个类型,其中一个线程会排他的(mutually exclusive)占用进程同步锁(thread synchronization lock),其他进程则会等待它执行完后,等它把类型构造函数跑完后,其他线程执行这个构造函数时就会直接返回。所以说,类型构造函数是一个理想的地方去获取一些单例的对象。

类型构造函数主要用来初始化一些静态的成员,它不能访问实例的成员,同样我们也可以用 in-line 的方式来初始化静态成员变量。

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

最后,我们来看看既有类型构造函数又有 in-line 初始化,是什么效果:

1 internal sealed class SomeType {
2     private static Int32 s_x = 5;
3 
4     static SomeType() {
5         s_x = 10;
6     }
7 }

s_x 最终的值是 10。

注意:CLR 不支持 静态的 Finalize 方法,当一个类型从内存中 unload 时,一般是 AppDomain 被 unload时,如果你想在这时做些事情的话,只能借助 System.AppDomain 的 DomainUnload 事件。

 

Operator Overload Methods 

其实对于 CLR 来说,根本就不知道操作符重载方法,它甚至不认识这些 >, = 等操作符。

CLR 规定 操作符重载方法必须是 publiic 和 static 的。

C# 编译器还要求,操作符重载方法的参数中至少要有一个参数的类型是当前这个类型。

下面是一个加号的方法定义:

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

这个方法对应到编译完的代码中是 op_Addition 方法。下面两个表列出了C#支持的操作符重载的符号,和相应的 CLS 方法名。

微软建议当我们定义操作符重载方法时,最好也提供一个相应的正常名字的方法,而这个正常名字方法的实现就是调用这个操作符运算,而上表中的第三列就是列出了这些方法的推荐命名。那么之前的例子应该改进为:

1 public sealed class Complex {
2     public static Complex operator+(Complex c1, Complex c2) { ... }
3     public static Complex Add(Complex c1, Complex c2) { return(c1 + c2); }
4 }

 

Conversion Operator Methods

类型转换重载方法必须是 public 和 static 的,C# 编译器还要求 类型转换重载方法 的参数或返回值之一必须是当前这个类型。下面以Rational 类型为例子:

 1 public sealed class Rational {
 2     // Constructs a Rational from an Int32
 3     public Rational(Int32 num) { ... }
 4     // Constructs a Rational from a Single
 5     public Rational(Single num) { ... }
 6     // Convert a Rational to an Int32
 7     public Int32 ToInt32() { ... }
 8     // Convert a Rational to a Single
 9     public Single ToSingle() { ... }
10     // Implicitly constructs and returns a Rational from an Int32
11     public static implicit operator Rational(Int32 num) {
12         return new Rational(num);
13     }
14     // Implicitly constructs and returns a Rational from a Single
15     public static implicit operator Rational(Single num) {
16         return new Rational(num);
17     }
18     // Explicitly returns an Int32 from a Rational
19     public static explicit operator Int32(Rational r) {
20         return r.ToInt32();
21     }
22     // Explicitly returns a Single from a Rational
23     public static explicit operator Single(Rational r) {
24         return r.ToSingle();
25     }
26 }

Implicit 和 Explicit 的区别可以从下面的调用语句的不同看出来:

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

那么在 IL 代码中这些 方法会被编译为:

1 public static Rational op_Implicit(Int32 num)
2 public static Rational op_Implicit(Single num)
3 public static Int32 op_Explicit(Rational r)
4 public static Single op_Explicit(Rational r)

 

如果你想更好的理解操作符重载方法和类型转换重载方法,你可以看看 FCL 中的 System.Decimal 类型的定义,这是一个很好很完整的例子。

 

Extension Methods

扩展方法允许你给一个穿在的类型定义静态方法,让后通过这个类型的实例调用这个静态扩展方法。要定义扩展方法,第一个参数必须是这个类型本身,并在前面加上 this 关键字。例如:

1 public static class StringBuilderExtensions {
2     public static Int32 IndexOf(this StringBuilder sb, Char value) {
3         for (Int32 index = 0; index < sb.Length; index++)
4             if (sb[index] == value) return index;
5         return -1;
6     }
7 }

使用:

StringBuilder sb = new StringBuilder("Hello. My name is Jeff."); // The initial string
Int32 index = sb.IndexOf('X');

使用扩展方法时要注意下面几点:

  • C# 只支持扩展方法,不支持扩展属性,扩展事件,扩展操作符。。等。
  • 扩展方法要放在非泛型的,静态类中定义。 而且这个类不是嵌套类,必须是最外层的类定义。
  • 谨慎的使用扩展方法,尽量用继承来做。

 

Extending Various Types with Extension Methods

可以给 Interface 定义扩展方法。。。。

可以给 delegate 类定义扩展方法。。。。

 

The Extension Attribute

其实在对扩展方法编译后的代码中,是用了一个属性来标示扩展方法的:

// Defined in the System.Runtime.CompilerServices namespace
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class ExtensionAttribute : Attribute {}

那么当 CLR 没发现有某个实例方法时,会找所有 ExtensionAttribute 的方法。

 

Partial Methods

假如我们用一个开发工具生成了一个类,我们需要重写部分方法,就继承了一个派生类,并重写了其中一个方法:

 1 // Tool-produced code in some source code file:
 2 internal class Base {
 3     private String m_name;
 4     // Called before changing the m_name field
 5         protected virtual void OnNameChanging(String value) {
 6     }
 7     public String Name {
 8         get { return m_name; }
 9         set {
10             OnNameChanging(value.ToUpper()); // Inform class of potential change
11             m_name = value; // Change the field
12         }
13     }
14 }
15 
16 // Developer-produced code in some other source code file:
17 internal class Derived : Base {
18     protected override void OnNameChanging(string value) {
19         if (String.IsNullOrEmpty(value))
20             throw new ArgumentNullException("value");
21     }
22 }

但是这有两个限制:

  • 不能应用于 sealed 类型 和 值类型, 不能支持静态方法。
  • 定义一个类仅仅只是为了重写一个方法。

C# 支持的部分方法可以帮我们解决这个问题:

 1 // Tool-produced code in some source code file:
 2 internal sealed partial class Base {
 3     private String m_name;
 4     // This defining-partial-method-declaration is called before changing the m_name field
 5     partial void OnNameChanging(String value);
 6     public String Name {
 7         get { return m_name; }
 8         set {
 9             OnNameChanging(value.ToUpper()); // Inform class of potential change
10             m_name = value; // Change the field
11         }
12     }
13 }
14 
15 // Developer-produced code in some other source code file:
16 internal sealed partial class Base {
17     // This implementing-partial-method-declaration is called before m_name is changed
18     partial void OnNameChanging(String value) {
19         if (String.IsNullOrEmpty(value))
20             throw new ArgumentNullException("value");
21     }
22 }

和前面一个版本的实现相比,部分方法的这个实现有以下特点:

  • 这个类可以是 sealed 了。
  • 其实机器产生的部分和开发人员写的部分一般是分布在两个文件中的,定义在部分类中,当机器重新产生代码时,不会影响开发人员写的代码。
  • 方法前面要加上 partial 关键字。

定义部分方法时,有一些规则和规范值得注意:

  • 只可以在类和结构中定义部分方法。
  • 返回值只能是 void 的,不能有 outer 的参数。
  • 部分方法都是 private 的,但是 C# 编译器却不允许你在方法前面加上 private 修饰符,它会自己加上。