方法

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

构造器方法在方法定义元数据表中始终叫做.ctor(constructor简称).

创建引用类型的实例时

  • 首先,为实例的数据字段分配内存
  • 初始化对象的附加字段(类型对象指针同步索引块)
    • 这些附加字段称为overhead fields,创建对象时必须的开销.
  • 最后调用类型的实例构造器,来设置对象的初始状态.

构造引用类型对象时,在调用类型的实例构造器之前,为对象分配的内存总是先被归零,没有被构造器显示重写的字段都保证获得0或null值.

  • 实例构造器不能使用以下修饰符: virtual,new,override,sealed,abstract.
  • 没有显示定义任何构造器时,C#编译器将定义一个默认的无参构造器.
  • 如果是抽象类(abstract),那么编译器生成的默认构造器可访问性是protected,否则就是public.
  • 如果基类没有提供 无参构造器,那么派生类必须 显式调用一个基类构造器.
  • 如果是静态类(static),编译器不会再定义中生成默认构造器.
public class SomeType {}
// 等价于
public class SomeType
{
  public SomeType() : base() {}
}
  • 类的实例构造器在访问从基类继承的任何字段之前,都必须先调用基类的构造器.
  • 如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认的基类构造器的调用.(System.Object的无参构造器会得到调用,直接返回什么都不做)
  • 极少数情况可以在不调用构造器的前提下创建类型的实例.
    • 一个典型的例子是Object的MemberwiseClone方法.该方法的作用是分配内存,初始化对的额外开销成员,然后将源对象的字节数据复制到新对象中.(不需要构造器去初始化成员)
    • 另外运行时序列化器反序列化对象时也不需要调用构造器.反序列化代码使用System.Runtime.Serialization.FormatterServices类型的GetUninitializedObject或者GetSafeUninitializedObject方法为对象分配内存. 期间不会调用一个构造器.

不要在构造器中调用虚方法

  • 假如基类构造器中调用了虚方法,派生类重写了虚方法, 派生类被实例化时,会先调用基类的构造器,但是基类构造器中的虚方法会使用派生类重写后的虚方法,但是派生类构造器还没运行,所以会导致无法预测的行为.
  • 尚未完成对继承层次结构中所有字段的初始化.

归根到底,这是由于调用虚方法时,直到运行之前都不会选择执行该方法实际的类型.

internal sealed class SomeType
{
  // C# 编译器允许嵌入方式初始化实例字段
  // 但是在幕后,它会将语法转换成构造器方法中的代码来初始化. <----代码膨胀效应
  private Int32 m_x = 5;
}

这段代码在IL代码如下工作:

  1. SomeType的构造器把值5存储到m_x.
  2. 再调用基类构造器System.Object::.cotr().
internal sealed class SomeType
{
  // 内联方式初始化实例字段   等同于嵌入代码的形式
  // 会将此种语法转换成代码在构造器方法中来执行初始化
  // 所以有多少个构造器,都会生成多少次所有字段初始化的代码
  private Int32  m_x = 5;
  private String m_s = "Hi there";
  private Double m_d = 3.1415;
  private Byte   m_b;

  public SomeType(){...}
  public SomeType(Int32 x){...}
  // 此构造器的执行过程
  // 1. 编译器生成代码 初始化m_x,m_s和 m_d(没有显示初始化也保证会被初始化为0)
  // 2. 调用基类构造器 base()  基类的无参构造器调用
  // 3. 再调用SomeType(String s) 类型自己的构造器
  // 4. 用10覆盖掉m_d值
  public SomeType(String s){...; m_d=10;}
}

.method public hidebysig specialname rtspecialname
        instance void  .ctor(string s) cil managed
{
  // 代码大小       64 (0x40)
  .maxstack  2
  //////////////////////////////////////////////////////////////////
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.5
  IL_0002:  stfld      int32 ConsoleApp1.SomeType::m_x
  IL_0007:  ldarg.0
  IL_0008:  ldstr      "Hi there"
  IL_000d:  stfld      string ConsoleApp1.SomeType::m_s
  IL_0012:  ldarg.0
  IL_0013:  ldc.r8     3.1415000000000002
  IL_001c:  stfld      float64 ConsoleApp1.SomeType::m_d
  IL_0021:  ldarg.0
  ////////////////////以上就是每个构造器都要生成的初始化m_x,m_s和 m_d代码
  // 调用基类构造器
  IL_0022:  call       instance void [System.Runtime]System.Object::.ctor()
  IL_0027:  nop
  IL_0028:  nop
  // 调用自己的构造器
  IL_0029:  ldarg.0
  IL_002a:  ldarg.1
  IL_002b:  stfld      string ConsoleApp1.SomeType::m_s
  // m_d=10;用10覆盖掉m_d值
  IL_0030:  ldarg.0
  IL_0031:  ldc.r8     10.
  IL_003a:  stfld      float64 ConsoleApp1.SomeType::m_d
  IL_003f:  ret
} // end of method SomeType::.ctor
  1. 编译器为这3个构造器方法生成代码时,在每个方法的开始位置,都会包含用于初始化m_x,m_s和m_d的代码.
  2. 在这些初始化代码之后,编译器会插入对基类构造器的调用.
  3. 再然后,会插入构造器自己的代码.

