C# 的 class

类(class)是一个数据结构,它可以包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、操作符、实例构造函数、终结器和静态构造函数)和嵌套类型。class 类型支持继承,这是一种派生类可以扩展和专门化基类的机制。

声明 class

: 属性? 类编辑器* '部分'? 'class' 标识符 类参列表? 基类? 类参约束子句* 类体 ';'?

属性(访问修饰符)

属性(attributes,访问修饰符)是可选的,类及其成员可以是下表中的任意属性:

属性 注解
private 该类(成员)是私有的,只能在声明它的类中使用 private 类(成员)
public 该类(成员)是共有的,可以在其他程序集内访问任意 public 类(成员)
internal 该类(成员)是私有的,只能在当前程序集中使用 internal 类(成员)
protected 该类(成员)是私有的,只能在该类或该类的派生类访问(派生类只能访问自己的 protected 成员,不能访问基类的 protected 成员)
protected internal 该类(成员)是私有的,只能在当前程序集或该类的派生类访问
private protected 该类(成员)是私有的,只能在当前程序集中该类的派生类或包含类的类型访问

类及其成员只能使用上述访问修饰符中的某一个。

根据出现成员声明的上下文,仅允许某些声明的可访问性。如果未在成员声明中指定访问修饰符,则将使用默认可访问性(即上下文的可访问性)。

未嵌套在其他类型中的顶级类型只能具有 internal 或 public 可访问性。这些类型的默认(未指定)可访问性为 internal。

类内的任何成员默认的可访问性为 private,可以指定为上述任意一个可访问性。
例如:

class LeiJI
    {
    protected int x = 36;
    }
class LeiPaiSheng : LeiJI
    {
        public static void Go ( )
            {
            LeiJI J = new ( );
            LeiPaiSheng P = new ( );

            Console . WriteLine ( $"P . x = {P . x}" );
            // J . x = 72; // 无法赋值基类的 x 元素,因为可访问性为 protected,因此产生 CS1540:无法通过“LeiJI”的限定符访问受保护成员“LeiJI.x”;限定符必须是类型“LeiPaiSheng”类型(或者从该类型派生)
            P . x = 72;
            Console . WriteLine ( $"P . x = {P . x}" );
            }
    }

上面这个基类(LeiJI)有一个可访问性 protected 的整数 x。派生类(LeiPaiSheng)继承自基类(LeiJI),其 Go 方法描述了你可以定义两个对象,基类和派生类。由于基类的 x 元素是 protected,所以无法在派生类中访问。但可以访问派生类的 x(被继承的)。如果代码在其他地方,例如程序的入口 Main,也无法访问派生类的 x,但可以访问派生类的 Go 方法(因为它是 public 的)。

若把基类的 x 元素可访问性修改为 private,派生类将没有 x 元素(没有被继承)。如果 x 元素可访问性为 public,则基类和派生类均可访问基类的 x 元素(派生类也有元素 x)。

类修饰符

类修饰符是可选的,包括以下能够控制类的状态的修饰符:

类型 注解
new 仅限于该类是嵌套于其他类的,指定该类隐藏同名的继承成员
abstract 该类是抽象的,只能被其他类继承(不能被声明为一个对象)。
partial 通过分部类型可以定义要拆分到多个定义中的类、结构、接口或记录。 这些多个定义可以位于同一项目内的不同文件中。
sealed 该类是具体的,不能被其他类继承(可以被声明为一个对象)
static 该类是静态的,不能被实例化,不能被用作类型,其成员都是静态的。只有静态类可以包含扩展方法的声明。
unsafe 仅限于 unsafe 代码块中,表明该类使用了 unsafe 代码。

类修饰符用于指定类的状态。任何重复使用都是产生编译错误的。两个作用相反的修饰符也不能共用于一个类(例如 abstract 和 sealed)。

new 不能声明非嵌套的类。

属性修饰符和类修饰符有时会发生冲突。

abstract 类

表明该类只能是抽象的,不能被实例化,只能是其他类的基类。详见 C# 的 abstract 一文。

partial 类

详见 C# 的 partial 一文。

sealed 类

表明该类只能是具体的,可以被实例化,不能是其他类的基类。详见 C# 的 sealed 一文。

static 类

静态类受到如下限制:

  • 不能使用 abstract 和 sealed 修饰符(static 类不能被派生,也不能被实例化,既抽象又具体)。
  • static 类不能包含 class_base 规范,也不能显式地指定基类或已实现的 interface 列表,隐式地继承自 object。
  • static 类只能包含 static 成员。所有常量和嵌套类都被自动归类为 static 成员。
  • static 类的成员不能具有 protected、private protected 和 protected internal 可访问性。
  • static 类没有默认的构造函数,也不能声明构造函数(不能被实例化)。

除常量和嵌套类以外,static 类的成员不是自动静态的,所有成员必须显式地包含 static 修饰符。如果类嵌套在 static 类外部,除非它显式地包含 static 修饰符,否则就不是静态类。

只有类的类型声明部分(一个或多个)包含 static 修饰符,类才是静态的。

详见 C# 的 static 一文。

类参列表

类参是一个简单的标识符,表示为创建构造类型而提供的类参的占位符。相比之下,类型实参是在创建构造类型时替代类型形参的类型。

类声明中的每个类参在该类的声明空间中定义了一个名称。因此,它不能与该类的其它类参、该类中声明的成员以及类本身具有相同的名称。

如果两个部分泛型类型声明(在同一程序中)具有相同的完全限定名(其中包含用于类参数量的generic_dimension 规范),则它们将生成相同的未绑定泛型类型。两个这样的部分类型声明应该按顺序为每个类参指定相同的名称。

基类

类声明可以包括 class_base 规范,该规范定义了类的直接基类和类直接实现的接口。

当 class 包含在基类中时,它指定要声明的类的直接基类。如果非分部类声明没有基类,或者基类只列出接口类型,则假定直接基类是 object。当分部类声明包含基类规范时,该基类规范应引用与该分部类型中包含基类规范的所有其他部分相同的类型。如果分部类的任何部分都不包括基类规范,则基类是 object。类从其直接基类继承成员。

对于构造的 class 类型,包括在泛型类型声明中声明的嵌套类型,如果在泛型类声明中指定了基类,则通过替换基类声明中的每个类参来获得构造类型的基类,构造类型的对应类参。

class Ji < U , V >
    {

    }
class PS < W > : Ji < W , TimeOnly >
    {

    }

上例中,基类 Ji 具有两个类参 U 和 V。派生类 PS 只有一个类参 W,代替了基类的 U,则需要指定基类的 V 的替代者,即上例中的 TimeOnly。

在类声明中指定的基类可以是构造的类类型。基类本身不能是类参,但它可以包含作用域内的类参。

class PS < V > : V // CS0689:“V”是一个类型参数,无法从它进行派生
    {

    }

class 类型的直接基类应至少与 class 类型本身具有同样的可访问性。例如,public 类继承自 private 类或 internal 类是编译时错误。

class 类型的直接基类不能是下列任何类型:System.Array(数组)、System.Delegate(委托)、System.Enum(枚举)System.ValueType(值类型)以及 dynamic(动态)类型。此外,泛型类声明不能使用 System.Attribute 作为直接或间接基类

在确定类 B 的直接基类规范 A 的含义时,B的直接基类被临时假定为 object,这确保了基类规范的含义不能递归地依赖于自身。

class Ji < U >
    {
    public class Ji2
        {
        }
    }

class PS : Ji< PS . Ji2 > // CS0146:涉及“PS”和“PS”的循环基类型依赖项(PS 声明时,直接基类被认为是 object,因此不包含 Ji2)
    {

    }

一个类的基类,包括直接基类及其基类。也就是说,基类是一个集合,至少包括 object 一个元素,或者包括所有基类的直接基类关系的传递闭包。详见 C# 的 base 一文。

class A {}
class B : A {}
class C : B> {}
class D : C {}

上例中的 D,其直接基类为 C,其他基类包括 B 和 A,以及 object。C 继承 B 的内容,B 继承 A 的内容,均可以在 D 中体现。例如:

class A
    {
    public virtual void FF ( )
        {
        Console . WriteLine ( $"A 的 方法 FF" );
        }
    }
class B : A
    {
    public override void FF ( )
    {
    Console . WriteLine ( $"B 的 方法 FF;然后是:" );
    base . FF ( );
    }
    }
class C : B
    {
    public override void FF ( )
    {
    Console . WriteLine ( $"C 的 方法 FF;然后是:" );
    base . FF ( );
    }
    }
class D : C
    {
    public override void FF ( )
    {
    base . FF ( );
    }
    }

上例中的 D 直接继承自 C,所以 D 类型的对象的 FF 方法其实是 C 类型的 FF 方法的复制品(仅执行了 base . FF):

D PSd = new ( );
PSd . FF ();

输出结果:
C 的 方法 FF;然后是:
B 的 方法 FF;然后是:
A 的 方法 FF

每个类都只能有一个显式的直接基类(若没有,则是直接继承自 object),其直接基类的可继承内容会自动传递到派生类本身。object 类唯一例外,它没有直接基类。

除了直接基类,所有的类均同时隐式直接继承自 object。所以每个类都具有继承自 object 的 Equals(相等)、GetHashCode(获取哈希码)和 ToString(返回字符串表示形式)三个方法。

类依赖自身是一个编译时错误。出于此规则的目的,类直接依赖于它的直接基类(如果有的话),并直接依赖于它嵌套在其中的最近的封闭类(如果有的话)。给定这个定义,一个类所依赖的类的完整集合是直接依赖关系的传递闭包。

class A : A {}

class A : B {}
class B : C {}
class C : A {}

class A : B . C {}
class B : A
{
    public class C {}
}

上述代码均会产生编译错误,警告 0146。第一段类继承自本身;第二段类递归,声明 C 类型的对象会递归引用到它自己;第三段声明 B 类型的对象引用了 A,但 A 却需要 B 中的 C。

嵌套类(子类)可以继承自母类。

class A
    {
    class B : A
        {

        }
    }

上例中,B 是继承自 A 的,且又封闭在 A 中。由于 A 并不依赖于 B,B 不是 A 的基类,也不是 A 的封闭类,所以,例子有效。

基类不能是密封类(基类声明包括 sealed 关键字)。警告 CS0509:“类名”无法从密封类型“密封类名”派生。

接口(interface)实现

class_base 规范可能包括接口类型的列表,在这种情况下,类被称为实现给定的接口类型。对于构造的 class 类型,包括在泛型类型声明中声明的嵌套类型,每个实现的接口类型都是通过将给定接口中的每个类参替换为构造类型的相应类参来获得的。

在多个部分声明的类型的 interface 集合是每个部分指定的接口的并集。一个特定的接口只能在每个部件上命名一次,但多个部件可以命名相同的基本接口。任何给定接口的每个成员只能有一个实现。

public class INum : INumber < int >
    {

    }

上例中类 INum 实现了 INumber < int > 接口。例子没完成,你需要为 INumber 接口的每个实现书写代码。

通常,每个部分都提供在该部分上声明的接口的实现;然而,这不是必需的。一个部件可以为在另一个部件上声明的接口提供实现。

partial class X
    {
    int IComparable . CompareTo ( object? obj )
        {
        if ( this == obj )
            return 0;
        else if ( this != obj )
            return -1;
        else
            throw new ArgumentException ( $"无法比较 {this} 和 {obj}" );
        }
    }

partial class X : IComparable
    {

    }

上例中为类型 X 指定了比较接口 IComparable,并实现,返回值为 0(相等)、-1(不相等)或 ArgumentException(无法比较)。

在类声明中指定的基接口可以被构造为接口类型。基接口本身不能是类型参数,但它可以涉及范围内的类型参数。

例如下面的代码演示了一个类如何实现和扩展构造类型:

class Ji < U , V >
    {

    }

interface ITest < V >
    {

     }

class PS1 : Ji < string , int > , ITest < string >
    {

    }

class PS2 < T > : Ji < int , T > , ITest < T >
    {

    }

类参约束子句

每个类参约束子句由标记 where、类型参数的名称、冒号(:)和该类型参数的约束列表组成。每个类参最多只能有一个 where 子句,并且 where 子句可以按任意顺序列出。与属性访问器中的 get 和 set 令牌一样,where 令牌不是关键字。

where 子句中给出的约束列表可以包括以下任何组件,顺序如下:单个主约束,一个或多个辅助约束,以及构造函数约束 new ( )。

主约束可以是 class 类型、引用类型约束类、值类型约束结构、非空约束 notnull 或非托管类型约束 unmanaged。class 类型和引用类型约束可以包括 nullable 类型注释。

次要约束可以是 interface 或 类参,可选地后跟一个 nullable 类型注释。nullable 类型注释的存在表明类参允许为可空引用类型,该类型对应于满足约束的非可空引用类型。

引用类型约束指定用于类型形参的类型实参必须是引用类型。所有已知为引用类型(定义如下)的 class 类型、interface 类型、delegate 类型、array 类型和类参都满足此约束。

class 类型、引用类型约束和辅助约束可以包括 nullable 类型注释。类参上是否有此注释表示类型实参的可空性期望:

  • 如果约束不包括 nullable 类型注释,则类型参数应该是一个非可空引用类型。如果类型参数是可空引用类型,编译器可能会发出警告。
  • 如果约束包含 nullable 类型注释,则非可空引用类型和可空引用类型都满足约束。

类型实参的可空性不必与类型形参的可空性匹配。如果类型形参的可空性与类型实参的可空性不匹配,编译器可能会发出警告。

注意:要指定类参为可空引用类型,不要将可空类型注释添加为约束(使用 T : class 或 T : BaseClass),而是使用 T? 在整个泛型声明中指出类参对应的可空引用类型。

nullable 类型注释(?)不能用于不受约束的类参。

对于类参 T,当类参是一个可空的引用类型 C?, T? 的实例被解释为 C?,不是 C??。

示例:下例显示了类型实参的可空性如何影响其类型形参声明的可空性:

/// 
/// 一个空的类
/// 
public class LeiKong
    {
    public override string ToString ( )
        {
        return "空的类";
        }
    }

/// 
/// 扩展的静态类
/// 
public static class LeiKuoZhan
    {
    /// 
    /// 扩展类的 FF 方法。
    /// 
    /// 可以为 null 的类型实参
    /// 形参
    public static void FF  ( this T? arg ) where T : notnull
        {
        if ( arg == null ) // 如果形参是 null,输出 null,否则输出形参
            {
            Console . WriteLine ( "null" );
            }
        else
            {
            Console .WriteLine ( arg );
            }
        }
    }
