C#

C#方法

Posted by LudoArt on July 10, 2023

C#方法

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

构造器是将类型的实例初始化为良好状态的特殊方法。创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段(类型对象指针和同步块索引),最后调用类型的实例构造器来设置对象的初始状态。

极少数时候可以在不调用实例构造器的前提下创建类型的实例,除了:

  • Object的 MemberwiseClone 方法;
  • 和运行时序列化器反序列化对象时;

注:不要在构造器中调用虚方法,调用虚方法会导致无法预测的行为。

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

在上述代码中,构造 SomeType 的对象时,它的 m_x 字段被初始化为5,具体可看其生成的IL代码:

.method public hidebysig spcialname rtspcialname 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: ldrag.0
    IL_0008: call		instance void [mscorlib]System.Object::.ctor()
    IL_000d: ret
} // end of method SomeType::.ctor

可以看出, SomeType的构造器把值5存储到字段 m_x ,再调用基类的构造器。

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

internal 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 = "Hi";
        m_d = 3.1415;
        m_b = 0xff;
    }
    
    // 该构造器将所有的字段都设为默认值,然后修改m_x
    public SomeType(Int32 x) : this()
    {
        m_x = x;
    }
    
    // 该构造器将所有的字段都设为默认值,然后修改m_s
    public SomeType(String s) : this()
    {
        m_s = s;
    }
    
    public SomeType(Int32 x, String s) : this()
    {
        m_x = x;
        m_s = s;
    }
}

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

CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。所以,值类型并不需要定义构造器,C#编译器根本不会为值类型内联(嵌入)默认的无参构造器。

值类型的实例构造器只有显式调用才会执行。如下代码:

internal struct Point
{
    public Int32 m_x, m_y;
    
    public Point()	// 其实这里编译不了:error CS0568 结构不能包含显式的无参数构造器
    {
        m_x = m_y = 5;
    }
}

internal sealed class Rectangle
{
    public Point m_top, m_bottom;
    
    public Rectangle() { }
}

// 此时构造新的Rectangle类时,m_top, m_bottom会被初始为0!!因为没有任何地方显式调用了Point的构造器

严格地说,只有当值类型的字段嵌套到引用类型中时,才保证被初始化为0或null。任何基于栈的值类型字段都必须在读取之前写入(赋值),若没有编译器会报错(问题不大,编译器真好)。

值类型的构造器中,this代表值类型本身的一个实例。而在引用类型的构造器中,this被认为是只读的,不能对它进行赋值。

类型构造器

CLR还支持类型构造器,也称为静态构造器、类构造器或类型初始化器。

类型构造器的作用是设置类型的初始状态。类型默认没有定义类型构造器,如果定义,也只能定义一个。

此外,类型构造器永远没有参数。

// 引用类型的类型构造器
class SomeRefType
{
    static SomeRefType()
    {
        // SomeRefType被首次访问时,执行这里的代码
    }
}

// 值类型的类型构造器
struct SomeValType
{
    // CLR有时不会调用值类型的静态类型构造器
    static SomeValType()
    {
        // SomeValType被首次访问时,执行这里的代码
    }
}

定义类型构造器类似于定义无参实例构造器,区别在于必须标记为 static。此外,类型构造器总是私有的,是为了防止任何由开发人员写的代码调用它,对它的调用总是由CLR负责

JIT编译器在编译一个方法时,会查看代码中都引用了哪些类型。任何一个类型定义了类型构造器,JIT编译器都会检查针对当前 AppDomain,是否已经执行了这个类型构造器:

  • 如果从未执行,JIT编译器会在它生成的本机代码中添加对类型构造器的调用
  • 如果已经执行,JIT编译器就不添加对它的调用

多个线程可能同时执行相同的方法,CLR希望确保在每个AppDomain中,一个类型构造器只执行一次,所以在调用类型构造器时,调用线程要获取一个互斥线程同步锁

因为这种独特的机制,所以非常适合在类型构造器中初始化类型需要的任何单实例(Singleton)对象。

如果类型构造器抛出未处理异常,CLR会认为类型不可用,试图访问该类型的任何字段或方法,都会抛出 System.TypeInitializationException 异常。

类型构造器不应调用基类型的类型构造器,因为类型不可能有静态字段是从基类型分享或继承的。

操作符重载方法

CLR规范要求操作符重载方法必须是 publicstatic 方法,另外,C#要求操作符重载方法至少有一个参数的类型与当前定义这个方法的类型相同

C#的一元操作符及其相容于CLS的方法名:

C#操作符 特殊方法名 推荐的相容于CLS的方法名
+ op_UnaryPlus Plus
- op_UnaryNegation Negation
! op_LogicalNot Not
~ op_OnesComplement OnesComplement
++ op_Increment Increment
op_Decrement Decrement
(无) op_True IsTrue { get; }
(无) op_False IsFalse { get; }