编译器在调用基类构造器前使用简化语法对所有字段初始化.

因此上述3个构造器就要生成3次这样相同的代码(初始化m_x,m_s和 m_d代码).

优化构造器代码膨胀效应的方法

如果有几个已初始化的实例字段和许多重载构造器方法,可以创建单个构造器来执行公共的初始化. 让其他构造器都显示调用这个公共初始化构造器. 不显示初始化字段(不在定义时赋值初始化).

    public sealed class SomeType
    {
        // 不要显示初始化下面的字段,减少生成相同的代码
        // 不然会在没个构造器中生成相同的初始化代码,
        private Int32  m_x;
        private String m_s;
        private Double m_d;
        private Byte   m_b;

        public SomeType()
        {
            m_x = 5;
            m_s = "ssss";
            m_d = 3.1415;
            m_b = 0xff;
        }
        // 先调用基类System.Object的无参构造器
        // 调用自己的无参构造器初始化
        public SomeType(Int32 x) : this()
        {
            m_x = x;
        }
        // 先调用基类System.Object的无参构造器
        // 调用自己的无参构造器初始化
        // 用10覆盖m_d
        public SomeType(String s) : this()
        {
            m_s = s;
            m_d = 10;
        }
        // 先调用基类System.Object的无参构造器
        // 调用自己的无参构造器初始化
        public SomeType(Byte b) : this()
        {
            m_b = b;
        }
    }

.method public hidebysig specialname rtspecialname
        instance void  .ctor(string s) cil managed
{
  // 代码大小       31 (0x1f)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance void ConsoleApp1.SomeType::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.0
  IL_0009:  ldarg.1
  IL_000a:  stfld      string ConsoleApp1.SomeType::m_s
  IL_000f:  ldarg.0
  IL_0010:  ldc.r8     10.
  IL_0019:  stfld      float64 ConsoleApp1.SomeType::m_d
  IL_001e:  ret
} // end of method SomeType::.ctor

如果还在字段定义时赋值, 其他构造器使用:this(),则不会在这些构造器中生成字段初始化的代码,而会在无参构造器中生成. 关键在于:this()

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

值类型(struct)构造器的工作方式与引用类型(class)的构造器截然不同. CLR总是允许创建值类型的实例,并且没有办法阻止类型的实例化.

  • 值类型构造器需要显示调用才会执行.
  • 不显式调用构造器都将初始化为0或null
  • C#编译器不允许结构包含显式的无参构造器.
  • 由于不能定义无参构造器,所以编译器永远不会生成自动调用它的代码

引用类型中的字段 保证初始化为0或null, C#是能生成”可验证”代码的编译器,可以保证所以 基于栈的值类型字段 对它们进行”置零”.

关于值类型的实例构造器

  1. C#不允许 值类型带有无参构造器
  2. 也就不允许值类型使用内联实例字段的初始化语法
    1. private Int32 m_x =5; 在结构中会编译出错.
  3. 值类型的任何构造器都必须初始化值类型的全部字段. 否则编译出错.
public SomeType(Int32 x)
{
  // 值类型结构中,this代表值类型本身的一个实例, 可以赋值(引用类型this是只读的)
  // 显示调用无参值类型构造函数, 所有值类型字段初始化为0或null
  this = new SomeType();

  // 覆盖原始的0
  m_x = x;
  // m_y已经初始化,所以不会编译出错.
}

类型构造器

除了实例构造器,CLR还支持类型构造器.也称为静态构造器,类构造器,类型初始化器.

  • 类型构造器的作用是设置类型的初始状态.
    • 实例构造器作用是设置类型实例的初始状态.
  • 类型构造器永远没有参数
  • 类型构造器必须标static,且默认不可更改,访问性是private