static void Main(string[] args)
    {
    LeiKong? knk = null; // 可以为 null 的空类对象
    LeiKong bnk = new ( ); // 不能为 null 的空类对象

    int zhs = 5; // 不能为 null 的 int
    int? zhsk = null; // 为 null 的 int

    knk . FF ( ); // 输出“null”
    bnk . FF ( ); // 输出“空的类”
    zhs . FF ( ); // 输出“5”
#pragma warning disable CS8714 // 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与 "notnull" 约束不匹配。
    zhsk . FF ( ); // 输出“null”
#pragma warning restore CS8714 // 类型不能用作泛型类型或方法中的类型参数。类型参数的为 Null 性与 "notnull" 约束不匹配。

上例中 zhsk 的输出可以屏蔽 CS8714 警告,但不屏蔽也不影响输出。

当类型参数为非空类型时,? 类型注释表明参数是相应的可空类型。当类型实参已经是可空引用类型时,形参是相同的可空类型。

notnull 约束指定用于类型形参的类型实参应该是非空值类型或非空引用类型。允许使用非可空值类型或非可空引用类型的类型参数,但编译器可能会产生诊断警告。

因为 notnull 不是关键字,所以在主约束中,非空约束在语法上总是与类类型不明确。出于兼容性原因,如果成功查找 notnull 的名称,则将其视为类类型。否则,它将被视为非空约束。

值类型约束指定用于类型参数的类型参数必须为非空值类型。所有非空结构类型、枚举类型和具有值类型约束的类型参数都满足此约束。请注意,尽管可空值类型被归类为值类型,但它不满足值类型约束。具有值类型约束的类型形参不能同时具有构造函数约束,尽管它可以用作具有构造函数约束的另一个类型形参的类型实参。

注:System . Nullable 类型指定 T 的非空值类型约束。因此,禁止使用“T??”和“Nullable>”形式的递归构造类型。

非托管类型约束指定用于类型形参的类型实参必须是不可空的非托管类型。

因为 unmanaged 不是关键字,所以在主约束中,非托管约束在语法上总是与类类型不明确。出于兼容性原因,如果名称 unmanaged 的名称查找成功,则将其视为类类型。否则,它将被视为非托管约束。

指针类型永远不允许作为类型参数,并且不满足任何类型约束,即使是非托管类型,也不满足非托管类型。

如果约束是类类型、接口类型或类型参数,则该类型指定用于该类型参数的每个类型实参都应支持的最小“基类型”。每当使用构造类型或泛型方法时,都会在编译时根据类型参数的约束检查类型实参。提供的类型参数应满足 C# 的 type 一文“满足约束条件”中描述的条件。

类类型约束必须满足以下规则:

  • 类型应为类类型。
  • 类型不应密封(sealed)。
  • 该类型不得为以下类型之一:Array 或 System.ValueType。
  • 类型不得为 object。
  • 给定类型参数最多只能有一个约束是类类型。

指定为接口类型约束的类型应满足以下规则:

  • 类型应为接口类型。
  • 在一个给定的 where 子句中,一个类型不得指定超过一次。

在任何一种情况下,约束都可能涉及作为构造类型的一部分的关联类型或方法声明的任何类型参数,并且可能涉及被声明的类型。

作为类型参数约束指定的任何类或接口类型应至少与声明的泛型类型或方法具有相同的可访问性。

作为类型参数约束指定的类型必须满足以下规则:

  • 类型应该是一个类型参数。
  • 在一个给定的 where 子句中,一个类型不得指定超过一次。

另外,类型参数的依赖关系图中不能有循环,其中依赖关系是一个传递关系,定义为:

  • 如果使用类型参数 T 作为类型参数 S 的约束,则 S 依赖于 T。
  • 如果一个类型参数 S 依赖于一个类型参数 T,而 T 依赖于一个类型参数 U,那么 S 依赖于 U。

考虑到这种关系,类型参数(直接或间接)依赖自身是一个编译时错误。

任何约束必须在依赖类型参数之间保持一致。如果类型参数 S 依赖于类型参数 T,则:

  • T 不能有值类型约束。否则,T 将被有效地密封,因此 S 将被迫为与 T 相同的类型,从而消除了对两个类型参数的需求。
  • 如果 S 有值类型约束,那么 T 不应该有类类型约束。
  • 如果 S 有一个类类型约束 A, T 有一个类类型约束 B,那么就应该存在从 A 到 B 的单位转换或隐式引用转换,或者从 B 到 A 的隐式引用转换。
  • 如果 S 也依赖于类型参数 U,并且 U 有一个类类型约束 A, T 有一个类类型约束 B,那么就应该存在从 A 到 B 的单位转换或隐式引用转换,或者从 B 到 A 的隐式引用转换。

S 具有值类型约束,T 具有引用类型约束是有效的。这有效地将 T 限制为类型 System . Object、System . ValueType、System . Enum 和任意接口类型。

如果类型参数的 where 子句包含构造函数约束(形式为 new ( )),则可以使用 new 操作符创建该类型的实例。用于带有构造函数约束的类型形参的任何类型实参必须是值类型、具有公共无参数构造函数的非抽象类,或者具有值类型约束或构造函数约束的类型形参。

如果类参约束具有结构(struct)的主约束,或者 unmanaged 还具有构造函数约束,则会导致编译时错误。例如:

interface I打印
    {
    void 打印 ( );
    }

interface I比较 < T >
    {
    int CompareTo ( T value );
    }

interface I键提供者 < T >
    {
    T GetKey ( );
    }

class Lei打印 < T > where T : I打印
    {}

class Lei排序的列表 < T > where T : I比较 < T >
    {}

class Lei词典 < K , V >
    where K : I比较 < K >
    where V : I打印 , I键提供者 < K >, new ( )
    {
    }

// 无效的……
class Lei圆 < S, T > // 警告 CS0454:涉及“S”和“T”的循环依赖项
    where S : T
    where T : S
    {
    }

class Sealed < S , T >
    where S : T
    where T : struct // 错误,`T` 是不可被继承的
{
}

class A { }
class B { }

class Incompat < S , T >
    where S : A , T
    where T : B // 错误,不兼容的类类型约束
{
}

class StructWithClass < S , T , U >
    where S : struct , T
    where T : U
    where U : A // 错误,不兼容的结构体
{
}

C 类型的动态擦除是 \( C_x \) 类型,构造如下:

  • 如果 C 是嵌套类型 Outer . Inner,则 \( C_x \) 是一个嵌套类型 \( Outer_x . Inner_x\)。
  • 如果 C 是构造类型 \( C_x \) 带类型参数 \( G < A^1 ,… , A^n > \) 则 \( C_x \) 为构造类型 \( G < A^1_x ,… , A^n_x > \)。
  • 如果 C 是数组类型 E[],则 \( C_x \) 是数组类型 \( E_x \)。
  • 如果 C 是动态的,那么 \( C_x \) 就是 object。
  • 否则,\( C_x \) 为 C。

类型参数 T 的有效基类定义如下:

设 R 是类型的集合,满足:

  • 对于作为类型参数的 T 的每个约束,R 包含其有效的基类。
  • 对于 T 的每个约束都是结构类型,R 包含 System . ValueType。
  • 对于 T 的每个枚举类型的约束,R 包含 System . Enum。
  • 对于 T 的每个委托类型约束,R 包含其动态擦除。
  • 对于数组类型的 T 的每个约束,R 包含 System . Array。
  • 对于 T 的每个约束都是类类型,R 包含它的动态擦除。

然后

  • 如果 T 有值类型约束,它的有效基类是 System . ValueType。
  • 否则,如果 R 为空,则有效基类为 object。
  • 否则,T 的有效基类是集合 r 的最包含类型。如果集合没有包含类型,则 T 的有效基类是 object。一致性规则确保存在包含最多的类型。

如果类型参数是方法类型参数,其约束从基方法继承,则在类型替换后计算有效的基类。

这些规则确保有效的基类总是一个类类型。

类型参数 T 的有效接口集定义如下:

  • 如果 T 没有附加约束,它的有效接口集为空。
  • 如果 T 有接口类型约束,但没有类参约束,则它的有效接口集是其接口类型约束的动态擦除集。
  • 如果 T 没有接口类型约束,但有类参约束,则其有效接口集是其类参约束的有效接口集的并集。
  • 如果 T 同时具有接口类型约束和类参约束,则其有效接口集是接口类型约束的动态擦除集和类参约束的有效接口集的并集。
    如果一个类型参数具有引用类型约束,或者它的有效基类不是 object 或 System . ValueType,那么它就是已知的引用类型。如果已知类型参数是引用类型并且具有不可空引用类型约束,则该类型参数已知为不可空引用类型。

约束类型参数类型的值可用于访问约束所暗示的实例成员。

interface I打印
    {
    void 打印 ( );
    }
class Lei打印 < T > where T : I打印
    {
    void DY1 ( T x ) => x . 打印 ( );
    }

I打印 的 打印 方法可以直接在 x 上调用,因为 T 被限制总是实现 I打印。

当部分泛型类型声明包含约束时,约束应与包含约束的所有其他部分一致。具体来说,包含约束的每个部分都应该具有针对同一组类型参数的约束,并且对于每个类型参数,主约束集、辅助约束集和构造函数约束集应该是等效的。如果两组约束包含相同的成员,则它们是等效的。如果部分泛型类型的任何部分都没有指定类型参数约束,则认为类型参数未受约束。

partial class 图 < K , V >
    where K : I比较 < K >
    where V : I键提供者 < K > , new ( )
{
}

partial class 图 < K , V >
    where V : I键提供者  ,  new ( )
    where K : I比较 < K >
{
}

partial class 图 < K , V >
{
}

都是是正确的,因为包含约束(前两个)的那些部分有效地分别为同一组类型参数指定了相同的主约束、辅助约束和构造函数约束。

类体

类的类体定义了该类的成员。

部分类型声明

当在多个部分中定义类、结构或接口类型时,使用 partial 修饰符。partial 修饰符是上下文关键字,在关键字 class、struct 和 interface 之前具有特殊含义。分部类型可以包含分部方法声明。

分部类型声明的每一部分都应包括 partial 修饰符,并应与其他部分在相同的命名空间或包含类型中声明。partial 修饰符表示类型声明的附加部分可能存在于其他地方,但这些附加部分的存在并不是必需的;它对唯一包含部分修饰符的类型声明有效。它只对分部类型的一次声明有效,以包含基类或实现的接口。但是,基类或实现接口的所有声明都必须匹配,包括任何指定类型参数的可空性。

分部类型的所有部分应一起编译,以便这些部分可以在编译时合并。部分类型特别不允许扩展已经编译的类型。

可以使用 partial 修饰符在多个部分声明嵌套类型。通常,包含类型也使用 partial 声明,嵌套类型的每个部分都在包含类型的不同部分中声明。

示例:下面的部分类由两个 partial 实现,它们驻留在不同的编译单元中。第一部分是由数据库映射工具机器生成的,第二部分是手工编写的:

public partial class Customer
{
    private int id;
    private string name;
    private string address;
    private List orders;

    public Customer()
    {
    }
}

// File: Customer2.cs
public partial class Customer
{
    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

当上面的两个部分一起编译时,结果代码的行为就像类被编写为单个单元一样,如下所示:

public class Customer
{
    private int id;
    private string name;
    private string address;
    private List orders;

    public Customer()
    {
    }

    public void SubmitOrder(Order orderSubmitted) => orders.Add(orderSubmitted);

    public bool HasOutstandingOrders() => orders.Count > 0;
}

类成员

类的成员由其类成员声明引入的成员和从直接基类继承的成员组成。

类的成员分为以下几类:

  • 常量(const),表示与类相关的常量值。
  • 字段(field),它是类的变量。
  • 方法(method),它实现可以由类执行的计算和操作。
  • 属性(property),它定义了已命名的特征以及与读取和写入这些特征相关的操作。
  • 事件(event),它定义了类可以生成的通知。
  • 索引器(indexer),它允许类的实例以与数组相同的方式(语法上)被索引。
  • 操作符(operator),定义可应用于类实例的表达式操作符。
  • 实例构造函数(constructor),实现初始化类实例所需的操作。
  • 终结器(finalizer),它实现了在类的实例被永久丢弃之前要执行的操作。
  • 静态构造函数(static constructor),实现初始化类本身所需的操作。
  • 类型(type),它表示类的局部类型。

类声明创建一个新的声明空间,类参和类成员声明直接包含在类声明中,将新成员引入该声明空间。以下规则适用于类成员声明:

  • 实例构造函数、终结器和静态构造函数的名称应与直接封闭类的名称相同。所有其他成员的名称必须与直接封闭类的名称不同。
  • 类声明的类参列表中的类型形参的名称不得与同一类参列表中的所有其他类型形参的名称不同,也不得与类的名称和类的所有成员的名称不同。
  • 类型的名称应不同于同一类中声明的所有非类型成员的名称。如果两个或多个类型声明共享相同的完全限定名,则声明应具有部分修饰符,并且这些声明组合起来定义单个类型。
    注意:由于类型声明的全限定名编码了类型参数的数量,所以两个不同的类型可以共享相同的名称,只要它们具有不同数量的类型参数。
  • 常量、字段、属性或事件的名称应与同一类中声明的所有其他成员的名称不同。
  • 方法的名称应与同一类中声明的所有其他非方法的名称不同。此外,一个方法的签名应该不同于在同一类中声明的所有其他方法的签名,并且在同一类中声明的两个方法的签名不应该仅仅在 In、out 和 ref 上不同。
  • 实例构造函数的签名必须不同于在同一类中声明的所有其他实例构造函数的签名,并且在同一类中声明的两个构造函数的签名不能仅因 ref 和 out 而不同。
  • 索引器的签名必须不同于在同一类中声明的所有其他索引器的签名。
  • 操作符的签名应不同于在同一类中声明的所有其他操作符的签名。

类的继承成员不是类声明空间的一部分。

注意:因此,允许派生类声明与继承成员具有相同名称或签名的成员(这实际上隐藏了继承成员)。

在多个部分声明的类型的成员集合是在每个部分声明的成员的并集。类型声明的所有部分的主体共享相同的声明空间,并且每个成员的作用域扩展到所有部分的主体。任何成员的可访问域总是包括封闭类型的所有部分;在一个部分中声明的私有成员可以从另一个部分自由访问。在类型的多个部分声明同一成员会导致编译时错误,除非该成员具有 partial 修饰符。

partial class Lei分部
    {
    int x;
    partial void FF ( ); // 仅定义了 FF 方法
    partial class Lei包含
        {
        static int y = 2;
        }
    }

partial class Lei分部
    {
    int x; // 警告 CS0102:“Lei分部”已经包含“x”的定义
    partial void FF ( ) // 书写了 FF 方法的实现,不能仅仅是定义 FF 方法
        {
        }
    partial class Lei包含 // 允许,本来就是 partial 类
        {
        static int z = 7;
        int a = y + z; // 引用了上一个部分的 y
        }
    }

字段初始化顺序在 C# 代码中可能很重要,并且提供了一些保证,如“变量初始化”所定义的那样。否则,类型内成员的顺序很少重要,但在与其他语言和环境接口时可能很重要。在这些情况下,在多个部分中声明的类型中的成员顺序是未定义的。

实例类型

每个类声明都有一个关联的实例类型。对于泛型类声明,实例类型是通过从类型声明中创建构造类型来形成的,每个提供的类型参数都是相应的类型形参。由于实例类型使用类型参数,它只能在类型参数范围内使用;也就是说,在类声明中。实例类型是在类声明内编写的代码的 this 类型。对于非泛型类,实例类型只是声明的类。

下面展示了几个类声明和它们的实例类型:

class A < T >              // 实例类型:A < T >
{
    class B {}             // 实例类型:A < T > . B
    class C < U > {}       // 实例类型:A < T > . C < U >
}
class D {}                 // 实例类型:D

结构类型成员

构造类型的非继承成员是通过将成员声明中的每个类型形参替换为构造类型的相应类型实参来获得的。替换过程基于类型声明的语义,而不是简单的文本替换。

在实例函数成员中,this 的类型是包含声明的实例类型。

泛型类的所有成员都可以直接或作为构造类型的一部分使用任何封闭类的类型参数。当在运行时使用特定的封闭构造类型时,每次使用类型形参时都用提供给构造类型的类型实参替换。

class Lei < V >
    {
    public V? FFfirst;
    public Lei < V > FFsecond;

    public Lei ( V x )
        {
        this . FFfirst = x;
        this . FFsecond = this;
        }
    }

internal class Program
{
    static void Main ( string [ ] args )
    {
    Lei < int > Xfirst = new ( 1 );
    Console . WriteLine ( $"{Xfirst . FFfirst}" );
    Lei < double > Xsecond = new ( double . Pi );
    Console . WriteLine ( $"{Xsecond . FFfirst}" );
    }
}

上例输出:
1
3.141592653589793

继承

类继承其直接基类的成员。继承意味着类隐式地包含其直接基类的所有成员,但基类的实例构造函数、终结器和静态构造函数除外。继承的一些重要方面是:

  • 继承是可传递的。如果 C 从 B 派生,B 从 A 派生,那么 C 既继承 B 中声明的成员,也继承 A 中声明的成员。
  • 派生类继承其直接基类。派生类可以向其继承的成员添加新成员,但不能删除继承成员的定义。
  • 实例构造函数、终结器和静态构造函数不被继承,但所有其他成员都可以继承,不管它们声明的可访问性如何。但是,根据其声明的可访问性,继承的成员可能无法在派生类中访问。
  • 派生类可以通过声明具有相同名称或签名的新成员来隐藏继承的成员。但是,隐藏继承的成员并不会删除该成员,它只是使该成员无法通过派生类直接访问。
  • 类的实例包含类及其基类中声明的所有实例字段的集合,并且存在从派生类类型到其任何基类类型的隐式转换。因此,对某个派生类实例的引用可以被视为对其任何基类实例的引用。
  • 类可以声明虚方法、属性、索引器和事件,派生类可以覆盖这些函数成员的实现。这使类能够显示多态行为,其中函数成员调用执行的操作取决于调用该函数成员的实例的运行时类型。

构造类类型的继承成员是直接基类类型的成员,通过替换基类规范中每次出现的相应类型形参的构造类型的类型实参来找到的。反过来,通过将成员声明中的每个类型形参替换为基类规范的对应类型实参来转换这些成员。

class B < U >
{
    public U F ( long index ) { }
}

class D < T > : B < T [ ] >
{
    public T G ( string zfc ) { }
}

在上面的代码中,构造类型 D < int > 有一个非继承成员 public int G ( string zfc ),通过将类型实参 int 替换为类型形参 T 而获得。D < int > 还有一个来自类声明 B 的继承成员。这个继承成员首先通过将基类规范 B < T [ ] > 中的 T 替换为 int 来确定 D < int > 的基类类型 B < int [ ] >。然后,作为 B 的类型参数,int [ ] 被替换为 public U F ( long index ) 中的 U,从而产生继承的成员 public int [ ] F ( long index )。

New 修饰符

允许类成员声明声明具有与继承成员相同的名称或签名的成员。发生这种情况时,就说派生类成员隐藏基类成员。

如果 M 是可访问的,并且没有其他继承的可访问成员 N 已经隐藏了 M,则继承的成员 M 被认为是可用的。隐式地隐藏继承的成员不被认为是错误,但编译器将发出警告,除非派生类成员的声明包含一个 new 修饰符,以显式地表明派生成员有意隐藏基成员。如果嵌套类型的部分声明的一个或多个部分包含 new 修饰符,则如果嵌套类型隐藏了可用的继承成员,则不会发出警告。

如果在没有隐藏可用继承成员的声明中包含了 new 修饰符,则会发出类似的警告。

访问修饰符

类成员声明可以具有任何一种允许声明的可访问性:public、protected internal、protected、private protected、internal 或 private。除了 protected internal 和 private protected 组合外,指定多个访问修饰符会导致编译时错误。如果类成员声明不包含任何访问修饰符,则假定为 private。

组成类型

在成员声明中使用的类型称为该成员的组成类型。可能的组成类型包括常量、字段、属性、事件或索引器的类型,方法或操作符的返回类型,以及方法、索引器、操作符或实例构造函数的参数类型。成员的组成类型至少应与该成员本身具有同样的可访问性。

静态成员与实例成员

类的成员要么是静态成员,要么是实例成员。

注意:一般来说,将静态成员视为属于类,将实例成员视为属于对象(类的实例)是有用的。

当字段、方法、属性、事件、操作符或构造函数声明包含静态修饰符时,它声明静态成员。此外,常量或类型声明隐式地声明静态成员。静态成员具有以下特点:

  • 当静态成员 M 在 E . M 形式的成员访问中被引用时,E 应表示具有成员 M 的类型。如果 E 表示实例,则会导致编译时错误。
  • 非泛型类中的静态字段仅标识一个存储位置。无论创建了多少个非泛型类的实例,静态字段都只有一个副本。每个不同的闭构造类型都有自己的一组静态字段,与闭构造类型的实例数量无关。
  • 静态函数成员(方法、属性、事件、操作符或构造函数)不对特定实例进行操作,在这样的函数成员中引用 this 会导致编译时错误。

当字段、方法、属性、事件、索引器、构造函数或终结器声明不包括静态修饰符时,它声明一个实例成员(实例成员有时称为非静态成员)。实例成员具有以下特征:

  • 当一个实例成员 M 在 E . M 形式的成员访问中被引用时,E 应该表示一个具有成员 M 的类型的实例。如果 E 表示一个类型,会导致绑定时间错误。
  • 类的每个实例都包含该类的所有实例字段的单独集合。
  • 实例函数成员(方法、属性、索引器、实例构造器或终结器)在类的给定实例上操作,并且可以像 this 那样访问该实例。
class JingTaiYuShiLi
    {
    int x;
    static int y;
        
    void FF ( )
        {
         x = 1; // 可以是 this . x = 1
         y = 1; // 可以是 JingTaiYuShiLi . y = 1
         }

    static void FFj ( )
        {
         // x = -1; // 警告 CS0120:对象引用对于非静态的字段、方法或属性“JingTaiYuShiLi . x”是必需的(意即静态方法不能使用实例字段)
         y = -1; 
         }

    static void Main ( )
        {
        JingTaiYuShiLi JS = new ( );
        JS . x = 2; // 实例可以访问实例成员
        // JS . y = 2; // 警告 CS0176:无法使用实例引用来访问成员“JingTaiYuShiLi . y”:请改用类型名来限定它(意即实例不能访问静态成员)
        // JingTaiYuShiLi . x = 3; // 警告 CS0120:对象引用对于非静态的字段、方法或属性“JingTaiYuShiLi . x”是必需的(意即类不具备 x 字段,因为它是非静态的)
        JingTaiYuShiLi . y = 3; // 类限定名可以访问静态字段
    }
}

FF 方法表明,在实例函数成员中,可以使用简单的名称访问实例成员和静态成员。FFj 方法表明,在静态函数成员中,通过简单名称访问实例成员是一个编译时错误。Main 方法表明,在成员访问中,实例成员应通过实例访问,静态成员应通过类型访问。

嵌套类型

嵌套类和非嵌套类

在类或结构中声明的类型称为嵌套类型。在编译单元或命名空间内声明的类型称为非嵌套类型。

class Wai
    {
    public static void FFw ( )
        {
        Nei N = new ( );
        N . FFn ( );
        }
    class Nei
        {
        public void FFn ( )
            {
            Console . WriteLine ( "Wai . Nei . FFn" );
            }
        }
    }

类 Nei 是嵌套类型,因为它是在类 Wai 中声明的,而类 Wai 是非嵌套类型,因为它是在编译单元中声明的。

完全限定名

嵌套类型声明的完全限定名是 Wai . Nei,其中 Wai 是声明类型 Nei 的类型声明的完全限定名,Nei 是嵌套类型声明(包括任何泛型维度说明符)的非限定名。

声明可访问性

非嵌套类型可以具有 public 或 internal 声明的可访问性,默认情况下具有 internal 声明的可访问性。嵌套类型也可以有这些形式的可访问性声明,再加上一个或多个附加形式的可访问性声明,这取决于包含类型是类还是结构:

  • 在类中声明的嵌套类型可以具有任何允许的声明可访问性类型,并且与其他类成员一样,默认为 private 声明的可访问性。
  • 在结构体中声明的嵌套类型可以具有三种声明的可访问性形式中的任何一种(public、internal 或 private),并且与其他结构体成员一样,默认为 private 声明的可访问性。

上例中的 Nei 类即为默认的 private 访问修饰符,即只有在 Wai 类中,才能声明 Nei 类型的实例或访问 Nei 类型的静态成员。

若 Nei 的访问修饰符为 public,则在程序集中,可以使用 Wai . Nei N = new ( ); 声明一个名为 N 的 Wai . Nei 的实例。

隐藏

嵌套类型可以隐藏基成员。new 修饰符允许用于嵌套类型声明,以便可以显式地表示隐藏。

class 基类
{
    public static void 基类方法 ( )
    {
        Console . WriteLine ( "基类 . 基类方法" );
    }
}

class 派生类 : 基类
{
    public new class 基类方法
    {
        public static void 派生类方法 ( )
        {
            Console . WriteLine ( "派生类 . 基类方法 . 派生类方法" );
        }
    }
}

使用 派生类 . 基类方法 . 派生类方法 输出:
派生类 . 基类方法 . 派生类方法
显示了一个嵌套类“基类方法”,它隐藏了在“基类”中定义的方法“基类方法”。

this 访问

嵌套类型和包含它的类型在 this 访问上没有特殊的关系。具体来说,嵌套类型中的 this 不能用于引用包含类型的实例成员。在嵌套类型需要访问其包含类型的实例成员的情况下,可以通过为包含类型的实例提供 this 作为嵌套类型的构造函数参数来提供访问。

class 基类
    {
        int z = 123;

    public void 基类方法 ( )
        {
        Lei嵌套 Q = new ( this);
                Q . FF ( );
        }

        public class Lei嵌套
            {
            基类 Ji;
            public Lei嵌套 ( 基类 ji )
                {
        Ji = ji;
        }

            public void FF ( )
                {
                Console . WriteLine ( Ji . z );
                }
    }
}

“基类”的实例创建了一个“Lei嵌套”的实例,并将自己的 this 传递给“Lei嵌套”的构造函数,以便提供对“基类”实例成员的后续访问。

对包含类型的私有和受保护成员的访问

嵌套类型可以访问其包含类型可访问的所有成员,包括包含类型中具有私有和受保护的声明可访问性的成员。

class Lei外
     {
     private static void FFw ( ) => Console . WriteLine ( "Lei外 . FFw" );

     public class Lei内
         {
         public static void FFn ( ) => FFw ( );
         }
     }

显示了包含嵌套类“Lei内”的类“Lei外”。在“Lei内”中,方法 FFn 调用“Lei外”中定义的静态方法 FFw,并且 FFw 具有 private 声明的可访问性。

class 基类
    {
    public void 基类方法 ( ) => Console . WriteLine ( "基类 . 基类方法" );
    }

class 派生类 : 基类
    {
    public class 嵌套类
        {
    public void 嵌套类方法 ( )
        {
                派生类 Ps = new ( );
                Ps . 基类方法 ( );
        }
    }
}

上例中的 派生类 . 嵌套类 通过访问派生类的实例,访问基类的 protected 方法“基类方法”。

泛型类中的嵌套类型

泛型类声明可以包含嵌套类型声明。封闭类的类型参数可以在嵌套类型中使用。嵌套类型声明可以包含仅应用于该嵌套类型的附加类型参数。

泛型类声明中包含的每个类型声明都是隐式泛型类型声明。当编写对嵌套在泛型类型中的类型的引用时,包含它的构造类型,包括它的类型参数,应该被命名。然而,在外部类中,可以不加限定地使用嵌套类型;外部类的实例类型可以在构造嵌套类型时隐式使用。

示例:下面展示了三种不同的正确方法来引用从Inner;前两个是等价的:

class Wai
    {
    class Nei
        {
        public static void FFn ( T t , U u )
            {
            }
        }

    static void FFw ( T t )
        {
        Wai . Nei . FFn ( t , "abc" );
        Nei . FFn( t , "abc" ); // 两个表达式相同的效果
        Wai . Nei . FFn ( 3 , "ABC" );
        Wai . Nei . FFn ( t , "abc" ); // 表达式有错误,Wai 需要类型参数
        }
    }

尽管这是一种糟糕的编程风格,但嵌套类型中的类型参数可以隐藏在外部类型中声明的成员或类型参数。

class Outer
{
    class Inner // 有效,隐藏 Outer 的 T
    {
        public T t; // 引用的是 Inner 的 T
    }
}

保留成员名称

为了方便底层 C# 运行时实现,对于每个作为属性、事件或索引器的源成员声明,实现应该根据成员声明的种类、名称和类型保留两个方法签名。如果程序声明的成员的签名与在同一作用域中声明的成员保留的签名相匹配,即使底层运行时实现没有使用这些保留,也会导致编译时错误。

保留名不引入声明,因此它们不参与成员查找。然而,声明关联的保留方法签名参与继承,并且可以用新的修饰符隐藏。

保留这些名称有三个目的:

  1. 允许底层实现使用普通标识符作为方法名,用于获取或设置对 C# 语言特性的访问。
  2. 允许其他语言使用普通标识符作为获取或设置访问 C# 语言特性的方法名进行互操作。
  3. 通过使保留成员名的细节在所有 C# 实现中保持一致,帮助确保一个符合标准的编译器接受的源代码被另一个编译器接受。

终结器(finalizer)的声明也会导致保留签名。

保留某些名称作为操作符方法名称。

为属性保留的成员名

对于 T 类型的属性 P,保留以下签名:

T get_P();
void set_P(T value);

这两个签名都是保留的,即使属性是只读的或只写的。

class 基类
    {
    public int Zhs => 123;
    }

class 派生类 : 基类
    {
    public int HZhs ( ) => 456;
    public void SZhs ( int 值 )
          {
          }
    }

        派生类 P = new ( );
        基类 J = P;
        Console . WriteLine ( J . Zhs );
        Console . WriteLine ( P . Zhs );
        Console . WriteLine ( P . HZhs ( ) );

类“基类”定义了一个只读属性 Zhs,因此为 HZhs 和 SZhs 方法保留了签名。类“派生类”派生自“基类”,并隐藏了这两个保留签名。上例输出:
123
123
456

为事件保留的成员名

对于委托类型 T 的事件 E,保留以下签名:

void TianJia_E(T handler);
void YiChu_E(T handler);

为索引器保留的成员名

对于带有形参列表 L 的类型 T 的索引器,保留以下签名:

T HXM(L);
void SXM(L, T value);

这两个签名都是保留的,即使索引器是只读的或只写的。

此外,成员名 XM 是保留的。

为终结器保留的成员名

对于包含终结器的类,保留以下签名:
void Finalize();

为操作符保留的方法名

保留以下方法名称。虽然许多操作符在本规范中有相应的操作符,但有些操作符保留供未来版本使用,而有些操作符保留用于与其他语言的互操作。

方法 C# 操作符
op_Addition + (二进制)
op_AdditionAssignment (保留)
op_AddressOf (保留)
op_Assign (保留)
op_BitwiseAnd &(二进制)
op_BitwiseAndAssignment (保留)
op_BitwiseOr \
op_BitwiseOrAssignment (保留)
op_CheckedAddition (留作将来使用)
op_CheckedDecrement (留作将来使用)
op_CheckedDivision (留作将来使用)
op_CheckedExplicit (留作将来使用)
op_CheckedIncrement (留作将来使用)
op_CheckedMultiply (留作将来使用)
op_CheckedSubtraction (留作将来使用)
op_CheckedUnaryNegation (留作将来使用)
op_Comma (保留)
op_Decrement --(前缀或后缀)
op_Division /
op_DivisionAssignment (保留)
op_Equality ==
op_ExclusiveOr ^
op_ExclusiveOrAssignment (保留)
op_Explicit 强制(显式)缩小
op_False false
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Implicit 强制(显式)扩大
op_Increment ++(前缀或后缀)
op_Inequality !=
op_LeftShift <<
op_LeftShiftAssignment (保留)
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd (保留)
op_LogicalNot !
op_LogicalOr (保留)
op_MemberSelection (保留)
op_Modulus %
op_ModulusAssignment (保留)
op_MultiplicationAssignment (保留)
op_Multiply *(二进制)
op_OnesComplement ~
op_PointerDereference (保留)
op_PointerToMemberSelection (保留)
op_RightShift >>
op_RightShiftAssignment (保留)
op_SignedRightShift (保留)
op_Subtraction -(二进制)
op_SubtractionAssignment (保留)
op_True true
op_UnaryNegation -(一元的)
op_UnaryPlus +(一元的)
op_UnsignedRightShift (留作将来使用)
op_UnsignedRightShiftAssignment (保留)

常量

常量是表示常数值的类成员:可在编译时计算的值。常量声明引入一个或多个给定类型的常量。

常量声明可能包括一组属性、修饰符(new),以及任何一种允许的声明辅助功能。属性和修饰符适用于常量声明声明的所有成员。 即使常量被视为静态成员,常量声明既不需要也不允许 static 修饰符。同一修饰符在常量声明中出现多次是错误的。

常量声明的类型指定声明引入的成员的类型。 该类型后跟常量声明,每个列表都会引入一个新成员。常量声明包含一个标识符,该标识符命名成员,后跟一个“”令牌,后跟一个“=”。

常量声明中指定的类型应为 sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、枚举(enum)或用户自定义类型。 每个常量表达式应生成一个目标类型或一种类型的值,该类型可以通过隐式转换转换为目标类型。

常量的类型至少应与常量本身具有同样的可访问性。

常量的值可以通过简单名称或成员访问在表达式中获得。

常数本身可以参与常数表达式。因此,常数可以用在任何需要常数表达式的结构中。

注意:这种结构的例子包括 case 标签、goto case 语句、枚举成员声明、属性和其他常量声明。

注意:常量表达式是可以在编译时完全求值的表达式。由于创建非 string 引用类型的非空值的唯一方法是应用 new 操作符,并且由于在常量表达式中不允许使用 new 操作符,因此非 string 引用类型的常量唯一可能的值是 null。

当需要常量值的符号名称,但常量声明中不允许该值的类型,或者常量表达式不能在编译时计算该值时,可以使用只读字段代替。

注意:const 和 readonly 的版本控制语义不同。

声明多个常量的常量声明相当于对具有相同属性、修饰符和类型的单个常量的多个声明。

class Lei
{
    public const double X = 1.0 , Y = 2.0 , Z = 3.0;
}
// 等效于:
class Lei
{
    public const double X = 1.0;
    public const double Y = 2.0;
    public const double Z = 3.0;
}

允许常量依赖于同一程序中的其他常量,只要依赖关系不是循环的。

class A
{
    public const int X = B . Z + 1;
    public const int Y = 10;
}

class B
{
    public const int Z = A . Y + 1;
}

编译器必须先求 A . Y,然后求 B . Z,最后求 A . X,生成值 10、11 和 12。

常量声明可能依赖于其他程序中的常量,但这种依赖只能在一个方向上实现。

示例:参考上面的例子,如果 A 和 B 在单独的程序中声明,则 A . X 可能依赖 B . Z,但 B . Z则不能同时依赖 A . Y。

字段

字段是表示与对象或类关联的变量的成员。字段声明引入一个或多个给定类型的字段。

Unsafe 修饰符仅在不安全代码中可用。

字段声明可以包括一组属性、一个 new 修饰符、四个访问修饰符的有效组合和一个 static 修饰符。此外,字段声明可以包括 readonly 或 volatile 修饰符,但不能同时包含这两种修饰符。属性和修饰符适用于字段声明声明的所有成员。同一个修饰符在字段声明中多次出现是错误的。

字段声明的类型指定由声明引入的成员的类型。类型后面是一个变量声明列表,每个变量都引入一个新成员。变量声明由一个标识符组成,该标识符命名该成员,可选地后跟一个“=”令牌和一个“变量初始器”,它给出该成员的初始值。

字段的类型至少应该和字段本身一样可访问。

字段的值可以在表达式中使用简易名称、成员访问或基类继承访问获得。非只读字段的值是通过赋值来修改的。非只读字段的值可以使用后置自增和自减操作符和前缀自增和自减操作符来获取和修改。

声明多个字段的字段声明相当于对具有相同属性、修饰符和类型的单个字段的多个声明。

class Lei
{
    public static double X = 1.0 , Y , Z = 3.0;
}
// 等效于:
class Lei
{
    public static double X = 1.0;
    public static double Y;
    public static double Z = 3.0;
}

静态字段和实例字段

当字段声明包含 static 修饰符时,由声明引入的字段是静态字段。当不存在 static 修饰符时,由声明引入的字段是实例字段。静态字段和实例字段是 C# 支持的几种变量中的两种,有时它们分别被称为静态变量和实例变量。

类的每个实例包含类的一套完整的实例字段,而每个非泛型类或封闭构造类型只有一套静态字段,与类或封闭构造类型的实例数量无关。

只读字段

当字段声明包含 readonly 修饰符时,由声明引入的字段是只读字段。对只读字段的直接赋值只能作为该声明的一部分或在同一类的实例构造函数或静态构造函数中发生(在这些上下文中,一个只读字段可以被多次赋值)。具体来说,只允许在以下上下文中直接赋值给只读字段:

  • 在引入字段的变量声明中(通过在声明中包含变量初始器)。
  • 对于实例字段,在包含字段声明的类的实例构造函数中;对于静态字段,在包含字段声明的类的静态构造函数中。在这些上下文中,将只读字段作为输出或引用参数传递是有效的。
  • 试图给只读字段赋值或在任何其他上下文中将其作为输出或引用参数传递都会导致编译时错误。
为常量使用 static readonly 字段

当需要常量值的符号名称,但在 const 声明中不允许该值的类型,或者在编译时无法计算该值时,静态只读字段很有用。

public class 颜色
{
    public static readonly Color 黑 = new Color(0, 0, 0);
    public static readonly Color 白 = new Color(255, 255, 255);
    public static readonly Color 红 = new Color(255, 0, 0);
    public static readonly Color 绿 = new Color(0, 255, 0);
    public static readonly Color 蓝 = new Color(0, 0, 255);

    private byte H, Lv, L;

    public Color(byte 红z, byte 绿z, byte 蓝z)
    {
        H = 红z;
        Lv = 绿z;
        L = 蓝z;
    }
}

黑、白、红、绿和蓝成员不能声明为常量成员,因为它们的值不能在编译时计算。然而,将它们声明为 static readonly 会产生相同的效果。

常量和静态只读字段的版本控制

常量和只读字段具有不同的二进制版本语义。当表达式引用常量时,在编译时获得常量的值,但当表达式引用只读字段时,直到运行时才获得该字段的值。

示例:考虑一个由两个独立程序组成的应用程序:

namespace DM1
{
    public class 操作
    {
        public static readonly int x = 1;
    }
}

namespace DM2
{
    class 测试
    {
        static void Main()
        {
            Console . WriteLine ( DM1 . 操作 . x );
        }
    }
}

DM1 和 DM2 命名空间表示分别编译的两个程序。因为 DM1 . 操作 . x 被声明为 static readonly 字段,所以 Console . WriteLine 语句在编译时是未知的,而是在运行时获得的。因此,如果更改了 x 的值并重新编译了 DM1,即使 DM2 没有重新编译,Console . WriteLine 语句也会输出新值。然而,如果 x 是一个常量,那么 x 的值将在编译 DM2 时获得,并且不会受到 DM1 中的更改的影响,直到 DM2 被重新编译。

Volatile 字段

当字段声明包含 volatile 修饰符时,该声明引入的字段是 volatile 字段。对于非易失性字段,重新排序指令的优化技术可能会导致多线程程序在没有同步访问字段的情况下产生意想不到的和不可预测的结果,例如 lock 语句提供的。这些优化可以由编译器、运行时系统或硬件执行。对于 volatile 字段,这样的重新排序优化是受限的:

  • 对 volatile 字段的读取称为易失性读取。易失读具有“获取语义”;也就是说,它保证发生在指令序列中对内存的任何引用之前。
  • 对 volatile 字段的写入称为易失性写入。易失性写具有“释放语义”;也就是说,它保证发生在指令序列中写指令之前的任何内存引用之后。

这些限制确保所有线程都将按照执行顺序观察任何其他线程执行的易失性写操作。符合标准的实现不需要提供从所有执行线程中看到的 volatile 写入的单一总顺序。易失性字段的类型应为下列类型之一:

  • 引用类型。
  • 已知为引用类型的类参。
  • 类型 byte,sbyte,short,ushort,int,uint,char,float,bool,System . IntPtr 或者 System . UIntPtr。
  • 枚举(enum),其 enum 基类为 byte、sbyte、short、ushort、int 或 uint。
class CeShi
    {
    public static int 结果;
    public static volatile bool 结束;

    public static void 进程 ( )
        {
        结果 = 235;
        结束 = true;
        }
    }

CeShi . 结束 = false;

// 在新线程中运行测试类的进程
new Thread ( new ThreadStart ( CeShi . 进程 ) ) . Start ( );

// 等待 CeShi . 进程 ( ) 通过将“结束”设置为 true 来表示它有结果
for ( ; ; )
    {
     if ( CeShi . 结束 )
          {
          Console . WriteLine ( $"结果 = {CeShi . 结果}" );
          break;
          }
     }

在这个例子中,启动了一个运行“进程”方法的新线程。该方法将一个值存储到一个名为“结果”的非易失性字段中,然后将 true 存储在易失性字段“结束”中。主线程等待字段“结束”设置为 true,然后读取字段结果。由于“结束”已被声明为 volatile,主线程应从字段结果中读取值 235。如果字段“结束”没有被声明为 volatile,那么在存储结束后,允许主线程对存储结果可见,因此主线程可以从字段结果中读取值 0。将“结束”声明为 volatile 字段可以防止任何此类不一致。

字段初始化

字段的初始值,无论是静态字段还是实例字段,都是字段类型的默认值。在此默认初始化发生之前,不可能观察到字段的值,因此字段永远不会“未初始化”。

变量初始化

字段声明可以包含变量初始化式。对于静态字段,变量初始化式对应于类初始化期间执行的赋值语句。对于实例字段,变量初始化项对应于在创建类的实例时执行的赋值语句。

默认值初始化适用于所有字段,包括具有变量初始化器的字段。因此,在初始化类时,该类中的所有 static 字段首先初始化为其默认值,然后按文本顺序执行 static 字段初始化器。同样,当创建类的实例时,首先将该实例中的所有实例字段初始化为其默认值,然后按文本顺序执行实例字段初始化器。当同一类型的多个部分类型声明中有字段声明时,部分的顺序是不指定的。但是,在每个部分中,字段初始化式是按顺序执行的。

具有变量初始化器的 static 字段可能处于默认值状态。

静态域初始化

类的 static 字段变量初始化项对应于一系列赋值,这些赋值按照它们在类声明中出现的文本顺序执行。在分部类中,“文本顺序”的含义由上一节规定。如果类中存在静态构造函数,则在执行静态构造函数之前立即执行静态字段初始化式。否则,静态字段初始化器将在该类的静态字段首次使用之前,在与实现相关的时间执行。

class CeShi
    {
    public override string ToString ( )
        {
    return ( $"{LeiA . X}\t{LeiB . Y}" );
    }

    public static int FFc ( string 字符串 )
        {
        Console . WriteLine ( 字符串 );
        return 1;
        }
    }

class LeiA
    {
    public static int X = CeShi . FFc ( "初始化 A" );
    }

    class LeiB
    {
    public static int Y = CeShi . FFc ( "初始化 B" );
    }

因为 X 的初始化式和 Y 的初始化式的执行顺序可以任意选择;它们只被限制在对这些字段的引用之前。

实例字段初始化

类的实例字段变量初始化项对应于在进入该类的任何一个实例构造函数时立即执行的赋值序列。在分部类中,“文本顺序”的含义由“变量初始化”一节规定。变量初始化式按照它们在类声明中出现的文本顺序执行。
实例字段的变量初始化项不能引用正在创建的实例。因此,在变量初始化器中引用 this 是编译时错误,正如变量初始化器通过简单名称引用任何实例成员是编译时错误一样。

方法

方法是实现可由对象或类执行的计算或操作的成员。使用方法声明来声明方法。

语法笔记:

  • Unsafe_modifier 仅在不安全代码中可用。
  • 在识别 method_body 时,如果 null 条件调用表达式和表达式替代都适用,则应选择前者。
    注意:这里选择的重叠和优先级只是为了便于描述;可以对语法规则进行细化以消除重叠。ANTLR 和其他语法系统采用了同样的便利,因此方法体自动具有指定的语义。

方法声明可以包含一组属性和一种允许声明的可访问性、new、static、virtual、override、sealed、abstract、extern 和 async 修饰符。

如果满足以下所有条件,则声明具有有效的修饰符组合:

  • 声明包括访问修饰符的有效组合。
  • 声明不能多次包含相同的修饰符。
  • 声明最多包括以下修饰符之一:static、virtual 和 override。
  • 声明最多包括以下修饰符中的一个:new 和 override。
  • 如果声明包含 abstract 修饰符,则声明不包含以下任何修饰符:static、virtual、sealed 或 extern。
  • 如果声明包含 private 修饰符,则声明不包含以下任何修饰符:virtual、override 或 abstract。
  • 如果声明包含 sealed 修饰符,则声明还包括 voerride 修饰符。
  • 如果声明包含 partial 修饰符,则它不包含以下任何修饰符:new、public、protected、internal、private、virtual、sealed、override、abstract 或 extern。

方法根据它们返回的内容(如果有的话)进行分类:

  • 如果 ref 存在,则该方法是按 ref 返回并返回一个变量引用,该变量引用可选为只读;
  • 否则,如果返回类型为 void,则该方法不返回任何值;
  • 否则,该方法按值返回并返回一个值。

按值返回或无值返回方法声明的返回类型指定了该方法返回的结果(如果有的话)的类型。只有不返回值的方法可以包含 partial 修饰符。如果声明包含 async 修饰符,则返回类型应为 void,或者方法按值返回,返回类型为 task 类型。

返回 ref 方法声明的引用返回类型指定了该方法返回的变量引用引用的变量的类型。

泛型方法是其声明包含类参列表的方法。这指定了方法的类型参数。可选的类参约束子句指定类型参数的约束。

显式接口成员实现的泛型方法声明不能有任何类参约束子句;声明从接口方法上的约束继承任何约束。

类似地,带有重写修饰符的方法声明不能有任何类参约束子句,方法类型参数的约束继承自被重写的虚方法。

方法名指定方法的名称。除非方法是显式接口成员实现,否则方法名只是一个标识符。

对于显式接口成员实现,方法名由一个 interface 类型后面跟着一个“.”和标识符。在这种情况下,除了(可能)extern 或 async 之外,声明不应包含任何修饰符。

可选的参数列表指定方法的参数。

返回类型或引用返回类型,以及在方法的参数列表中引用的每个类型,至少应该与方法本身一样可访问。

按值返回或不返回值的方法的方法体可以是分号、块体或表达式体。块体由一个块组成,该块指定调用方法时要执行的语句。表达式体由“=>”组成,后面跟着一个空条件调用表达式或表达式,以及一个分号,表示调用方法时要执行的单个表达式。

对于 abstract 和 extern 方法,方法体仅由一个分号组成。对于 partial 方法,方法体可以由分号、块体或表达式体组成。对于所有其他方法,方法体要么是块体,要么是表达式体。

如果方法体由分号组成,则声明中不应包含 async 修饰符。

引用返回类型方法的 ref 方法体可以是分号、块体或表达式体。块体由一个块组成,该块指定调用方法时要执行的语句。表达式主体由“=>”组成,后面跟着 ref、引用变量和分号,表示调用方法时要计算的单个引用变量。

对于 abstract 和 extern 方法,引用方法体仅由一个分号组成;对于所有其他方法,引用方法体要么是块体,要么是表达式体。

方法的名称、类型参数的数量和参数列表定义了方法的签名。具体地说,方法的签名包括它的名称、它的类型参数的数量,以及它的参数的数量、参数模式修饰符和类型。返回类型不是方法签名的一部分,参数的名称、类型参数的名称或约束的名称也不是。当参数类型引用方法的类型参数时,类型参数的顺序位置(而不是类型参数的名称)用于类型等价。

方法的名称应与同一类中声明的所有其他非方法的名称不同。此外,一个方法的签名必须不同于在同一类中声明的所有其他方法的签名,并且在同一类中声明的两个方法的签名不能仅仅是 In、out 和 ref 不同。

方法的类参在整个方法声明的作用域中,并且可以在返回类型或引用返回类型、方法体或引用方法体以及类参约束子句中用于在整个作用域中形成类型,但不能在 attributes 中使用。

所有参数和类型参数应具有不同的名称。

方法参数

方法的参数(如果有的话)由方法的参数列表声明。

参数列表由一个或多个以逗号分隔的参数组成,其中只有最后一个参数可以是参数数组。

固定(fixed)参数由一组可选的属性组成;可选的 in、out、ref 或 this 修饰符;一个类型;一个标识符;还有一个可选的默认参数(default)。每个固定参数用给定的名称声明一个给定类型的参数。this 修饰符将方法指定为扩展方法,并且只允许在非泛型、非嵌套 static 类中的 static 方法的第一个参数上使用。如果形参是 struct 类型或约束于 struct 的类型形参,则 this 修饰符可以与 ref 或 in 修饰符组合,但不能与 out 修饰符组合。带默认参数的固定参数被称为可选参数,而不带默认参数的固定参数是必选参数。必需参数不能出现在参数列表中的可选参数之后。

带有 ref、out 或 this 修饰符的形参不能有默认参数。输入形参可以有一个默认参数。默认参数中的表达式应该是下列之一:

  • 一个常量表达式(const)
  • new S() 形式的表达式,其中 S 是一个值类型
  • 形式为 default(S) 的表达式,其中 S 是一个值类型

表达式应通过单位或可空转换隐式转换为参数类型。

如果可选形参出现在实现 partial 方法声明、显式 interface 成员实现、单形参 indexer 声明或 operator 声明中,编译器应该给出警告,因为这些成员永远不能以允许省略实参的方式调用。

参数数组由一组可选的属性、一个参数修饰符、一个 array 类型和一个标识符组成。参数数组用给定的名称声明给定数组类型的单个参数。形参数组的 array 类型必须是一维数组类型。在方法调用中,参数数组允许指定给定数组类型的单个参数,或者允许指定数组元素类型的零个或多个参数。

参数数组可以出现在可选参数之后,但不能有默认值-省略参数数组的参数反而会导致创建空数组。

void M(
    ref int i,
    decimal d,
    bool b = false,
    bool? n = false,
    string s = "Hello",
    object o = null,
    T t = default(T),
    params int[] a
) { }

在方法 M 的参数列表中,i 为必选引用(ref)参数,d 为必选值参数,b、s、o 和 t 为可选值参数,a 为参数数组。

方法声明为参数和类型参数创建了一个单独的声明空间。名称通过类型参数列表和方法的参数列表引入该声明空间。方法体(如果有的话)被认为嵌套在这个声明空间中。方法声明空间的两个成员具有相同的名称是错误的。

方法调用创建一个特定于该调用的方法的形参和局部变量的副本,并且调用的参数列表为新创建的形参分配值或变量引用。在方法块中,参数可以通过简单名称表达式中的标识符引用。

存在以下几种参数:

  • 值参数。
  • 输入参数。
  • 输出参数。
  • 引用参数。
  • 参数数组。

注意:in、out 和 ref 修饰符是方法签名的一部分,但 params 修饰符不是。

值参数

没有修饰符声明的参数是值参数。值形参是一个局部变量,它从方法调用中提供的相应参数中获取其初始值。

方法调用中对应的实参应该是可隐式转换为形参类型的表达式。

允许方法将新值赋给值参数。这样的赋值只影响由值形参表示的本地存储位置-它们对方法调用中给出的实际实参没有影响。

按引用(ref)调用参数

输入、输出和引用参数是按引用的参数。按引用形参是一个局部引用变量;初始引用从方法调用中提供的相应参数中获得。

注意:引用形参的引用对象可以使用 ref 赋值(= ref)操作符进行更改。

当形参是按引用形参时,方法调用中相应的实参应由相应的关键字 in、ref 或 out 组成,后跟与形参类型相同的引用变量。然而,当形参是 in 形参时,实参可以是一个表达式,该表达式存在从该实参表达式到相应形参类型的隐式转换。

在声明为迭代器(iterator)或 async 函数的函数中不允许使用引用形参。

在采用多个按引用参数的方法中,可以使用多个名称表示相同的存储位置。

输入参数

用 in 修饰符声明的参数是输入参数。与输入形参相对应的实参要么是方法调用时存在的变量,要么是在方法调用中由实现创建的变量。

修改输入参数的值是编译时错误。

注:输入参数的主要目的是为了提高效率。当方法形参的类型是一个大结构体(就内存需求而言)时,在调用方法时避免复制实参的整个值是很有用的。输入参数允许方法引用内存中的现有值,同时提供保护,防止对这些值进行不必要的更改。

引用参数

用 ref 修饰符声明的形参是引用形参。

static void Main ( string [ ] args )
    {
    int i = 1 , j = 2;
    JiaoHuan ( ref i , ref j );
    Console . WriteLine ( $"i = {i};j = {j}" );
    }

static void JiaoHuan ( ref int x , ref int y )
    {
    (y, x) = (x, y);
    }

上例输出:
i = 2;j = 1
对于 Main 中的 JiaoHuan 调用,x 表示 i, y 表示 j。因此,调用具有交换 i 和 j 的值的效果。

static void Main ( string [ ] args )
    {
    string Yi = "";

    void FF1 ( ref string Er , ref string San )
        {
        Yi = "1";
        Er = "2";
        San = "3";
    Console . WriteLine ( $"Yi = {Yi};Er = {Er};San = {San}" );
    }

    void FF2 ( )
        {
        FF1 ( ref Yi , ref Yi );
        }
    FF2 ( );
    Console . WriteLine ( $"Yi = {Yi}" );
    }

上例输出:
Yi = 3;Er = 3;San = 3
Yi = 3
在 FF2 中调用 FF1 将 Er 和 San 的引用传递给 Yi。因此,对于该调用,名称 Yi、Er 和 San 都指向相同的存储位置,并且三个赋值都修改了实例字段 Yi。

对于结构类型,在实例方法、实例访问器或带有构造函数初始化项的实例构造函数中,this 关键字完全充当结构类型的引用形参。

输出参数

用 out 修饰符声明的参数是输出参数。

声明为 partial 方法的方法不能有输出参数。

注意:输出参数通常用于产生多个返回值的方法。

static void Main ( string [ ] args )
    {
    string dir , name; // 声明两个字符串,保存文件路径和文件名
    FGLJ ( @"C:\Windows\System\win.exe" , out dir , out name ); // 调用分隔路径方法分隔给定的路径
    Console . WriteLine ( $"文件夹:{dir}\n文件名:{name}" );
    }

/// 
/// 分隔路径方法,返回路径中的文件夹和文件名
/// 
/// 欲分隔的路径
/// 方法完成后保存文件夹
/// 方法完成后保存文件名
private static void FGLJ ( string 路径 , out string 文件夹 , out string 文件名 )
    {
    文件夹 = "";
    文件名 = "";
    int Zhs = 路径 . Length;
    while ( Zhs > 0 )
        {
        char Zf = 路径 [ Zhs - 1 ];
        if ( Zf == '\\' || Zf == '/' || Zf == ':' )
            {
            break;
            }
        Zhs--;
        文件夹 = 路径 [ ..Zhs ];
        文件名 = 路径 [ Zhs.. ];
        }
    }

上例输出:
文件夹:C:\Windows\System\
文件名:win.exe

请注意,dir 和 name 变量可以在传递给“FGLJ”之前取消赋值,并且它们被认为是在调用之后明确赋值的。

参数数组

用参数修饰符声明的参数是参数数组。如果参数列表中包含参数数组,则该参数应为列表中的最后一个参数,且该参数应为一维数组类型。

例如:string[] 和 string[][] 类型可以作为参数数组的类型,string[ , ] 类型不能作为参数数组的类型。

注意:不能将参数修饰符与 in、out 或 ref 修饰符组合使用。

形参数组允许在方法调用中以两种方式之一指定实参:

  • 形参数组的实参可以是一个表达式,该表达式可以隐式转换为形参数组类型。在这种情况下,参数数组的作用与值参数完全相同。
  • 或者,调用可以为形参数组指定零个或多个实参,其中每个实参都是一个表达式,可隐式转换为形参数组的元素类型。在这种情况下,调用创建一个参数数组类型的实例,其长度与参数的数量相对应,用给定的参数值初始化数组实例的元素,并使用新创建的数组实例作为实际参数。

除了允许在调用中使用可变数量的实参外,形参数组与相同类型的值形参完全等价。

private static void FF数组的元素 ( params object [ ] 数组 )
    {
    if ( 数组 . Length == 0 )
        {
        Console . WriteLine ( $"数组包含 0 个元素。" );
        }
    else
        {
        Console . Write ( $"数组包含 {数组 . Length} 个元素:" );
        foreach ( var xm in 数组 )
            {
            Console . Write ( $"    {xm}" ); 
            }
        Console . WriteLine ( );
        }
    }
static void Main ( string [ ] args )
    {
    object [ ] Dxs = [1 , 2 , 3 , "Hello"];
    FF数组的元素 ( Dxs );
    FF数组的元素 ( 20 , 30 , 40 , 50 , 60 );
    FF数组的元素 ( "红" , "黑" , "黄" , "白" , "绿" , "紫" , "青" );
    FF数组的元素 ( );
    }

上例如下输出:
数组包含 4 个元素: 1 2 3 Hello
数组包含 5 个元素: 20 30 40 50 60
数组包含 7 个元素: 红 黑 黄 白 绿 紫 青
数组包含 0 个元素。

“FF数组的元素”的第一次调用只是将数组 Dxs 作为值参数(整数)或引用参数(字符串)传递。第二次调用自动地用给定的元素值创建一个包含五个元素的 object [ ](实例为 int),并将该数组实例作为值参数传递。同样,第三次调用创建了一个七个元素的 object [ ](实例为 string),最后一次调用则创建零元素 object [ ],并将该实例作为值形参传递。最后一个调用完全等同于书写:
FF数组的元素 ( new object [ ] {} );

当执行重载解析时,带参数数组的方法可以适用,可以是其正常形式,也可以是其扩展形式。只有当方法的正常形式不适用,并且与扩展形式具有相同签名的适用方法尚未在同一类型中声明时,方法的扩展形式才可用。

static void F( params object [ ] a ) => Console . WriteLine ( "F ( object [ ] )" );

static void F ( ) => Console . WriteLine ( "F ( )" );

static void F ( object a , object b ) => Console . WriteLine ( "F ( object , object )" );

static void Main ( )
    {
        F ( );
        F ( 1 );
        F ( 1 , 2 );
        F ( 1 , 2 , 3 );
        F ( 1 , 2 , 3 , 4 );
    }

上例输出如下:
F ( )
F ( object [ ] )
F ( object , object )
F ( object [ ] )
F ( object [ ] )
在本例中,带有参数数组的方法的两种可能的扩展形式已经作为常规方法包含在类中。因此,在执行重载解析时不考虑这些展开的表单,因此第一个和第三个方法调用选择常规方法。当一个类声明一个带有参数数组的方法时,将一些扩展形式作为常规方法也包括进来是很常见的。通过这样做,可以避免在调用带有参数数组的方法的扩展形式时分配数组实例。

数组是引用类型,因此传递给参数数组的值可以为 null。

当参数数组的类型是 object [ ] 时,在方法的正常形式和单个对象参数的扩展形式之间可能会产生歧义。产生歧义的原因是 object [ ] 本身隐式地转换为对象类型。但是,这种歧义没有问题,因为如果需要,可以通过插入强制转换来解决。

{
    static void F ( params object [ ] args )
    {
        foreach ( object o in args )
        {
            Console . Write ( o . GetType ( ) . FullName );
            Console . Write( " " );
        }
        Console . WriteLine ( );
    }

    static void Main ( )
    {
        object [ ] a = {1, "Hello", 123.456};
        object o = a;
        F ( a );
        F ( ( object ) a );
        F ( o );
        F ( ( object [ ] ) o );
    }
}

上例如下输出:
System.Int32 System.String System.Double
System.Object[]
System.Object[]
System.Int32 System.String System.Double
在 F 的第一次和最后一次调用中,F 的标准形式是适用的,因为存在从实参类型到形参类型的隐式转换(两者都是 object [ ] 类型)。因此,重载解析选择 F 的标准形式,实参作为标准值形参传递。在第二次和第三次调用中,F 的标准形式不适用,因为不存在从实参类型到形参类型的隐式转换(object 对象不能隐式地转换为 object [ ])。但是,F 的展开形式是适用的,所以它是通过重载解析来选择的。因此,调用将创建一个单元素 object [ ],并使用给定的参数值(其本身是对 object [ ] 的引用)初始化数组的单个元素。

静态和实例方法

当方法声明包含 static 修饰符时,该方法被称为静态方法。当没有 static 修饰符时,该方法被称为实例方法。

静态方法不对特定实例进行操作,在静态方法中引用此实例会导致编译时错误。

实例方法对类的给定实例进行操作,并且该实例可以访问 this。

虚拟(virtual)方法

当实例方法声明包含 virtual 修饰符时,该方法被称为虚方法。当不存在 virtual 修饰符时,该方法被称为非虚方法。

非虚方法的实现是不变的:无论在声明该方法的类的实例上调用该方法,还是在派生类的实例上调用该方法,实现都是相同的。相反,虚方法的实现可以被派生类取代。取代继承虚方法实现的过程称为重写该方法。

在虚拟方法调用中,发生该调用的实例的运行时类型决定了要调用的实际方法实现。在非虚方法调用中,实例的编译时类型是决定因素。准确地说,当一个名为 N 的方法在一个编译时类型为 C、运行时类型为 R(其中 R 是 C 或从 C 派生的类)的实例上用参数列表 a 调用时,调用过程如下:

  • 在绑定时,重载解析应用于 C、N 和 A,以从 C 中声明并继承的方法集中选择特定的方法 M。
  • 然后在运行时:
    ** 如果 M 是一个非虚方法,则调用 M。
    ** 否则,M 是一个虚方法,并且调用 M 相对于 R 的最派生的实现。

对于在类中声明或由类继承的每个虚方法,都存在一个相对于该类的最派生的方法实现。虚方法 M 相对于类 R 的最派生实现确定如下:

  • 如果 R 包含引入 M 的虚声明,那么这是 M 关于 R 的最派生的实现。
  • 否则,如果 R 包含 M 的重写,那么这是 M 关于 R 的最派生的实现。
  • 否则,M 关于 R 的最派生实现和 M 关于 R 的直接基类的最派生实现是一样的。
{
    static void Main ( )
        {
    Lei2 S = new ( );
    Lei1 F = S;

    F . FF非虚拟的 ( );
    S . FF非虚拟的 ( );
    F . FF虚拟的 ( );
    S . FF虚拟的 ( );
    }

class Lei1
    {
    public void FF非虚拟的 ( ) => Console . WriteLine ( "Lei1 . FF非虚拟的" );
    public virtual void FF虚拟的 ( ) => Console . WriteLine ( "Lei1 . FF虚拟的" );
    }

class Lei2 : Lei1
    {
    public new void FF非虚拟的 ( ) => Console . WriteLine ( "Lei2 . FF非虚拟的" );
    public override void FF虚拟的 ( ) => Console . WriteLine ( "Lei2 . FF虚拟的" );
    }
}

在示例中,“Lei1”引入了一个非虚方法“FF非虚拟的”的和一个虚方法“FF虚拟的”。类“Lei2”引入了一个新的非虚方法“FF非虚拟的”,从而隐藏了继承的“FF非虚拟的”,并覆盖了继承的方法“FF虚拟的”。示例产生如下输出:
Lei1 . FF非虚拟的
Lei2 . FF非虚拟的
Lei2 . FF虚拟的
Lei2 . FF虚拟的
请注意,语句 Lei1 . FF虚拟的 ( ) 调用 Lei2 . FF虚拟的 ( ),而不是 Lei1 . FF虚拟的 ( )。这是因为实例的运行时类型(即 Lei2),而不是实例的编译时类型(即 Lei1),决定要调用的实际方法实现。

因为方法可以隐藏继承的方法,所以一个类可以包含几个具有相同签名的虚拟方法。这不会出现歧义问题,因为除了最派生的方法之外,所有方法都是隐藏的。

class A
{
    public virtual void F ( ) => Console . WriteLine( "A . F" );
}

class B : A
{
    public override void F ( ) => Console . WriteLine( "B . F" );
}

class C : B
{
    public new virtual void F ( ) => Console . WriteLine( "C . F" );
}

class D : C
{
    public override void F ( ) => Console . WriteLine ( "D . F" );
}

class Test
{
    static void Main()
    {
        D d = new D ( ); // 输出 D . F(D 重写了 F)
        A a = d; // A 的 F 是虚拟的,而 B 的 F 是重写的,所以均输出 B . F
        B b = d;
        C c = d; // C 的 F 是虚拟的,由于 c = d,所以输出 D . F
        a . F ( );
        b . F ( );
        c . F ( );
        d . F ( );
    }
}

C 和 D 类包含两个具有相同签名的虚方法:一个是由 A 引入的,另一个是由 C 引入的。C 引入的方法隐藏了从 A 继承的方法。因此,D 中的覆盖声明覆盖了 C 引入的方法,而 D 不可能覆盖 A 引入的方法。输出如下:
B . F
B . F
D . F
D . F
请注意,可以通过不隐藏方法的派生较少的类型访问D的实例来调用隐藏的虚拟方法。

重写方法

当实例方法声明包含 override 修饰符时,该方法被称为重写方法。重写方法覆盖具有相同签名的继承虚方法。虚拟方法声明引入了一个新方法,而重写方法声明通过提供该方法的新实现来专门化现有继承的虚拟方法。

对于在类 C 中声明的覆盖方法 M,通过检查 C 的每个基类来确定被覆盖的基方法,从 C 的直接基类开始,然后继续检查每个后续的直接基类,直到在给定的基类类型中找到至少一个可访问的方法,该方法在替换类型参数后具有与 M 相同的签名。为了定位被覆盖的基方法,如果一个方法是 public、protected、protected internal,或者如果它是 internal 或 private protected,并且在与 C 相同的程序中声明,那么它被认为是可访问的。

除非 override 声明的下列所有条件都为真,否则会发生编译时错误:

  • 可以按照上面描述的方式定位被重写的基方法。
  • 只有一个这样的重写基方法。此限制仅在基类类型为构造类型时有效,其中类型参数的替换使两个方法的签名相同。
  • 被重写的基方法是虚拟、抽象或重写方法。换句话说,被重写的基方法不能是 static 或非虚拟的。
  • 被重写的基方法不是密封方法。
  • 在重写基方法的返回类型和重写方法之间存在标识转换。
  • 重写声明和被重写的基方法具有相同的声明可访问性。换句话说,重写声明不能改变虚方法的可访问性。但是,如果被重写的基方法是 protected internal,并且它是在与包含重写声明的程序集不同的程序集中声明的,则重写声明声明的可访问性应受到保护。
  • 重写声明没有指定任何类型参数约束子句。相反,约束是从重写的基方法继承的。在被重写的方法中作为类型参数的约束可以被继承约束中的类型参数替换。这可能导致在显式指定时无效的约束,例如值类型或密封类型。

重写声明可以使用基访问访问被重写的基方法。

class LeiJi
    {
    int x;
    public virtual void dy ( ) => Console . WriteLine ( $"x = {x}" );
    }

class LeiP : LeiJi
    {
    int y;
    public override void dy ( )
        {
    base . dy ( );
    Console . WriteLine ( $"y = {y}" );
    }
    }

LeiP(派生类)中的 base . dy ( ) 调用 LeiJi(基类)中声明的 dy 方法。基访问禁用虚拟调用机制,并简单地将基方法视为非虚拟方法。如果 LeiP 中的调用写成 ( ( LeiJi ) this ) . dy ( ),它将递归地调用在 LeiP 中声明的 dy 方法,而不是在 LeiJi 中声明的 dy 方法(产生 StackOverflowException 异常),因为 dy 是虚拟的,而 ( ( LeiJi ) this ) 的运行时类型是 LeiP。

只有通过包含 override 修饰符,一个方法才能重写另一个方法。在所有其他情况下,具有与继承方法相同签名的方法只是隐藏继承方法。

internal class Program
    {
    static void Main ( )
        {
        LeiP p = new ( );
        p . dy ( );
        }

    class LeiJi
        {
        public virtual void dy ( )
            {
            Console . WriteLine ( "abc1" );
            }
        }

    class LeiP : LeiJi
        {
        public virtual void dy ( )
            {
            Console . WriteLine ( "abc" );
            }
        }
    }

LeiP 中的 dy 方法不包括 override 修饰符,因此不重写 LeiJi 中的 dy 方法。相反,LeiP 中的 dy 方法隐藏了 LeiJi 中的方法,并报告一个警告(CS0114:“Program.LeiP.dy()”隐藏继承的成员“Program.LeiJi.dy()”。若要使当前成员重写该实现,请添加关键字 override。否则,添加关键字 new。),因为声明不包括 new 修饰符。

internal class Program
    {
    static void Main ( )
        {
    LeiP1 p = new ( );
    p . dy ( );
    }

    class LeiJiz
        {
    public virtual void dy ( )
        {
        Console . WriteLine ( "abc啊" );
        }
    }

    class LeiP : LeiJiz
        {
    private new void dy ( )
        {
        Console . WriteLine ( "abc哦" );
        }
    }

    class LeiP1 : LeiP
        {
    public override void dy ( )
        {
        Console . WriteLine ( "abc哎" );
        }
    }
    }

LeiP 中的 dy 方法隐藏了从 LeiJi 继承的 Vitual dy 方法。由于 LeiP 中的新 dy 具有 private 访问,其作用域仅包括 LeiP 的类体,而不扩展到 LeiP1。因此,LeiP1 中的 dy 声明允许重写从 LeiJi 继承的 dy。

密封方法

当实例方法声明包含 sealed 修饰符时,该方法被称为密封方法。密封方法重写具有相同签名的继承虚方法。密封方法也应标有 override 修饰符。使用 sealed 修饰符可防止派生类进一步重写该方法。

class LeiJi
    {
        public virtual void dy ( )
        {
            Console . WriteLine ( "abc啊" );
        }
        public virtual void ff ( )
        {
            Console . WriteLine ( "abcff" );
        }
    }

class LeiP : LeiJi
    {
        public sealed override void ff ( )
        {
            Console . WriteLine ( "abc-sealed" );
        }
        public override void dy ( )
        {
            Console . WriteLine ( "abc-override" );
        }
    }

class LeiP1 : LeiP
    {
        public override void dy ( )
            {
                Console . WriteLine ( "C . dy" );
            }
    }

类 LeiP 提供了两个重写方法:具有 sealed 修饰符的 ff 方法和没有 sealed 修饰符的 dy 方法。LeiP 对 sealed 修饰符的使用阻止了 LeiP1 进一步覆盖 ff。

抽象方法

当实例方法声明包含 abstract 修饰符时,该方法被称为抽象方法。尽管抽象方法隐式地也是虚方法,但它不能有修饰符 virtual。

抽象方法声明引入了一个新的虚拟方法,但不提供该方法的实现。相反,非抽象派生类需要通过重写该方法来提供它们自己的实现。因为抽象方法不提供实际的实现,所以抽象方法的方法体只由一个分号组成。

抽象方法声明只允许在抽象类中使用。参见 C# 的 abstract。

外部方法

当方法声明包含 external 修饰符时,该方法被称为外部方法。外部方法在外部实现,通常使用 C# 以外的语言。由于外部方法声明不提供实际实现,因此外部方法的方法体仅由一个分号组成。外部方法不应是通用的。

实现到外部方法的链接的机制是由实现定义的。

示例:下面的示例演示了 external 修饰符和 DllImport 属性的使用:

class Path
{
    [DllImport("kernel32", SetLastError=true)]
    static extern bool CreateDirectory(string name, SecurityAttribute sa);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool RemoveDirectory(string name);

    [DllImport("kernel32", SetLastError=true)]
    static extern int GetCurrentDirectory(int bufSize, StringBuilder buf);

    [DllImport("kernel32", SetLastError=true)]
    static extern bool SetCurrentDirectory(string name);
}

分部方法

当方法声明包含 partial 修饰符时,该方法被称为分部方法。分部方法只能声明为 partial 类型的成员,并且受到许多限制。

分部方法可以在类型声明的一部分定义,在另一部分实现。实现是可选的;如果没有部分实现分部方法,则分部方法声明和对它的所有调用将从由各部分组合产生的类型声明中删除。

分部方法不能定义访问修饰符;它们是隐式 private 的。它们的返回类型应为 void,其参数不得为输出参数。只有当标识符 partial 出现在 void 关键字之前时,它才会在方法声明中被识别为上下文关键字。分部方法不能显式地实现接口方法。

有两种类型的分部方法声明:如果方法声明的主体是分号,则该声明称为定义分部方法声明。如果主体不是分号,则该声明称为实现部分方法声明。在类型声明的各个部分中,只能有一个具有给定签名的定义部分方法声明,并且最多只能有一个具有给定签名的实现部分方法声明。如果给出了实现的部分方法声明,则应存在相应的定义部分方法声明,并且声明应按照以下规定进行匹配:
声明应该具有相同的修饰符(尽管不一定顺序相同)、方法名、类型参数的数量和参数的数量。
声明中相应的参数应该具有相同的修饰符(尽管不一定顺序相同)和相同的类型,或者相同的可转换类型(类型参数名称的模差异)。

  • 声明中相应的类型参数应具有相同的约束(类型参数名称的模差)。
  • 实现的部分方法声明可以与相应的定义部分方法声明出现在同一部分中。
  • 只有定义的部分方法参与重载解析。因此,无论是否给出了实现声明,调用表达式都可以解析为对分部方法的调用。因为分部方法总是返回 void,所以这样的调用表达式总是表达式语句。此外,由于分部方法是隐式 private 的,因此此类语句将始终出现在声明分部方法的类型声明的某个部分中。

注意:匹配定义和实现部分方法声明的定义不要求参数名匹配。当使用命名参数时,这可能会产生令人惊讶的行为,尽管定义良好。例如,给定在一个文件中定义 M 的部分方法声明,在另一个文件中实现部分方法声明:

// File P1.cs:
partial class P
    {
    static partial void M(int x);
    }

// File P2.cs:
partial class P
    {
    static void Caller() => M(y: 0); // 警告 CS1739:“M”的最佳重载没有名为“y”的参数
    static partial void M(int y) {} // 警告 CS8826:分部方法声明“void P.M(int x)”和“void P.M(int y)具有签名差异。”
    }

无效,因为调用使用来自实现的参数名称,而不是定义的部分方法声明。

如果分部类型声明的任何部分都不包含给定分部方法的实现声明,则只需从组合类型声明中删除调用它的任何表达式语句。因此,调用表达式(包括任何子表达式)在运行时不起作用。分部方法本身也被删除,并且不再是组合类型声明的成员。

如果存在给定部分方法的实现声明,则保留部分方法的调用。分部方法产生的方法声明类似于分部方法声明的实现,不同之处在于:

  • 不包括 partial 修饰符。
  • 结果方法声明中的属性是定义和实现部分方法声明的组合属性,顺序未指定。重复项不会被移除。
  • 结果方法声明的参数上的属性是按未指定顺序定义和实现部分方法声明的相应参数的组合属性。重复项不会被移除。

如果给出了分部方法 M 的定义声明而不是实现声明,则适用以下限制:

  • 从 M 创建委托是编译时错误。
  • 在被转换为表达式树类型的匿名函数中引用 M 是一个编译时错误。
  • 作为 M 调用的一部分出现的表达式不会影响确定赋值状态,这可能会导致编译时错误。
  • M 不能作为应用程序的入口点。

分部方法对于允许类型声明的一部分自定义另一部分的行为非常有用,例如,由工具生成的部分。考虑下面的部分类声明:

partial class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    partial void OnNameChanging(string newName);
    partial void OnNameChanged();
}

如果这个类在没有任何其他部分的情况下编译,则定义的部分方法声明及其调用将被删除,并且结果组合的类声明将等同于以下内容:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set => name = value;
    }
}