C#的二元操作符及其相容于CLS的方法名:

C#操作符 特殊方法名 推荐的相容于CLS的方法名
+ 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

转换操作符方法

当需要将对象从一种类型转换为另一种类型时(基元类型编译器自己知道如何生成转换对象所需的代码):

public sealed class Rational
{
    // 由一个Int32构造一个Rational
    public Rational(Int32 num) { ... }
    
    // 由一个Single构造一个Rational
    public Rational(Single num) { ... }
    
    // 将一个Rational转换成一个Int32
    public Int32 ToInt32() { ... }
    
    // 将一个Rational转换成一个Single
    public Single ToSingle() { ... }
    
    // 由一个Int32隐式构造并返回一个Rational
    public static implicit operator Rational(Int32 num)
    {
        return new Rational(num);
    }
    
    // 由一个Single隐式构造并返回一个Rational
    public static implicit operator Rational(Single num)
    {
        return new Rational(num);
    }
    
    // 由一个Rational显式返回一个Int32
    public static explicit operator Int32(Rational r)
    {
        return r.ToInt32();
    }
    
    // 由一个Rational显式返回一个Single
    public static explicit operator Single(Rational r)
    {
        return r.ToSingle();
    }
}


// 使用
public static void Main()
{
    Rational r1 = 5;			// Int32隐式转型为Rational
    Rational r2 = 2.5F;			// Single隐式转型为Rational
    
    Int32 x = (Int32) r1;		// Rational显式转型为Int32
    Single s = (Single) r2;		// Rational显式转型为Single
}

转换操作符是将对象从一种类型转换为另一种类型的方法。CLR规范要求转换操作符重载方法必须是 publicstatic 方法,另外,C#要求参数类型和返回类型二者必有其一与定义这个方法的类型相同(与操作符重载类似)。

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

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

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

扩展方法

先举个例子,假如希望在 StringBuilder 类中添加一个 IndexOf 方法,可以这么做:

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.");
Int32 index = StringBuilderExtensions.IndexOf(sb.Replace('.', '!'), '!');

上面的例子work,但有以下几个问题:

  • 要获取一个 StringBuilder 中的某个字符的索引,必须先知道 StringBuilderExtensions 类的存在;
  • 代码没有反映出在 StringBuilder 对象上执行的操作的顺序

改进:

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


// 使用:
StringBuilder sb = new StringBuilder("Hello.");
Int32 index = sb.Replace('.', '!').IndexOf('!');

当编译器看到 IndexOf

  • 首先检查 StringBuilder 类或者它的任何基类是否提供了获取单个 Char 参数、名为 IndexOf 的一个实例方法
  • 如果没有找到匹配的实例方法,就继续检查是否有任何静态类定义了名为 IndexOf 的静态方法,方法的一个参数的类型和当前用于调用方法的那个表达式的类型匹配,而且该类型必须用this关键字标识

规则和原则

关于扩展方法,有一些附加的规则和原则需要注意:

  • C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符等;
  • 扩展方法(只有第一个参数前面有this的方法)必须在非泛型的静态类中声明
  • C#编译器在静态类中查找扩展方法时,要求静态类本身必须具有文件作用域(类要具有整个文件的作用域,而不能嵌套在某个类中而只具有该类的作用域)
  • 由于静态类可以取任何名字,所以C#编译器要花一定时间来寻找扩展方法,它必须检查文件作用域中所有的静态类,并扫描它们的所有静态方法来查找一个匹配
    • 为增强性能,并避免找到非你所愿的扩展方法,C#编译器要求“导入”扩展方法
  • 多个静态类可以定义相同的扩展方法。此时不能再用实例方法语法来调用这个静态方法,相反,必须使用静态方法语法。换言之,必须显式指定静态类的名称,明确告诉编译器要调用哪个方法
  • 扩展方法可能存在版本控制问题

用扩展方法扩展各种类型

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

StringBuilder sb = null;

// 调用扩展方法:NullReferenceException异常不会在调用IndexOf时抛出,
// 相反,NullReferenceException是在IndexOf内部的for循环中抛出的
sb.IndexOf('X');

// 调用实例方法:NullReferenceException异常在调用Replace时抛出
sb.Replace('.', '!');

可以为接口类型定义扩展方法:

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

// 任何表达式,只要最终实现了IEnumerable<T>接口,就能调用上述扩展方法:
public static void Main()
{
    "Grant".ShowItems();	// 每个Char在控制台上单独显示一行
    
    new [] { "Jeff", "Kristin" }.ShowItems();	// 每个String在控制台上单独显示一行
    
    new List<Int32>() { 1, 2, 3 }ShowItems();	// 每个Int32在控制台上单独显示一行
}