类型默认没有定义类型构造器, 如果定义也只能定义一个.

internal sealed class SomeRefType
{
    // 类型构造器必须标static,且默认不可更改访问性是private
    static SomeRefType()
    {
        // SomeRefType被首次访问时,执行这里的代码
    }
}

/// 永远不要定义值类型类型构造器, CLR有时不会调用值类型的静态类型构造器
internal struct SomeValType
{
    // C#允许值类型定义无参的构造类型
    // 类型构造器必须标static,且默认不可更改访问性是private
    static SomeValType()
    {
        // SomeValType被首次访问时,执行这里的代码
    }
}

之所以是私有,是为了防止任何开发人员写代码去调用它,对它(类型构造器)的调用总是由CLR负责.

  1. 不要在静态构造函数中执行复杂的逻辑、它只是为了对静态字段进行初始化而设置的,并且只能访问静态字段.
  2. 不要出现两个或者多个类的静态构造函数相互调用的情况,因为它是线程安全的,是要加锁的,如果出现相互调用,可能导致死锁。
  3. 不要在类的静态构造函数中写你期望按照某个顺序执行的代码逻辑,因为静态构造函数的调用时由CLR控制的,程序员不能准确把握运行时机。
  4. 永远不要定义值类型类型构造器, CLR有时不会调用值类型的静态类型构造器
  5. 如果在类型构造器中抛出未处理的异常,CLR会认为类型不可用,访问该类型的字段或方法都会抛出System.TypeInitializationException异常.

CLR并不支持静态的Finalize方法, 就是在类型卸载时执行一些代码,类型只有在AppDomain卸载时才会卸载,AppDomain卸载时,用于标识类型的对象将成为不可达的对象,垃圾回收期会回收类型对象的内存.
要实现这样的需求,要在AppDomain卸载时执行一些代码,可向System.AppDomain类型的DomainUnload啥时间登记一个回调方法.

操作符重载方法

CLR对操作符重载一无所知.甚至不知道什么是操作符.

  • 操作符重载必须是 public static
  • 操作符重载方法的参数类型至少有一个和当前定义这个方法的类型相同.(能在合理的时间内找到要绑定的操作符重载方法)
  • 编译器会为名为op_Addition的方法生成元数据方法定义项,还会设置一个specialname标志.表明这是一个特殊的方法.
public sealed class Complex
{
  public static Complex operator+(Complex c1, Complex c2){...}
}

FCL的System.Decimal类型很好地演示了如何重载操作符并根据Microsoft的设计规范定义友好的方法名.

操作符方法的友好名称方法

public sealed class Complex
{
  // 重载操作符方法
  public static Complex operator+(Complex c1, Complex c2){...}
  // 定义友好方法,内部调用操作符重载方法
  public static Complex Add(Complex c1, Complex c2){ return c1+c2;}
}

作者认为这种额外的复杂性没必要,调用它们导致额外的性能损失.

转换操作符方法

将对象从一种类型转换为另一种类型.

  • 如果源类型和目标类型都是编译器识别的基元类型,编译器自己就知道如何生成转换对象所需的代码.
  • 如果不是,编译器会生成代码,要求CLR执行强制转型.
  • 转换操作符重载方法必须是 public static
internal sealed class Rational
{
    public Rational(Int32 num)
    {...}

    public Rational(Single num)
    {...}

    public Int32 ToInt32()
    {...}

    public Single ToSingle()
    {...}

    // implicit关键字告诉编译器为了生成代码来调用方法. 不需要在源代码中进行显示转换.
    // 由一个Int32隐式构造并返回一个Rational
    public static implicit operator Rational(Int32 num)
    {
        return new Rational(num);
    }

    // 由一个Single隐式构造并返回一个Single
    public static implicit operator Rational(Single num)
    {
        return new Rational(num);
    }

    // explicit关键字告诉编译器只有在发现了显示转型时,才调用方法.
    // 由一个Rational 显式返回一个Int32
    public static explicit operator Int32(Rational r)
    {
        return r.ToInt32();
    }

    // 由一个Rational 显式返回一个Single
    public static explicit operator Single(Rational r)
    {
        return r.ToSingle();
    }
}
  • implicit关键字
    • 该关键字告诉编译器为了生成代码来调用方法. 不需要在源代码中进行显示转换.
  • explicit关键字
    • 该关键字告诉编译器只有在发现了显示转型时,才调用方法.