然而,假设给出了另一部分,它提供了部分方法的实现声明:

partial class Customer
{
    partial void OnNameChanging(string newName) => Console.WriteLine($"Changing {name} to {newName}");

    partial void OnNameChanged() => Console.WriteLine($"Changed to {name}");
}

然后生成的组合类声明将等同于以下内容:

class Customer
{
    string name;

    public string Name
    {
        get => name;
        set
        {
            OnNameChanging(value);
            name = value;
            OnNameChanged();
        }
    }

    void OnNameChanging(string newName) => Console.WriteLine($"Changing {name} to {newName}");

    void OnNameChanged() => Console.WriteLine($"Changed to {name}");
}

扩展方法

当方法的第一个参数包含 this 修饰符时,该方法被称为扩展方法。扩展方法只能在非泛型、非嵌套的 static 类中声明。扩展方法的第一个参数是有限制的,如下所示:

  • 只有当它具有值类型时,它才可能是一个输入参数
  • 只有当它具有值类型或具有约束为 struct 的泛型类型时,它才可能是引用形参
  • 它不能是指针类型。
public static class KuoZhan
    {
    public static int Toint32 ( this string 字符串 ) => Int32 . Parse ( 字符串 );

    public static T [ ] Pian < T > ( this T [ ] 源 , int 索引 , int 个数 )
    {
    if ( 索引 < 0 || 个数 < 0 || 源 . Length - 索引 < 个数 )
        {
    throw new ArgumentException ( );
    }
    T [ ] result = new T [ 个数 ];
    Array . Copy ( 源 , 索引 , result , 0 , 个数 );
    return result;
    }
}
static void Main ( )
    {
    string Zfc = "123";
    Console . WriteLine ( KuoZhan . Toint32 ( Zfc ) );
    string [ ] zfcs = [ "123" , "456" , "789" , "147" , "258" ];
    string [ ] zfck = KuoZhan . Pian ( zfcs , 1 , 2 );
    foreach ( string z in zfck )
        {
        Console . WriteLine ( z );
        }
    }