扩展方法是LINQ技术的基础。System.Linq.Enumerable 及其所有静态扩展方法,这个类中的每个扩展方法都扩展了 IEnumerableIEnumerable<T> 接口。

可以为委托类型定义扩展方法:

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

// 使用:
Action<Object> action = o => Console.WriteLine(o.GetType()); // 抛出NullReferenceException
action.InvokeAndCatch<NullReferenceException>(null); // 吞噬NullReferenceException

可以为枚举类型定义扩展方法:

public enum FileAttributes
{
    ReadOnly,
    System,
}

internal static class FileAttributesExtensionMethods
{
    public static Boolean IsSet(this FileAttributes flag, FileAttributes flagToTest)
    {
        if (flagToTest == 0)
            throw new ArgumentOutOfRangeException("flagToTest", "Value must not be 0");
        return (flags & flagToTest) == flagToTest;
    }
    
    public static Boolean IsClear(this FileAttributes flag, FileAttributes flagToTest)
    {
        if (flagToTest == 0)
            throw new ArgumentOutOfRangeException("flagToTest", "Value must not be 0");
        return !IsSet(flags, flagToTest);
    }
    
    public static Boolean AnyFlagsSet(this FileAttributes flag, FileAttributes TestFlags)
    {
        return ((flags & TestFlags) != 0);
    }
    
    public static FileAttributes Set(this FileAttributes flags, FileAttributes setFlags)
    {
        return flags | setFlags;
    }
    
    public static FileAttributes Clear(this FileAttributes flags, FileAttributes clearFlags)
    {
        return flags & ~clearFlags;
    }
    
    public static void ForEach(this FileAttributes flags, Action<FileAttributes> processFlag)
    {
        if (processFlag == null) throw new ArgumentNullException("processFlag");
        
        for (UInt32 bit = 1; bit != 0; bit <<= 1)
        {
            UInt32 temp = ((UInt32)flags) & bit;
            if (temp != 0)
                processFlag((FileAttributes)temp);
        }
    }
}

// 使用:
FileAttributes fa = FileAttributes.System;
fa = fa.Set(FileAttributes.ReadOnly);
fa = fa.Clear(FileAttributes.System);
fa.ForEach(f => Console.WriteLine(f));

C#编译器允许创建委托来引用一个对象上的扩展方法

public static void Main()
{
    // 创建一个Action委托(实例)来引用静态ShowItems扩展方法,
    // 并初始化第一个实参来引用字符串"Jeff"
    Action a = "Jeff".ShowItems;
    
    // 调用(Invoke)委托,后者调用(call)ShowItems,
    // 并向它传递对字符串"Jeff"的引用
    a();
}

ExtensionAttribute类

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

// 在System.Runtime.CompilerServices命名空间中定义
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly)]
public sealed class ExtensionAttribute : Attribute { }

除此之外,任何静态类只要包含至少一个扩展方法,它的元数据中也会应用这个特性。利用这个技术,代码能以最快速度编译完毕:

  • 如果代码调用了一个不存在的实例方法,编译器就能快速扫描引用的所有程序集,判断它们哪些包含了扩展方法。
  • 然后在这些程序集中,可以只扫描包含了扩展方法的静态类。
  • 在每个这样的静态类中,可以只扫描扩展方法来查找匹配

分部方法

假设用某个工具生成了包含类型定义的C#源代码文件,工具知道你想在代码的某些位置定制类型的行为。

正常情况下,是让工具生成的代码调用虚方法来进行定制,如果想定制类的行为,就必须从基类派生并定义自己的类,重写虚方法来实现自己想要的行为,如下所示:

// 工具生成的代码,存储在某个源代码文件中:
internal class Base
{
    private String m_name;
    
    protected virtual void OnNameChanging(String value) { }
}

// 开发人员生成的代码,存储在另一个源代码文件中
internal class Derived : Base
{
    protected override void OnNameChanging(String value)
    {
        if (String.IsNullOrEmpty(value))
            throw new ArgumentNullException("value");
    }
}

上述代码存在几个问题:

  • 类型必须是非密封的类。这个技术不能用于密封类,也不能用于值类型。还不能用于静态方法,因为静态方法不能重写
  • 效率问题。这会浪费少量系统资源

以下代码使用分部方法实现和上述代码完全一样的语义:

// 工具生成的代码,存储在某个源代码文件中:
internal sealed partial class Base
{
    private String m_name;
    
    // 这是分部方法的声明
    partial void OnNameChanging(String value) { }
}

// 开发人员生成的代码,存储在另一个源代码文件中
internal sealed partial class Base
{
    // 这是分部方法的实现
    partial void OnNameChanging(String value)
    {
        if (String.IsNullOrEmpty(value))
            throw new ArgumentNullException("value");
    }
}