关键字之后要指定operator关键字告诉编译器该方法是一个转换操作符, 在operator之后,指定对象要转换成什么类型,圆括号内指定要从什么类型转换.
public static explicit/implicit operator 目标类型(源类型 r)

定义完转换操作符之后,就可以写出像下面这样的C#代码:

Rational r1 = 5; // Int32隐式转型为Rational
Rational r2 = 2.5f;// Single隐式转型为Rational

Int32 x = (Int32) r1; // Rational 显示转型为Int32
Single s = (Single) r2;// Rational显示转型为Single

使用强制类型转换表达式时,C#生成代码来调用显示转换操作符方法, 使用C#的as或is操作符时,则永远不会调用这些方法.

要理解操作符重载和转换操作符方法. 建议用System.Decimal类型作为典型来研究.

扩展方法

理解C#扩展方法最好是从例子中学习.

允许定义一个静态方法,并用实例方法的语法来调用. 只需要在第一个参数前添加this关键字.

public static class StringBuilderExtensions
{
    public static Int32 IndexOf(this StringBuilder sb, Char value)
    {
        ....
    }
}

这样就可以通过Int32 index = sb.IndexOf('X');方式去调用.

编译这句话的过程:

  1. 首先检查StringBuilder类或者它的任何基类是否提供了参数为Char,名为IndexOf的一个实例方法.
  2. 如果有,就生成IL代码来调用它.
  3. 如果没有找到匹配的实例方法,就继续检查是否有任何静态类定义了名为IndexOf的静态方法,方法的第一个参数和类型和当前调用方法的表达式类型匹配,必须用this关键字标识.
  4. 编辑器就会找到IndexOf(this StringBuilder sb, Char value)方法,生成对应的IL代码来调用这个静态方法.

扩展方法的规则和原则

  • C#只支持扩展方法,不支持扩展属性,扩展事件,扩展操作符等等.
  • 扩展方法(第一个参数前面有this的方法)必须在 非泛型的静态类 中声明.
  • 类名没有限制.
  • 至少要有一个参数,而且第一个参数能用 this关键字标记.
  • 扩展方法必须在 顶级静态类 中定义.具有 整个文件的作用域 (不能嵌套在另一个类中而只有该类的作用域).
  • C#要求导入扩展方法所在的命名空间. 例如:在Wintellect命名空间下定义了一个扩展方法,那么别人访问这个扩展方法就需要添加using Wintellect;语句.
  • 如果多个静态类存在相同的扩展方法,就必须显示指定静态类(扩展方法所在类)的名称,明确指定.
  • 同时也会扩展派生类型.
  • 如果StringBuilder未来提供了IndexOf方法,则程序不会调用我的静态扩展方法,而是绑定微软提供的IndexOf方法.
  • 由于扩展方法实际是一个静态方法的调用.CLR不会对调用的表达式进行null值检查(不保证非空).
StringBuilder sb = null;
// 调用扩展方法: NullReferenceException异常不会在调用IndexOf时抛出
// 而会在,IndexOf内部的for循环中抛出
sb.IndexOf('x');
// 调用实例方法,NullReferenceException异常在调用Replace时抛出
sb.Replace('x','!');

为接口类型定义扩展方法

class Program
{

    static void Main(string[] args)
    {
        // 每个char在控制台上单独显示一行
        "Grant".ShowItems();

        // 每个String在控制在单独显示一行
        new[]{"Jeff","Kristin"}.ShowItems();

        // 每个Int32在控制台上单独显示一行
        new List<Int32>(){1,2,3}.ShowItems();
    }
}

static class ExClass
{
    // // 任何表达式只要它最终的类型实现了IEnumerable<T>接口,就能此调用扩展方法
    public static void ShowItems<T>(this IEnumerable<T> collection)
    {
        foreach (var item in collection)
        {
            Console.WriteLine(item);
        }
    }
}

任何表达式只要它最终的类型实现了IEnumerable<T>接口,就能调用此扩展方法.

LINQ (Language Integrated Query) 语言集成查询

想要仔细研究提供了许多扩展方法的一个典型类, System.LinQ.Enumerable及其所有静态扩展方法.这个类中每个扩展方法.

为委托类型定义扩展方法

class Program
{
    static void Main(string[] args)
    {
        // 抛出NullReferenceException
        Action<Object> action = o => Console.WriteLine(o.GetType());
        // 吞噬NullReferenceException
        action.InvokeAndCatch<NullReferenceException>(null);
    }
}

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

向枚举类型添加扩展方法

在15.3节有例子.

创建委托来引用对象上的扩展方法

Action a = "Jeff".ShowItems;