上例使用了扩展方法(ToInt32 和 Pian),用于返回 string 转换为 Int32 或返回数组中的某个片段。上例输出:
123
456
789

扩展方法是一个常规的静态方法。此外,当它的封闭静态类在作用域中时,可以使用实例方法调用语法调用扩展方法,使用接收者表达式作为第一个参数。

Pian 方法在 string [ ] 上可用,ToInt32 方法在 string 上可用,因为它们已被声明为扩展方法。该程序的含义与以下相同,使用普通的静态方法调用:

static void Main()
    {
    string[] strings = { "1", "22", "333", "4444" };
    foreach (string s in Extensions.Slice(strings, 1, 2))
        {
             Console.WriteLine(Extensions.ToInt32(s));
        }
    }

方法体

方法声明的方法体由块体、表达式体或分号组成。

抽象和外部方法声明不提供方法实现,因此它们的方法体只是由一个分号组成。对于任何其他方法,方法体是一个块,其中包含调用该方法时要执行的语句。
如果返回类型是 void,或者如果方法是 async 且返回类型是 «TaskType»,则方法的有效返回类型是 void。否则,非异步方法的有效返回类型是它的返回类型,而返回类型为 «TaskType» < T > 的异步方法的有效返回类型是 T。

当方法的有效返回类型为 void 并且该方法具有块体时,块中的返回语句不得指定表达式。如果 void 方法块的执行正常完成(也就是说,控制从方法体的末尾流出),该方法就返回给它的调用者。

当一个方法的有效返回类型是 void 并且该方法有一个表达式体时,表达式 E 应该是一个语句表达式,并且表达式体完全等价于 {E;}。

对于按值返回的方法,该方法体中的每个返回语句都应指定一个隐式转换为有效返回类型的表达式。

对于按引用返回方法,该方法体中的每个返回语句都应该指定一个表达式,该表达式的类型是有效返回类型的表达式,并且具有一个调用者上下文的引用安全上下文。

对于按值返回和按引用返回的方法,方法体的端点不可达。换句话说,不允许控制从方法体的末尾流出。

class Lei
    {
    public static int FFf ( ) { }

    public static int FFg ( )
        {
    return 1;
    }

    public int FFh ( bool b )
        {
    if ( b ) return 1;
    else return 0;
    }

    public int FFi ( bool b ) => b ? 1 : 0;
}

只有 FFf 是有警告的(“Lei.FFf()”:并非所有的代码路径都返回值)。其余的方法 FFg 和 FFh 方法是正确的,因为所有可能的执行路径都以指定返回值的返回语句结束。FFi 方法是正确的,因为它的主体相当于一个只有一条返回语句的块。

属性

属性是一个成员,它提供对对象或类的特征的访问。属性的示例包括字符串的长度、字体的大小、窗口的标题和客户的名称。属性是字段的自然扩展—两者都是具有关联类型的命名成员,并且访问字段和属性的语法是相同的。但是,与字段不同,属性不表示存储位置。相反,属性具有指定读取或写入其值时要执行的语句的访问器。因此,属性提供了一种机制,将操作与对象或类的特征的读写联系起来;此外,它们还允许计算这些特性。
使用属性声明声明属性,Unsafe 修饰符仅在不安全代码中可用。