// 调用(Invoke)委托,后者调用(call)ShowItems
// 并向它传递对字符串"Jeff"的引用
a();
  1. 编译器生成IL代码来构造一个Action委托.
  2. 创建委托时,会向构造器传递应调用的方法,同时传递一个对象引用,这个引用传给方法的隐藏this阐述.
  3. 正常情况下,创建引用静态方法的委托时,对象引用为null,因为静态方法没有this参数
  4. 这个例子中,C#编译器生成特殊代码创建一个委托来引用静态方法(ShowItems),而静态方法的目标对象是”Jeff”字符串的引用.
  5. 这个委托被调用时,CLR会调用静态方法,并向其传递对”Jeff”字符串的引用.

关于invoke和call的翻译区别

调用:

  • invoke:理解为唤出更为恰当, 需要先唤出某个东西帮你调用一个信息不明的方法时,用invoke比较恰当.
  • call:调用, 在执行一个所有信息都已知的方法时,用call比较恰当.

ExtensionAttribute特性类

  1. 一旦用this关键字标记了某个静态方法的第一个参数
  2. 编辑器就会在内部向该方法应用一个定制特性
  3. 此特性会在最终生成的文件的元数据中持久性的存储下来.
  4. 任何静态类只要包含扩展方法,它的元数据也会应用这个特效.

分部方法

假设用某个工具生成了C#源代码文件,并且这个类会让你定制类型的行为.
一般做法:

  1. 定义个虚方法
  2. 从这个类派生并定义自己的类
  3. 重写虚方法,实现定制的行为
//工具生成的代码,存储在某个代码文件中
internal class Base
{
    private String m_name;    
    // 虚方法定义
    protected virtual void OnNameChanging(String value){}

    public String Name
    {
        get{return m_name;}
        set{
            OnNameChanging(value.ToUpper());
            m_name=value;
        }
    }
}
//开发人员生成的代码。存储在另一个源代码文件中
internal class Derived:Base
{
    protected override void OnNameChanging(string value)
    {
        if(String.IsNullOrEmpty(value))
            throw new ArgumentNullException("value");
    }
}

这个做法有几个问题:

  1. 因为用到继承,所以基类不能是密封类,也不能用于值类型(值类型隐式密封).
  2. 不能用于静态方法,静态方法不能重写.
  3. 效率问题,如果派生类不重写方法,也会生成对ToUpper()进行调用的IL代码

以下用 分部方法 功能来解决,并且在一些情况下能提升运行时性能:

//工具生成的代码,存储在某个代码文件中
internal sealed partial class Base
{
    private String m_name;

    // 分部方法的声明
    partial virtual void OnNameChanging(String value);
    public String Name
    {
        get{return m_name;}
        set{
            OnNameChanging(value.ToUpper());
            m_name=value;
        }
    }
}
//开发人员生成的代码。存储在另一个源代码文件中
internal sealed partial class Base
{
    partial void OnNameChanging(string value)
    {
        if(String.IsNullOrEmpty(value))
            throw new ArgumentNullException("value");
    }
}
  1. 类可以是密封类,静态类,值类型.
  2. 工具生成的代码包含分部方法的声明,要用partial关键字标记,无主体.
  3. 开发者生成的代码实现这个声明. 该方法也要用partial关键字标记,有主题.

输入partial按空格能智能感知列出当前类型定义的还没有匹配实现的所有分部方法声明.

如果不提供自己的源文件(不需要修改工具生成的类型的行为),编译器编译工具生成的代码就不会包含任何代表分部方法的元数据,也不会生成任何调用分部方法的IL指令. 而且, 编译器不会生成对本该传给分部方法的实参进行求值的IL指令. 在此例中,编译器不会生成调用ToUpper方法的代码. 使运行时的性能得到了提升.

分部方法的规则和原则

  1. 只能在分部类或结构中声明.
  2. 分部方法的返回类型始终是void, 任何蚕食都不能用out修饰符来标记.
    1. 是因为运行时,方法可能不存在(之前说过,可以不提供自己的实现代码),不能进行初始化(out参数必须进行初始化)
  3. 分部方法可以有ref参数,可以是泛型方法,可以使实例或静态方法,而且可以标记为unsafe.
  4. 分部方法的声明和实现必须有完全一致的签名.
  5. 分部方法总是被视为private方法,但C#编译器禁止在分部方法声明之前添加private关键字.
  6. 如果没有对应的实现部分,便不能在代码中创建一个委托来引用这个分部方法.