属性声明有两种类型:

  • 第一个声明了一个非重值属性。它的值具有 type 类型。这种属性可以是可读和/或可写的。
  • 第二个声明一个重值属性。它的值是一个引用变量,可以是只读的,指向一个类型为 type 的变量。这种属性只能读。

属性声明可以包含一组属性和任何一种允许声明的可访问性 new、static、virtual、override、sealed、abstract 和 extern 修饰符。

关于修饰符的有效组合,属性声明遵循与方法声明相同的规则。

属性名称指定属性的名称。除非属性是显式接口成员实现,否则属性名称只是一个标识符。对于显式接口成员实现,属性名称由一个 interface 类型后面跟着一个“.”和标识符。

属性的类型至少应该和属性本身一样容易接近。

属性体可以由语句体或表达式体组成。在语句体中,用“{”和“}”标记括起来的访问器声明声明属性的访问器。访问器指定与读写属性相关联的可执行语句。

在属性体中,表达式体由 => 后跟表达式和分号组成,完全等同于语句体 {get {return E;}},因此只能用于指定只读属性,其中 get 访问器的结果由单个表达式给出。

属性初始器只能用于自动实现的属性,并使用表达式给出的值初始化这些属性的底层字段。

ref 属性体可以由语句体或表达式体组成。在语句体中,get 访问器声明声明属性的 get 访问器。访问器指定与读取属性相关联的可执行语句。

在 ref 属性体中,表达式体由 => 后面跟着 ref、变量引用 V 和分号组成,完全等同于语句体 {get {return ref V;}}
注意:即使访问属性的语法与访问字段的语法相同,属性也不会被归类为变量。因此,不可能将属性作为 in、out 或 ref 参数传递,除非该属性被重新赋值并因此返回一个变量引用。

当属性声明包含 external 修饰符时,该属性被称为外部属性。由于外部属性声明不提供实际实现,因此其访问器声明中的每个访问器体都应该是分号。

静态和实例属性

当属性声明包含 static 修饰符时,该属性被称为静态属性。当不存在静态修饰符时,该属性被称为实例属性。

静态属性不与特定实例相关联,在静态属性的访问器中引用此实例会导致编译时错误。

实例属性与类的给定实例相关联,并且该实例可以在该属性的访问器中以 this 的方式访问。

访问器

注意:此条款适用于属性(property)和索引器(indexer)。子句是根据属性编写的。

属性的访问器声明指定与写入和/或读取该属性相关的可执行语句。

访问器声明由 get 访问器声明、set 访问器声明或两者组成。每个访问器声明由可选属性、可选访问器修饰符、令牌 get 或 set 以及访问器体组成。

对于重值属性,ref get 访问器声明由可选属性、可选访问器修饰符、令牌 get 和 ref 访问器体组成。

访问器修饰符的使用受以下限制:

  • 访问器修饰符不能在接口或显式接口成员实现中使用。
  • 对于没有重写修饰符的属性或索引器,只有当属性或索引器同时具有 get 和 set 访问器时才允许使用 访问修饰符,并且只允许在其中一个访问器上使用。
  • 对于包含 override 修饰符的属性或索引器,访问器必须匹配被重写的访问器的访问修饰符(如果有的话)。
  • 访问修饰符声明的可访问性要比属性或索引器本身声明的可访问性严格得多。准确地说:
    如果属性或索引器声明的可访问性为 public,则访问修饰符声明的可访问性可以是 private protected、protected internal、internal、protected 或 private。
    如果属性或索引器声明的可访问性为 protected internal,则访问修饰符声明的可访问性可以是private protected、protected private、internal、protected 或 private。
    如果属性或索引器声明的可访问性是 internal 或 protected,则访问修饰符声明的可访问性应该是 private protected 或 private。
    如果属性或索引器声明的可访问性为 private protected,则访问修饰符声明的可访问性应为 private。
    如果属性或索引器的可访问性声明为 private,则不能使用访问修饰符。

对于 abstract 和 eaternal 非自定义值属性,指定的每个访问器的任何访问体只是一个分号。非抽象的、非外部的属性,但不是索引器(indexer),也可以让指定的所有访问器的访问体为分号,在这种情况下,它是一个自动实现的属性。自动实现的属性至少应该有一个 get 访问器。对于任何其他非抽象、非外部属性的访问器,访问体是:

  • 一个块,指定调用相应访问器时要执行的语句;
  • 表达式体,由 => 后面跟着一个表达式和一个分号组成,表示在调用相应的访问器时要执行的单个表达式。

对于抽象属性和外部属性,ref_accessor_body只是一个分号。对于任何其他非抽象、非外部属性的访问器,ref_accessor_body是:

  • 一个块,指定调用 get 访问器时要执行的语句;
  • 表达式体,由 => 后面跟着 ref、引用变量和分号组成。在调用 get 访问器时计算变量引用。

非回调值属性的 get 访问器对应于返回值为属性类型的无参数方法。除非作为赋值的目标,否则在表达式中引用此类属性时,将调用其 get 访问器来计算该属性的值。

非重新赋值属性的 get 访问器的主体应符合“方法体”中描述的返回值方法的规则。特别是,get 访问器体中的所有返回语句都应该指定一个可以隐式转换为属性类型的表达式。此外,get 访问器的端点不可达。

返回值属性的 get 访问器对应于一个无参数方法,该方法的返回值为引用变量,指向该属性类型的变量。当在表达式中引用这样的属性时,将调用其 get 访问器来计算该属性的引用变量值。该变量引用和其他变量引用一样,用于读取或根据上下文的要求写入被引用的变量(对于非只读的引用变量)。

示例:下面的示例演示了一个重值属性作为赋值的目标:

class Program
{
    static int 字段;
    static ref int 属性 => ref 字段;

    static void Main()
    {
        字段 = 10;
        Console.WriteLine ( 属性 ); // 输出 10
        属性 = 20; // 这将调用 get 访问器,然后通过结果变量引用进行赋值
        Console.WriteLine ( 字段 );    // 输出 20
    }
}

重值属性的 get 访问器的主体应该符合“方法体”中描述的重值方法的规则。

set 访问器对应于具有单个属性类型的值形参和 void 返回类型的方法。set 访问器的隐式参数总是命名为 value。当一个属性被引用为赋值操作的目标,或者被引用为 ++ 或 -- 的操作数时,调用 set 访问器时使用提供新值的实参。set 访问器的主体应该符合“方法体”描述的 void 方法的规则。特别是,set 访问器体中的返回语句不允许指定表达式。由于 set 访问器隐式地具有名为 value 的形参,因此在 set 访问器中声明的局部变量或常量具有该名称会导致编译时错误。

根据 get 和 set 访问器的存在与否,属性被分类如下:

  • 同时包含 get 访问器和 set 访问器的属性称为读写属性。
  • 只有 get 访问器的属性称为只读属性。将只读属性作为赋值的目标是编译时错误。
  • 只有集合访问器的属性称为只写属性。除非作为赋值的目标,否则在表达式中引用只写属性是编译时错误。

注意:前置和后置的 ++ 和 -- 操作符以及复合赋值操作符不能应用于只写属性,因为这些操作符在写入新操作数之前会读取其操作数的旧值。

public class AnNiu : Control
{
    private string ZfcText;

    public string Text
    {
        get => ZfcText;
        set
        {
            if (ZfcText != value)
            {
                ZfcText = value;
                Repaint();
            }
        }
    }

    public override void Paint(Graphics g, Rectangle r)
    {
        // Painting code goes here
    }
}

AnNiu 控件(继承自 Control)声明了一个公共标题属性 Text。标题属性的 get 访问器返回存储在私有标题字段中的字符串。set 访问器检查新值是否与当前值不同,如果不同,则存储新值并重新绘制控件。属性通常遵循上面所示的模式:get 访问器简单地返回存储在 private 字段中的值,set 访问器修改该 private 字段,然后执行完全更新对象状态所需的任何其他操作。给出上面的 AnNiu 类,下面是一个使用 Text 属性的例子:

AnNiu okanniu = new ( );
okanniu . Text = "确定"; // 调用 set 访问器
string zfc = okanniu . Text; // 调用 get 访问器

在这里,set 访问器是通过给属性赋值来调用的,get 访问器是通过在表达式中引用属性来调用的。

属性的 get 和 set 访问器不是不同的成员,并且不可能单独声明属性的访问器。若如此,不是声明单个读写属性。相反,它声明了两个同名的属性,一个是只读的,一个是只写的。由于在同一类中声明的两个成员不能具有相同的名称,因此该示例会导致发生编译时错误。

当派生类声明与继承属性同名的属性时,派生属性在读取和写入方面都隐藏继承属性。

public class LeiJi
    {
    public int Zhs
        {
        set => Zhs = value;
        }
    }

public class LeiP : LeiJi
    {
    public new int Zhs
        {
        get
            {
            return Zhs;
            }
        }
    }

在读写方面,LeiP 中的 Zhs 属性隐藏了 LeiJi 中的 Zhs 属性。因此,在声明中

LeiP ps = new ( );
// ps . Zhs = 1; // 无法为属性或索引器“LeiP . Zhs”赋值 - 它是只读的
( ( LeiJi ) ps ) . Zhs = 1; // 使用 LeiJi . Zhs 的 set 访问器

对 LeiP . Zhs 的赋值会导致一个编译时错误报告,因为 LeiP 中的只读 Zhs 属性(只写了 get 访问器)隐藏了 LeiJi 中的只写 Zhs 属性(只写了 set 访问器)。注意,但是可以使用强制类型转换来访问隐藏的 Zhs 属性。

与公共字段不同,属性提供了对象的内部状态与其公共接口之间的分离。考虑下面的代码,它使用 Point 结构体来表示位置:

class BiaoQian
    {
    private int x, y;
    private string zfcText;

    public BiaoQian ( int x , int y , string 文本 )
        {
        this . x = x;
        this . y = y;
        this . zfcText = 文本;
        }

    public int X => x;
    public int Y => y;
    public Point 位置 => new Point ( x , y );
    public string 文本 => zfcText;
    }

这里,BiaoQian 类使用两个 int 字段 x 和 y 来存储它的位置。位置公开为 X 和 Y 属性以及 Point 类型的位置属性。如果在 BiaoQian 的未来版本中,在内部将位置存储为 Point 变得更加方便,则可以在不影响类的公共接口的情况下进行更改:

class BiaoQian
    {
    private Point 位置;
    private string zfc文本;

    public BiaoQian ( int x , int y , string 文本 )
        {
        this . 位置 = new Point ( x , y );
        this . zfc文本 = 文本;
        }

    public int X => 位置 . X;
    public int Y => 位置 . Y;
    public Point 位置 => 位置;
    public string 文本 => zfc文本;
}

如果 x 和 y 是公共只读字段,就不可能对 BiaoQian 类进行这样的更改。

注意:通过属性公开状态并不一定比直接公开字段效率低。特别是,当属性是非虚拟的并且只包含少量代码时,执行环境可能会用访问器的实际代码替换对访问器的调用。这个过程被称为内联,它使属性访问和字段访问一样高效,同时保留了属性的灵活性。

由于调用 get 访问器在概念上等同于读取字段的值,因此 get 访问器具有可观察到的副作用被认为是糟糕的编程风格。在这个例子中

class Counter
    {
    private int next;
    public int Next => next++;
    }

Next 属性的值取决于先前访问该属性的次数。因此,访问该属性会产生一个可观察的副作用,而该属性应该作为一个方法来实现。
get 访问器的“无副作用”约定并不意味着应该总是简单地编写 get 访问器来返回存储在字段中的值。实际上,get 访问器通常通过访问多个字段或调用方法来计算属性的值。但是,正确设计的 get 访问器不会执行导致对象状态发生可观察变化的操作。

属性可以用来延迟资源的初始化,直到它第一次被引用。

public class Console
    {
    private static TextReader dqq;
    private static TextWriter xrq;
    private static TextWriter ych;

    public static TextReader In
        {
        get
            {
            if ( dqq == null )
                {
                dqq = new StreamReader ( Console . OpenStandardInput ( ) );
                }
            return dqq;
           }
        }

    public static TextWriter Out
        {
        get
            {
            if ( xrq == null )
                {
                xrq = new StreamWriter ( Console . OpenStandardOutput ( ) );
                }
            return xrq;
            }
        }

    public static TextWriter Error
        {
        get
            {
            if ( ych == null )
                {
                ych = new StreamWriter ( Console . OpenStandardError ( ) );
                }
            return ych;
            }
        }
...
    }

Console(控制台)类包含三个属性:In、Out 和 Error,它们分别表示标准输入、输出和错误设备。通过将这些成员公开为属性,Console 类可以延迟其初始化,直到实际使用它们。例如,在第一次引用 Out 属性时
Console . Out . WriteLine ( "hello, world" );
为输出设备创建底层的 textWriter。但是,如果应用程序不引用 In 和 Error 属性,则不会为这些设备创建对象。

自动实现的属性

自动实现的属性(简称 auto-property)是一个非抽象、非外部、非重值的属性,只有分号和访问器体。自动属性应该有一个 get 访问器,也可以有一个 set 访问器。

当将属性指定为自动实现的属性时,将自动为该属性提供隐藏的后备字段,并且实现访问器以读取和写入该后备字段。隐藏的后备字段是不可访问的,只能通过自动实现的属性访问器读写,即使在包含类型中也是如此。如果自动属性没有 set 访问器,则后备字段被认为是只读的。就像只读字段一样,也可以在封闭类的构造函数体中分配只读自动属性。这样的赋值直接分配给属性的只读后备字段。

一个自动属性可以有一个属性初始化器,它作为一个变量初始化器直接应用于后备字段。

public class Point
{
    public int X { get; set; } // 自动实现
    public int Y { get; set; } // 自动实现
}

等价于以下声明:

public class Point
{
    private int x;
    private int y;

    public int X { get { return x; } set { x = value; } }
    public int Y { get { return y; } set { y = value; } }
}

下一个例子:

public class ReadOnlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadOnlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

等价于以下声明:

public class ReadOnlyPoint
{
    private readonly int _x;
    private readonly int _y;
    public int X { get { return _x; } }
    public int Y { get { return _y; } }

    public ReadOnlyPoint(int x, int y)
    {
        _x = x;
        _y = y;
    }
}

对只读字段的赋值是有效的,因为它们发生在构造函数中。

虽然后备字段是隐藏的,但该字段可以通过自动实现属性的属性声明直接应用于字段目标属性。

[Serializable]
public class ShiWu
    {
    [field : NonSerialized]
    public string? MingCheng { get; set; }
    }

导致以字段为目标的属性 NonSerialized 被应用于编译器生成的后备字段,就好像代码是这样编写的:

[Serializable]
    public class ShiWu
        {
    [field : NonSerialized]
    private string? _MingCheng;

    public string MingCheng { get { return _MingCheng; } set { _MingCheng = value; } }
        }
可访问性

如果访问器具有访问修饰符,则使用访问修饰符声明的可访问性来确定访问器的可访问性域。如果访问器没有访问修饰符,则访问器的可访问域将根据声明的属性或索引器的可访问性来确定。

访问修饰符的存在不会影响成员查找或重载解析。属性或索引器上的修饰符总是决定绑定到哪个属性或索引器,而不管访问的上下文如何。

一旦选择了特定的非反值属性或非反值索引器,将使用所涉及的特定访问器的可访问域来确定该使用是否有效:

  • 如果用法是作为一个值,则 get 访问器必须存在并且是可访问的。
  • 如果用法是作为简单赋值的目标,则 set 访问器必须存在并且是可访问的。
  • 如果用法是作为复合赋值的目标,或作为 ++ 或 -- 操作符的目标,则 get 访问器和 set 访问器都必须存在并且是可访问的。

示例:在下面的示例中,属性 LeiJi . Text 被属性 LeiP . Text 隐藏,即使在只调用 set 访问器的上下文中也是如此。相反,主程序无法访问属性 LeiP . Count,因此使用了可访问属性 LeiJi . Count。

static void Main ( )
    {
    LeiP P = new ( );
    P . Count = 12;
    int z = P . Count;
    // P . Text = "拜拜"; // 警告 CS0272:属性或索引器“LeiP . Text”不能用在此上下文中,因为 set 访问器不可访问
    string zfc = P . Text;
    }

class LeiJi
    {
    public string Text
        {
        get
            {
            return "你好";
            }
        set
            {
            }
        }

    public int Count
        {
        get => 5;
        set
            {
            }
       }
    }

class LeiP : LeiJi
    {
    private string _text = "再见";
    private int _count = 0;

    public new string Text
        {
        get
            {
            return _text;
            }
        protected set => _text = value;
        }

    protected new int Count
        {
        get => _count;
        set => _count = value;
        }
    }

一旦选择了特定的重值属性或重值索引器(无论其用法是作为一个值、一个简单赋值的目标还是一个复合赋值的目标),将使用 get 访问器的可访问性域来确定该用法是否有效。

用于实现接口的访问器不能有访问修饰符。如果只使用一个访问器来实现接口,则可以使用访问修饰符声明另一个访问器:

public interface I
{
    string Prop { get; }
}

public class C : I
{
    public string Prop
    {
        get => "April";     // 必须不存在访问修饰符
        internal set {...}  // 允许存在访问修饰符,因为接口 I 的 Prop 属性不存在 set 访问器
    }
}
virtual、sealed、override 和 abstract 访问器

注意:此条款适用于属性和索引器。子句是根据属性编写的,当读取索引器时,用 indexer/indexers 代替 property/properties,并参考属性和索引器之间的差异列表。

virtual 属性声明指定属性的访问器是虚拟的。virtual 修饰符适用于属性的所有非 private 访问器。当虚拟属性的访问器具有 private 访问修饰符时,该 private 访问器隐式地不是虚拟的。

abstract 属性声明指定属性的访问器是虚拟的,但不提供访问器的实际实现。相反,非抽象派生类需要通过重写属性为访问器提供它们自己的实现。因为抽象属性声明的访问器不提供实际实现,所以它的访问器体仅由一个分号组成。抽象属性不能有私有访问器。

包含 abstract 和 override 修饰符的属性声明指定该属性是抽象的,并覆盖基本属性。这种属性的访问器也是抽象的。

abstract 属性声明只允许在 abstract 类中使用。通过包含指定重写指令的属性声明,可以在派生类中重写继承的虚拟属性的访问器。这被称为重写属性声明。重写属性声明不声明新属性。相反,它只是专门化现有虚拟属性的访问器的实现。

override 声明和被重写的基属性必须具有相同的声明可访问性。换句话说,重写声明不应改变基本属性的可访问性。但是,如果被覆盖的基属性是内部保护的,并且它是在与包含覆盖声明的程序集不同的程序集中声明的,则覆盖声明声明的可访问性应受到保护。如果继承的属性只有一个访问器(即,如果继承的属性是只读的或只写的),覆盖的属性应该只包括那个访问器。如果继承的属性包括两个访问器(即,如果继承的属性是读写的),覆盖的属性可以包括单个访问器或两个访问器。在重写属性和继承属性的类型之间应进行标识转换。

override 属性声明可以包含 sealed 修饰符。使用此修饰符可防止派生类进一步重写该属性。sealed 属性的访问器也是密封的。

除了声明和调用语法不同之外,virtual、sealed、override 和 abstract 访问器的行为与virtual、sealed、override 和 abstract 方法完全相同。具体地说,在“virtual 方法”、“override 方法”、“sealed 方法”、“abstract 方法”描述的规则就好像访问器是相应形式的方法一样适用:

  • get 访问器对应于一个无参数方法,该方法具有属性类型的返回值和与包含属性相同的修饰符。
  • set 访问器对应的方法具有属性类型的单个值参数、void 返回类型和与包含属性相同的修饰符。
abstract class LeiJi
{
    int y;

    public virtual int X
    {
        get => 0;
    }

    public virtual int Y
    {
        get => y;
        set => y = value;
    }

    public abstract int Z { get; set; }
}

X 是 virtual 只读属性,Y 是 virtual 读写属性,Z 是 abstract 读写属性。因为 Z 是 abstract 的,所以包含类 LeiJi 也要声明为抽象的。当一个类派生自 LeiJi:

class LeiP : LeiJi
    {
    int z = 20;

    public override int X
        {
        get => base . X + 1;
        }

    public override int Y
        {
        set => base . Y = value < 0 ? 0 : value;
        }

    public override int Z
        {
        get => z;
        set => z = value;
        }
    }

X、Y 和 Z 的声明是 override 属性声明。每个属性声明都与相应继承属性的可访问性修饰符、类型和名称完全匹配。X 的 get 访问器和 Y 的 set 访问器使用 base 关键字访问继承的访问器。Z 的声明重写了两个抽象访问器 - 因此,LeiP 中没有突出的抽象函数成员,并且 LeiP 被允许为非抽象类。

当将属性声明为 override 时,重写代码可以访问任何被重写的访问器。此外,属性或索引器本身以及访问器的声明可访问性应与覆盖的成员和访问器的可访问性相匹配。

public class B
{
    public virtual int P
    {
        get {...}
        protected set {...}
    }
}

public class D : B
{
    public override int P
    {
        get {...}            // 必须没有修饰符
        protected set {...}  // 必须指定 protected
    }
}

事件

事件是使对象或类能够提供通知的成员。客户端可以通过提供事件处理程序为事件附加可执行代码。

unsafe 修饰符仅在不安全代码中可用。

事件声明可以包含一组属性和任何一种允许声明的可访问性、new、static、virtual、override、sealed、abstract 和 extern 修饰符。

关于修饰符的有效组合,事件声明遵循与方法声明相同的规则。

事件声明的类型应该是一个委托类型,并且这个委托类型至少应该和事件本身一样可访问。

事件声明可以包含事件访问修饰符。但是,如果没有,对于非外部的、非抽象的事件,编译器将自动提供它们;对于外部事件,访问器是外部提供的。

省略参数访问修饰符的事件声明定义了一个或多个事件 - 每个变量声明对应一个事件。属性和修饰符应用于这样一个事件声明声明的所有成员。

如果事件声明同时包含 abstract 修饰符和事件访问修饰符,则会导致编译时错误。

当事件声明包含 extern 修饰符时,该事件被称为外部事件。由于外部事件声明不提供实际实现,因此同时包含 extern 修饰符和事件访问修饰符是错误的。

带有 abstract 修饰符或 extern 修饰符的事件声明的变量声明包含变量初始化器会导致编译时错误。

事件可以用作 += 和 -= 操作符的左操作数。这些操作符分别用于将事件处理程序附加到事件或从事件中删除事件处理程序,事件的访问修饰符控制允许此类操作的上下文。

声明事件的类型之外的代码允许对事件进行的唯一操作是 += 和 -=。因此,尽管此类代码可以为事件添加和删除处理程序,但它不能直接获取或修改事件处理程序的底层列表。

在 x += y 或 x -= y 形式的操作中,当 x 是事件时,操作的结果具有类型 void(与在非事件类型上定义的其他 += 和 -= 操作符的赋值后具有 x 的类型相反)。这可以防止外部代码间接检查事件的底层委托。

下面的例子展示了事件处理程序是如何附加到 Button 类的实例上的:

public delegate void EventHandler ( object sender , EventArgs e );

public class AnNiu : Control
{
    public event EventHandler Click;
}

public class Lei登录对话框 : Form
{
    AnNiu okButton;
    AnNiu cancelButton;

    public Lei登录对话框 ( )
    {
        okButton = new AnNiu ( );
        okButton . Click += new EventHandler ( OkButtonClick );
        cancelButton = new Button( );
        cancelButton.Click += new EventHandler( CancelButtonClick );
    }

    void OkButtonClick ( object sender, EventArgs e )
    {
        // Handle okButton . Click event
    }

    void CancelButtonClick ( object sender, EventArgs e )
    {
        // Handle cancelButton . Click event
    }
}

这里,“Lei登录对话框”实例构造函数创建了两个 AnNiu 实例,并将事件处理程序附加到 Click 事件。

类似字段的事件

在包含事件声明的类或结构的程序文本中,可以像使用字段一样使用某些事件。要以这种方式使用,事件不能是 abstract 的或 extern 的,也不能显式地包含参数访问声明。这样的事件可以在任何允许字段的上下文中使用。该字段包含一个委托,它指的是已添加到事件的事件处理程序列表。如果没有添加事件处理程序,则该字段包含 null。

示例:如下代码所示

public delegate void EventHandler ( object sender, EventArgs e );

public class AnNiu : Control
{
    public event EventHandler Click;

    protected void OnClick ( EventArgs e )
    {
        EventHandler handler = Click;
        if ( handler != null )
        {
            handler ( this , e );
        }
    }

    public void Reset ( ) => Click = null;
}

Click 用作 AnNiu 类中的字段。如示例所示,可以在委托调用表达式中检查、修改和使用该字段。AnNiu 类中的 OnClick 方法“引发”Click 事件。引发事件的概念完全等同于调用由事件表示的委托 - 因此,不存在用于引发事件的特殊语言结构。请注意,在委托调用之前进行检查,以确保委托非空,并且检查是在本地副本上进行的,以确保线程安全。

在 AnNiu 类的声明之外,Click 成员只能在 += 和 -= 操作符的左侧使用,如 AN . Click += new EventHandler ( )。将委托追加到 Click 事件的调用列表,以及 Click -= new EventHandler ( )。它从 Click 事件的调用列表中删除委托。

在编译类似字段的事件时,编译器应自动创建存储来保存委托,并为该事件创建访问器,以便向委托字段添加或删除事件处理程序。添加和删除操作是线程安全的,并且可以(但不是必须)在持有实例事件的包含对象的锁或静态事件的 System . Type 对象时完成。

因此,一个实例事件声明的形式是:

class X
{
    public event D Ev;
}

应编译成相当于以下内容:

class X
{
    private D _Ev; // 字段用来保存委托

    public event D Ev
    {
        add
        {
            /* 以线程安全的方式添加委托 */
        }
        remove
        {
            /* 以线程安全的方式删除委托 */
        }
    }
}

在类 X 中,对 += 和 -= 操作符左侧的 Ev 的引用导致调用 add 和 remove 访问器。所有其他对 Ev 的引用被编译为引用隐藏字段 _Ev 代替。名称“_Ev”是任意的;隐藏字段可以有任何名称,也可以没有名称。

事件访问器

注意:事件声明通常省略事件访问器声明,就像上面的 AnNiu 例子一样。例如,如果每个事件的一个字段的存储成本不可接受,则可能包括它们。在这种情况下,类可以包含事件访问器声明,并使用 private 机制来存储事件处理程序列表。

事件的事件访问器声明指定与添加和删除事件处理程序相关的可执行语句。

访问器声明由“add”和“remove”组成。每个访问器声明由令牌 add 或 remove 后跟一个块组成。与 add 关联的块指定在添加事件处理程序时执行的语句,与 remove 关联的块指定在删除事件处理程序时执行的语句。

每个“add”和“remove”都对应于一个方法,该方法具有事件类型的单个值参数和一个 void 返回类型。事件访问器的隐式参数名为 value。当在事件赋值中使用事件时,将使用适当的事件访问器。具体来说,如果赋值操作符为 +=,则使用添加访问器,如果赋值操作符为 -=,则使用删除访问器。在这两种情况下,赋值操作符的右操作数都用作事件访问器的参数。“add”或“remove”的块必须符合“分部方法”中描述的 void 方法的规则。特别是,这样的块中的 return 语句不允许指定表达式。

由于事件访问器隐式地具有名为 value 的参数,因此在事件访问器中声明的局部变量或常量具有该名称会导致编译时错误。

class KJ : Component
{
    // 事件的唯一密钥
    static readonly object mouseDownEventKey = new object ( );
    static readonly object mouseUpEventKey = new object ( );

    // 返回与秘钥关联的事件处理程序
    protected Delegate GetEventHandler ( object key )
    {
    }

    // 添加与秘钥关联的事件处理程序
    protected void AddEventHandler ( object key , Delegate handler )
    {
    }

    // 移除与秘钥关联的事件处理程序
    protected void RemoveEventHandler ( object key , Delegate handler )
    {
    }

    // MouseDown 事件
    public event MouseEventHandler MouseDown
    {
        add { AddEventHandler ( mouseDownEventKey ,  value ); }
        remove { RemoveEventHandler ( mouseDownEventKey , value ); }
    }

    // MouseUp 事件
    public event MouseEventHandler MouseUp
    {
        add { AddEventHandler ( mouseUpEventKey , value ); }
        remove { RemoveEventHandler ( mouseUpEventKey , value ); }
    }

    // 调用 MouseUp 事件
    protected void OnMouseUp ( MouseEventArgs args )
    {
        MouseEventHandler handler;
        handler = 
 ( MouseEventHandler ) GetEventHandler ( mouseUpEventKey );
        if ( handler != null )
        {
            handler ( this , args );
        }
    }
}

KJ 类实现了事件的内部存储机制。AddEventHandler 方法将委托值与密钥关联,GetEventHandler 方法返回当前与密钥关联的委托,RemoveEventHandler 方法将委托作为指定事件的事件处理程序移除。据推测,底层存储机制的设计使得将空委托值与键关联没有成本,因此未处理的事件不消耗存储。

static 事件和实例事件

当事件声明包含静态修饰符时,该事件被称为静态事件。当没有静态修饰符时,事件被称为实例事件。

静态事件不与特定实例相关联,在静态事件的访问器中引用此实例会导致编译时错误。

实例事件与类的给定实例相关联,该实例可以在该事件的访问器中以 this 的方式访问。

静态成员和实例成员之间的区别将在“静态成员和实例成员”中进一步讨论。

virtual、sealed、override 和 abstract 访问器

virtual 事件声明指定该事件的访问器是虚拟的。virtual 修饰符适用于事件的两个访问器。

abstract 事件声明指定事件的访问器是虚拟的,但不提供访问器的实际实现。相反,非抽象派生类需要通过覆盖事件来为访问器提供它们自己的实现。因为 abstract 事件声明的访问器不提供实际实现,所以它不应该提供事件访问器声明。

包含 abstract 修饰符和 override 修饰符的事件声明指定事件是抽象的,并覆盖基事件。这种事件的访问器也是抽象的。

abstract 事件声明只允许在 abstract 类中使用。

通过包含指定 override 修饰符的事件声明,可以在派生类中重写继承的 virtual 事件的访问器。这被称为覆盖事件声明。重写事件声明不声明新事件。相反,它只是专门化现有虚拟事件的访问器的实现。

override 事件声明应该指定与被重写事件完全相同的可访问性修饰符和名称,重写事件和被重写事件的类型之间应该有标识转换,并且添加和删除访问器都应该在声明中指定。

override 事件声明可以包含 sealed 修饰符。使用此修饰符可防止派生类进一步重写事件。sealed 事件的访问器也是密封的。

override 事件声明包含 new 修饰符是编译时错误。

除了声明和调用语法不同之外,virtual、sealed、override 和 abstract 访问器的行为与 virtual、sealed、override 和 abstract 方法完全相同。具体地说,具体地说,在“virtual 方法”、“override 方法”、“sealed 方法”、“abstract 方法”中描述的规则就好像访问器是相应表单的方法一样适用。每个访问器对应一个方法,该方法具有事件类型的单个值参数、void 返回类型和与包含事件相同的修饰符。

索引器

indexer 是一种成员,它允许以与数组相同的方式对对象进行索引。

Unsafe 修饰符仅在不安全代码中可用。

indexer 声明有两种类型:

  • 第一个声明了一个非重值索引器。它的值具有 type 类型。这种索引器可以是可读和/或可写的。
  • 第二个声明了一个重值索引器。它的值是一个引用变量,可以是只读的,指向一个类型为 type 的变量。这种索引器是只读的。

indexer 声明可以包含一组属性和任何一种允许声明的可访问性,new、virtual、override、sealed、abstract 和 extern 修饰符。

关于修饰符的有效组合,indexer 声明遵循与“方法”声明相同的规则,唯一的例外是 static 修饰符不允许在索引器声明中使用。

indexer 声明的类型指定由该声明引入的索引器的元素类型。

注意:由于索引器被设计为在类似数组元素的上下文中使用,因此为数组定义的术语元素类型也用于索引器。

除非 indexer 是显式接口成员实现,否则类型后跟关键字 this。对于显式接口成员实现,类型后跟一个接口类型,即“. this”。与其他成员不同,索引器没有用户定义的名称。

参数列表指定 indexer 的参数。索引器的参数列表对应于方法的参数列表,除了必须指定至少一个参数,并且不允许使用 this、ref 和 out 参数修饰符。

indexer 的类型和参数列表中引用的每个类型至少应该与索引器本身具有同样的可访问性。

索引器体可以由语句体或表达式体组成。在语句体中,访问器声明声明索引器的访问器,它应该被“{”和“}”标记包围。访问器指定与读写索引器元素相关联的可执行语句。

在索引器体中,表达式体由“ => ”后跟表达式 E 和分号组成,完全等同于语句体 { get { return E; } },因此只能用于指定只读索引器,其中 get 访问器的结果由单个表达式给出。

引用索引器体可以由语句体或表达式体组成。在语句体中,get 访问器声明声明索引器的 get 访问器。访问器指定与读取索引器相关联的可执行语句。

在引用索引器体中,表达式体由“=>”后面跟着 ref、引用变量 V 和分号组成,完全等同于语句体 { get { return ref V; } }。

注意:尽管访问索引器元素的语法与访问数组元素的语法相同,但索引器元素不被归类为变量。因此,不可能将索引器元素作为 in、out 或 ref 参数传递,除非索引器被重赋值并因此返回一个引用。

索引器的参数列表定义了索引器的签名。具体来说,索引器的签名由其参数的数量和类型组成。元素类型和参数名称不是索引器签名的一部分。

索引器的签名必须不同于在同一类中声明的所有其他索引器的签名。

当索引器声明包含 extern 修饰符时,该索引器被称为外部索引器。由于外部索引器声明不提供实际实现,因此其访问器声明中的每个“访问器体”都应该是一个分号。

下面的例子声明了一个 Wei数组类,它实现了一个索引器,用于访问位数组中的单个位。

class Wei数组
    {
    int [ ] Wei;
    int CD;

    public Wei数组 ( int 长度 )
        {
        if ( 长度 < 0 )
            {
            throw new ArgumentException ( );
            }
        Wei = new int [ ( ( 长度 - 1 ) >> 5 ) + 1 ];
        this . CD = 长度;
        }

    public int C => CD;
    public bool this [ int 索引 ]
        {
        get
            {
            if ( 索引 < 0 || 索引 >= CD )
                {
                throw new IndexOutOfRangeException ( );
                }
            return ( Wei [ 索引 >> 5 ] & 1 << 索引 ) != 0;
            }
        set
            {
            if ( 索引 < 0 || 索引 >= CD )
                {
                throw new IndexOutOfRangeException ( );
                }
            if ( value )
                {
                Wei [ 索引 >> 5 ] |= 1 << 索引;
                }
            else
                {
                Wei [ 索引 >> 5 ] &= ~( 1 << 索引 );
                }
            }
        }
    }

“Wei数组”类的实例比对应的 bool[] 消耗的内存少得多(因为前者的每个值只占用一个比特,而后者占用一个字节),但它允许与 bool[] 相同的操作。

下面的“统计素数”类使用“Wei数组”类和经典的“筛”算法来计算 2 到给定最大值之间的质数:

你可能感兴趣的:(visual-studio)