7 基本概念

7.1 应用程序启动

可以将程序编译为类库,以用作其他应用程序的一部分,也可以作为可能直接启动的应用程序进行编译。 确定此编译模式的机制是实现定义的,也是此规范的外部。

编译为应用程序的程序应至少包含一个符合以下要求作为入口点的方法:

  • 它应具有名称 Main
  • 它应该是 static
  • 它不应是通用的。
  • 它应以非泛型类型声明。 如果声明该方法的类型是嵌套类型,则其封闭类型都不可以是泛型类型。
  • 它可能有 async 修饰符,前提是方法的返回类型为 System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<int>
  • 返回类型应为voidintSystem.Threading.Tasks.TaskSystem.Threading.Tasks.Task<int>
  • 它不应是没有实现的分部方法(§15.6.9)。
  • 参数列表应为空,或者具有类型 string[]为单个值参数。

注意:具有 async 修饰符的方法必须正好具有上面指定的两种返回类型之一,才能限定为入口点。 方法 async voidasync 返回其他可等待类型的方法,例如 ValueTaskValueTask<int> 不符合入口点的条件。 end note

如果在程序中声明了多个限定为入口点的方法,则外部机制可用于指定哪个方法被视为应用程序的实际入口点。 如果限定方法的返回类型为或void已找到,则任何具有返回类型的intSystem.Threading.Tasks.Task限定方法或System.Threading.Tasks.Task<int>未被视为入口点方法。 将程序编译为应用程序时出现编译时错误,且没有完全一个入口点。 编译为类库的程序可能包含将限定为应用程序入口点的方法,但生成的库没有入口点。

通常,方法的声明辅助功能(§7.5.2)由在其声明中指定的访问修饰符(§15.3.6)确定,并且类型的声明可访问性由其声明中指定的访问修饰符确定。 为了使给定类型的给定方法可调用,该类型和成员都可以访问。 但是,应用程序入口点是一种特殊情况。 具体而言,执行环境可以访问应用程序的入口点,而不考虑其已声明的可访问性及其封闭类型声明的声明可访问性。

当入口点方法的返回类型 System.Threading.Tasks.Task 为或 System.Threading.Tasks.Task<int>,编译器将合成一个调用相应 Main 方法的同步入口点方法。 合成方法具有基于 Main 该方法的参数和返回类型:

  • 合成方法的参数列表与方法的参数列表 Main 相同
  • 如果方法的 Main 返回类型为 System.Threading.Tasks.Task,则合成方法的返回类型为 void
  • 如果方法的 Main 返回类型为 System.Threading.Tasks.Task<int>,则合成方法的返回类型为 int

合成方法的执行继续,如下所示:

  • 合成方法调用该方法Main,如果Main该方法具有此类参数,则将其string[]参数值作为参数传递。
  • Main如果方法引发异常,则异常由合成方法传播。
  • 否则,合成入口点会等待返回的任务完成,调用GetAwaiter().GetResult()任务,使用无参数实例方法或 §C.3 描述的扩展方法。 如果任务失败, GetResult() 将引发异常,并且此异常由合成方法传播。
  • 对于返回类型System.Threading.Tasks.Task<int>Main/> 的方法,如果任务成功完成,int则从合成方法返回的值GetResult()

应用程序的有效入口点是在程序内声明的入口点,或合成方法(如果需要,如上文所述)。 因此,有效入口点的返回类型始终 void 为或 int

运行应用程序时, 将创建新的应用程序域 。 应用程序的不同实例化可能同时存在于同一台计算机上,并且每个实例都有其自己的应用程序域。 应用程序域通过充当应用程序状态的容器来启用应用程序隔离。 应用程序域充当应用程序中定义的类型和其使用的类库的容器和边界。 加载到一个应用程序域中的类型不同于加载到另一个应用程序域中的相同类型,并且对象实例不会直接在应用程序域之间共享。 例如,每个应用程序域都有其自己的这些类型的静态变量副本,并且每个应用程序域最多运行一次类型的静态构造函数。 实现可以自由提供实现定义的策略或机制,用于创建和销毁应用程序域。

执行环境调用应用程序的有效入口点时,将发生应用程序启动。 如果有效的入口点声明参数,则在应用程序启动期间,实现应确保该参数的初始值是对字符串数组的非 null 引用。 此数组应包含对字符串的非 null 引用(称为 应用程序参数),这些字符串在应用程序启动之前由主机环境提供实现定义的值。 目的是向应用程序信息提供在托管环境中的其他地方启动应用程序之前确定的应用程序信息。

注意:在支持命令行的系统上,应用程序参数对应于通常称为命令行参数的内容。 end note

如果有效入口点的返回类型为 int,则执行环境调用的方法返回值用于应用程序终止(§7.2)。

除上面列出的情况外,入口点方法的行为与并非每个方面入口点的方法类似。 具体而言,如果在应用程序的生存期内在任何其他点调用入口点(例如通过常规方法调用),则没有特殊处理方法:如果有参数,则它可能具有初始值 null,或者引用包含 null 引用的数组的非null 值。 同样,入口点的返回值在从执行环境调用中没有特殊意义。

7.2 应用程序终止

应用程序终止 将控制权返回到执行环境。

如果应用程序的有效入口点方法int的返回类型和执行完成而不导致异常,则返回的值int将用作应用程序的终止状态代码。 此代码的目的是允许成功或失败的通信到执行环境。 如果有效入口点方法 void 的返回类型和执行完成而不导致异常,则终止状态代码为 0

如果有效的入口点方法因异常(§21.4)而终止,则退出代码是实现定义的。 此外,实现还可以提供用于指定退出代码的替代 API。

是否在应用程序终止过程中运行终结器(§15.13)是实现定义的。

注意:.NET Framework 实现对其尚未进行垃圾回收的所有对象进行调用终结器(§15.13),除非已取消此类清理(例如,通过对库方法GC.SuppressFinalize的调用)。 end note

7.3 声明

C# 程序中的声明定义程序的构成元素。 C# 程序使用命名空间进行组织。 这些是使用命名空间声明(§14)引入的,可以包含类型声明和嵌套命名空间声明。 类型声明(§14.7)用于定义类(§15)、结构(§16)、接口(§18)、枚举(§19)和委托(§20)。 类型声明中允许的成员类型取决于类型声明的形式。 例如, 类声明可以包含常量(§15.4)、字段(§15.5)、方法(§15.6)、属性(§15.7)、事件(§15.8)、索引器(§15.8)的声明 9)、运算符(§15.10)、实例构造函数(§15.11)、静态构造函数(§15.12)、终结器(§15.13)和嵌套类型(§15.3.9)。

声明在声明所属的声明空间定义名称。 在声明空间中引入同名成员的两个或更多声明是编译时错误,但以下情况除外:

  • 同一声明空间中允许两个或多个具有相同名称的命名空间声明。 此类命名空间声明聚合为形成单个逻辑命名空间并共享单个声明空间。
  • 单独程序中的声明,但允许在同一命名空间声明空间中共享相同的名称。

    注意:但是,如果包含在同一应用程序中,这些声明可能会引入歧义。 end note

  • 在同一声明空间(§7.6)中允许两个或多个具有相同名称但不同的签名的方法。
  • 在同一声明空间(§7.8.2)中,允许两个或多个具有相同名称但类型参数数量不同的类型声明。
  • 具有相同声明空间中部分修饰符的两个或多个类型声明可以共享相同的名称、相同数量的类型参数和相同的分类(类、结构或接口)。 在这种情况下,类型声明对单个类型的贡献,并自行聚合为形成单个声明空间(§15.2.7)。
  • 命名空间声明和同一声明空间中的类型声明可以共享同一名称,前提是类型声明至少有一个类型参数(§7.8.2)。

有几种不同类型的声明空间,如下所述。

  • 在程序的所有编译单元中,没有封闭namespace_declaration的namespace_member_declaration是称为全局声明空间的单个合并声明空间的成员。
  • 在程序的所有编译单元中, namespace_declaration具有相同 完全限定命名空间名称的namespace_member_declaration是单个合并声明空间的成员。
  • 每个compilation_unitnamespace_body都有别名声明空间。 compilation_unit或namespace_body的每个extern_alias_directiveusing_alias_directive都为别名声明空间(§14.5.2)贡献成员。
  • 每个非分部类、结构或接口声明都会创建新的声明空间。 每个分部类、结构或接口声明都与同一程序中的所有匹配部分共享的声明空间(§16.2.4)。 名称通过 class_member_declarations、struct_member_declaration s、interface_member_declarations 或 type_parameters 引入此声明空间。 除了重载的实例构造函数声明和静态构造函数声明外,类或结构不能包含与类或结构同名的成员声明。 类、结构或接口允许声明重载方法和索引器。 此外,类或结构允许声明重载的实例构造函数和运算符。 例如,类、结构或接口可能包含具有相同名称的多个方法声明,前提是这些方法声明在其签名(§7.6)中有所不同。 请注意,基类不为类的声明空间做出贡献,基接口不为接口的声明空间做出贡献。 因此,允许派生类或接口声明与继承成员同名的成员。 据说这样一个成员隐藏继承的成员。
  • 每个委托声明都会创建新的声明空间。 名称通过参数(fixed_parameter s 和 parameter_arrays)type_parameters 引入此声明空间。
  • 每个枚举声明都会创建新的声明空间。 名称通过 enum_member_declarations引入此声明空间。
  • 每个方法声明、属性声明、属性访问器声明、索引器声明、索引器访问器声明、运算符声明、实例构造函数声明、匿名函数和本地函数都会创建一 个名为局部变量声明空间的新声明空间。 名称通过参数(fixed_parameter s 和 parameter_arrays)type_parameters 引入此声明空间。 属性或索引器的 set 访问器将名称 value 作为参数引入。 函数成员、匿名函数或本地函数(如果有)的主体被视为嵌套在本地变量声明空间中。 当局部变量声明空间和嵌套局部变量声明空间包含具有相同名称的元素时,在嵌套本地名称的范围内,外部本地名称被嵌套的本地名称隐藏(§7.7.1)。
  • 其他局部变量声明空间可能发生在成员声明、匿名函数和本地函数中。 名称通过模式、declaration_expression s、declaration_statements 和 exception_specifiers 引入到这些声明空间中。 局部变量声明空间可以嵌套,但局部变量声明空间和嵌套的局部变量声明空间包含具有相同名称的元素是错误的。 因此,在嵌套声明空间中,不可能在封闭声明空间中声明与参数、类型参数、局部变量、局部函数或常量同名的局部变量、局部函数或常量。 只要声明空间都不包含另一个声明空间,两个声明空间都可包含同名的元素。 本地声明空间由以下构造创建:
    • 字段和属性声明中的每个variable_initializer都引入了自己的局部变量声明空间,该空间不会嵌套在任何其他局部变量声明空间中。
    • 函数成员、匿名函数或本地函数的主体(如果有)将创建一个局部变量声明空间,该空间被视为嵌套在函数的局部变量声明空间中。
    • 每个 constructor_initializer 都会创建一个嵌套在实例构造函数声明中的局部变量声明空间。 构造函数正文的局部变量声明空间反过来嵌套在此局部变量声明空间中。
    • 每个 switch_blockspecific_catch_clauseiteration_statementusing_statement 都会创建嵌套的局部变量声明空间。
    • 不属于statement_list的每个embedded_statement都会创建嵌套的局部变量声明空间。
    • 每个 switch_section 都会创建一个嵌套的局部变量声明空间。 但是,直接在switch_section的statement_list声明的变量(但不在statement_list内的嵌套局部变量声明空间内)直接添加到封闭switch_block的局部变量声明空间,而不是switch_section
    • query_expression§12.20.3)的语法翻译可能会引入一个或多个 lambda 表达式。 作为匿名函数,每个函数都会创建一个本地变量声明空间,如前所述。
  • 每个 switch_block 都会为标签创建单独的声明空间。 名称通过 labeled_statements 引入此声明空间,并通过 goto_statements 引用名称。 块的标签声明空间包括任何嵌套块。 因此,在嵌套块中,不可能在封闭块中声明与标签同名的标签。

注意:将直接在switch_section声明的变量添加到switch_block局部变量声明空间中,而不是switch_section可能会导致令人惊讶的代码。 在下面的示例中,局部变量 y 位于默认事例的 switch 节中,尽管声明出现在开关节中,但大小写为 0。 局部变量 z 不在默认情况的 switch 节范围内,因为它在发生声明的 switch 节的局部变量声明空间中引入。

int x = 1;
switch (x)
{
    case 0:
        int y;
        break;
    case var z when z < 10:
        break;
    default:
        y = 10;
        // Valid: y is in scope
        Console.WriteLine(x + y);
        // Invalid: z is not scope
        Console.WriteLine(x + z);
        break;
}

end note

声明名称的文本顺序通常没有意义。 具体而言,文本顺序对于命名空间、常量、方法、属性、事件、索引器、运算符、实例构造函数、终结器、静态构造函数和类型的声明和使用并不重要。 声明顺序在以下方面非常重要:

  • 字段声明的声明顺序决定了执行其初始值设定项(如果有)的顺序(§15.5.6.2§15.5.6.3)。
  • 应先定义局部变量,然后再使用它们(§7.7)。
  • 当省略constant_expression值时,枚举成员声明的声明顺序(§19.4)非常重要。

示例:命名空间的声明空间为“open end”,并且具有相同完全限定名称的两个命名空间声明对同一声明空间的贡献。 例如

namespace Megacorp.Data
{
    class Customer
    {
        ...
    }
}

namespace Megacorp.Data
{
    class Order
    {
        ...
    }
}

上述两个命名空间声明为相同的声明空间,在本例中声明两个具有完全限定名称 Megacorp.Data.Customer 的类和 Megacorp.Data.Order。 由于这两个声明对同一声明空间的贡献,因此如果每个声明都包含具有相同名称的类的声明,则会导致编译时错误。

end 示例

注意:如上所述,块的声明空间包括任何嵌套块。 因此,在下面的示例中, FG 方法会导致编译时错误,因为名称 i 是在外部块中声明的,无法在内部块中重新声明。 但是,由于HI这两i个函数在单独的非嵌套块中声明,并且方法有效。

class A
{
    void F()
    {
        int i = 0;
        if (true)
        {
            int i = 1;
        }
    }

    void G()
    {
        if (true)
        {
            int i = 0;
        }
        int i = 1;
    }

    void H()
    {
        if (true)
        {
            int i = 0;
        }
        if (true)
        {
            int i = 1;
        }
    }

    void I()
    {
        for (int i = 0; i < 10; i++)
        {
            H();
        }
        for (int i = 0; i < 10; i++)
        {
            H();
        }
    }
}

end note

7.4 成员

7.4.1 常规

命名空间和类型具有 成员

注意:实体的成员通过使用以对实体的引用开头的限定名称(后跟“.”标记)后跟成员的名称正式发布。 end note

类型的成员在类型声明中声明或继承自该类型的基类。 当类型继承自基类时,基类的所有成员(实例构造函数、终结器和静态构造函数除外)将成为派生类型的成员。 基类成员的声明可访问性不控制成员是否继承 — 继承扩展到不是实例构造函数、静态构造函数或终结器的任何成员。

注意:但是,继承的成员可能无法在派生类型中访问,例如,由于其声明的辅助功能(§7.5.2)。 end note

7.4.2 命名空间成员

没有封闭命名空间的命名空间和类型是全局命名空间的成员。 这直接对应于全局声明空间中声明的名称。

命名空间中声明的命名空间和类型是该命名空间的成员。 这直接对应于命名空间的声明空间中声明的名称。

命名空间没有任何访问限制。 无法声明专用、受保护或内部命名空间,并且命名空间名称始终可公开访问。

7.4.3 结构成员

结构的成员是在结构中声明的成员,以及继承自结构直接基类和间接基类System.ValueTypeobject的成员。

简单类型的成员直接与简单类型(§8.3.5)别名的结构类型的成员相对应。

7.4.4 枚举成员

枚举的成员是在枚举中声明的常量,也是从枚举的直接基类和间接基类System.EnumSystem.ValueType继承的成员。object

7.4.5 类成员

类的成员是在类中声明的成员和从基类继承的成员(没有基类的类 object 除外)。 从基类继承的成员包括常量、字段、方法、属性、事件、索引器、运算符和基类的类型,但不包括基类的实例构造函数、终结器和静态构造函数。 基类成员继承而不考虑其可访问性。

类声明可能包含常量、字段、方法、属性、事件、索引器、运算符、实例构造函数、终结器、静态构造函数和类型的声明。

(§8.2.3) 和 string§8.2.5) 的成员object直接对应于它们别名的类类型的成员。

7.4.6 接口成员

接口的成员是在接口和接口的所有基接口中声明的成员。

注意:类 object 中的成员不是任何接口的成员(§18.4)。 但是,类 object 中的成员可通过任何接口类型(§12.5)中的成员查找获得。 end note

7.4.7 数组成员

数组的成员是从类 System.Array继承的成员。

7.4.8 委托成员

委托从类 System.Delegate继承成员。 此外,它还包含一个方法,其 Invoke 声明中指定的返回类型和参数列表相同(§20.2)。 此方法的调用的行为应与同一委托实例上的委托调用(§20.6)相同。

实现可以通过继承或直接在委托本身中提供其他成员。

7.5 成员访问权限

7.5.1 常规

成员声明允许控制成员访问。 成员的可访问性由成员的声明辅助功能(§7.5.2)与立即包含类型的辅助功能(如果有)建立。

允许访问特定成员时,据说该成员可访问。 相反,不允许访问特定成员时,该成员据说不可 访问。 在成员的辅助功能域(§7.5.3)中包含访问的文本位置时,允许访问成员。

7.5.2 已声明的辅助功能

成员 的声明可访问性 可以是下列项之一:

  • 公共,方法是在成员声明中包括 public 修饰符。 直观的含义 public 是“访问不受限制”。
  • 受保护,通过在成员声明中包括 protected 修饰符来选择它。 直观的含义 protected 是“访问仅限于派生自包含类的包含类或类型”。
  • 内部,通过在成员声明中包括 internal 修饰符来选择它。 直观的含义 internal 是“访问仅限于此程序集”。
  • 受保护的内部,通过包括成员声明中的修饰 protected 符和 internal 修饰符来选择。 直观的含义 protected internal 是“在此程序集中访问,以及派生自包含类的类型”。
  • 专用保护,通过同时包括成员声明中的修饰 private 符和 protected 修饰符来选择。 直观的含义 private protected 是“可通过派生自包含类的包含类和类型在此程序集中访问”。
  • 私有,通过在成员声明中包括 private 修饰符来选择它。 直观的含义 private 是“访问仅限于包含类型”。

根据成员声明的发生上下文,只允许某些类型的声明辅助功能。 此外,当成员声明不包含任何访问修饰符时,声明所在的上下文将确定默认声明的可访问性。

  • 命名空间隐式声明 public 了辅助功能。 命名空间声明上不允许任何访问修饰符。
  • 直接在编译单元或命名空间中声明的类型(而不是在其他类型内)可以具有 publicinternal 声明辅助功能,并默认声明 internal 辅助功能。
  • 类成员可以具有任何允许的已声明辅助功能类型,并默认为 private 声明的辅助功能。

    注意:声明为类成员的类型可以具有任何允许的声明辅助功能类型,而声明为命名空间成员的类型只能 public 具有或 internal 声明的可访问性。 end note

  • 结构成员可以具有 publicinternalprivate 声明的辅助功能,并且默认为 private 已声明的辅助功能,因为结构是隐式密封的。 在(即不是由该结构继承)中struct引入的结构成员不能具有protectedprotected internalprivate protected声明的可访问性。

    注意:声明为结构成员的类型可以具有 publicinternalprivate 声明的可访问性,而声明为命名空间成员的类型只能 public 具有或 internal 声明的可访问性。 end note

  • 接口成员隐式声明 public 了辅助功能。 接口成员声明上不允许任何访问修饰符。
  • 枚举成员隐式声明 public 可访问性。 枚举成员声明上不允许任何访问修饰符。

7.5.3 辅助功能域

成员 的辅助功能域 由允许访问成员的程序文本的(可能不连续)部分组成。 为了定义成员的辅助功能域,如果成员未在类型中声明,则表示成员是顶级成员,如果成员在另一种类型中声明,则表示该成员将嵌套。 此外,程序的程序文本定义为程序的所有编译单元中包含的所有文本,并且类型的程序文本定义为该类型type_declaration中包含的所有文本(包括可能嵌套在该类型中的类型)。

预定义类型的辅助功能域(例如 objectintdouble)不受限制。

在程序中P声明的顶级未绑定类型的T辅助功能域(§8.4.4)的定义如下:

  • 如果声明的 T 辅助功能是公共的,则辅助功能域 T 是程序文本 P 以及引用 P的任何程序。
  • 如果声明的 T 辅助功能是内部的,则辅助功能域 T 是程序文本 P

注意:从这些定义来看,顶级未绑定类型的辅助功能域始终至少是声明该类型的程序的程序文本。 end note

构造类型的 T<A₁, ..., Aₑ> 辅助功能域是未绑定泛型类型的 T 辅助功能域和类型参数的辅助功能域的 A₁, ..., Aₑ交集。

在程序中P的类型中T声明的嵌套成员M的辅助功能域定义如下(指出M自身可能是一种类型):

  • 如果 M 的已声明可访问性为 public,则 M 的可访问域就是 T 的可访问域。
  • 如果声明的可访问性Mprotected internal,则让D程序文本与派生自T的任何类型的程序文本P的并集,该文本在外部P声明。 辅助功能域M是辅助D功能域T的交集。
  • 如果声明的可访问性M为,则让我们D成为程序文本的P交集以及派生自T的任何类型的程序文本Tprivate protected。 辅助功能域M是辅助D功能域T的交集。
  • 如果声明的可访问性Mprotected,则让D程序文本与派生自T的任何类型的程序文本T的并集。 辅助功能域M是辅助D功能域T的交集。
  • 如果 M 的已声明可访问性为 internal,则 M 的可访问域就是 T 的可访问域与 P 的程序文本之间的交集。
  • 如果 M 的已声明可访问性为 private,则 M 的可访问域就是 T 的程序文本。

注意:从这些定义中,嵌套成员的辅助功能域始终至少是声明成员的类型的程序文本。 此外,它遵循的是,成员的辅助功能域绝不比声明成员类型的辅助功能域更具包容性。 end note

注意:在直观的术语中,当访问类型或成员 M 时,将评估以下步骤以确保允许访问:

  • 首先,如果在 M 类型(而不是编译单元或命名空间)中声明,则如果无法访问该类型,则会发生编译时错误。
  • 然后,如果是Mpublic,则允许访问。
  • 否则,如果是Mprotected internal,则允许在声明的程序内M进行访问,或者在从声明的类派生的类M(§7.5.4)内进行访问。
  • 否则,如果是Mprotected,则允许在声明的M类内进行访问,或者在从其声明并通过派生类类型(§7.5.4)派生的类M中发生访问。
  • 否则,如果是Minternal,则允许在声明的程序内M进行访问。
  • 否则,如果是Mprivate,则允许在声明其M类型内进行访问。
  • 否则,类型或成员不可访问,并且会发生编译时错误。 end note

示例:在以下代码中

public class A
{
    public static int X;
    internal static int Y;
    private static int Z;
}

internal class B
{
    public static int X;
    internal static int Y;
    private static int Z;

    public class C
    {
        public static int X;
        internal static int Y;
        private static int Z;
    }

    private class D
    {
        public static int X;
        internal static int Y;
        private static int Z;
    }
}

类和成员具有以下辅助功能域:

  • 辅助功能域不受AA.X限制。
  • 辅助功能域A.Y、、BB.XB.YB.C以及B.C.XB.C.Y包含程序的程序文本。
  • 辅助功能域A.Z是程序文本 。A
  • 辅助功能域 B.Z 及其 B.D 是程序文本 B,包括程序文本 B.CB.D
  • 辅助功能域B.C.Z是程序文本 。B.C
  • 辅助功能域 B.D.X 及其 B.D.Y 是程序文本 B,包括程序文本 B.CB.D
  • 辅助功能域B.D.Z是程序文本 。B.D 如示例所示,成员的辅助功能域永远不会大于包含类型的辅助功能域。 例如,即使所有 X 成员都具有公共声明的可访问性,但所有 A.X 成员都具有受包含类型的约束的辅助功能域。

end 示例

如 §7.4 中所述,除实例构造函数、终结器和静态构造函数外,基类的所有成员均由派生类型继承。 这包括基类的私有成员。 但是,专用成员的辅助功能域仅包括声明成员的类型的程序文本。

示例:在以下代码中

class A
{
    int x;

    static void F(B b)
    {
        b.x = 1;         // Ok
    }
}

class B : A
{
    static void F(B b)
    {
        b.x = 1;         // Error, x not accessible
    }
}

B类从A类继承私有成员x。 由于成员是私有的,因此只能在class_bodyA访问该成员。 因此,访问方法 b.x 成功 A.F ,但在方法中 B.F 失败。

end 示例

7.5.4 受保护的访问

protected当在声明它的类的程序文本之外访问某个或private protected实例成员,并在声明它的程序的程序文本外部访问实例成员时protected internal,访问应在派生自声明它的类的类声明中进行。 此外,需要通过派生类类型的实例或从中构造的类类型进行访问。 此限制可防止一个派生类访问其他派生类的受保护成员,即使成员继承自同一基类也是如此。

让我们 B 成为声明受保护实例成员 M的基类,并成为 D 派生自 B的类。 在class_bodyD中,访问M可以采用以下形式之一:

  • 表单的不合格type_nameprimary_expressionM
  • 窗体E.M的primary_expression,前提是ET类型是或派生自T的类,其中T是类D,或从D中构造的类类型。
  • 窗体base.M的primary_expression
  • 窗体base[argument_list的primary_expression。]

除了这些形式的访问外,派生类还可以访问constructor_initializer(§15.11.2)中基类的受保护实例构造函数。

示例:在以下代码中

public class A
{
    protected int x;

    static void F(A a, B b)
    {
        a.x = 1; // Ok
        b.x = 1; // Ok
    }
}

public class B : A
{
    static void F(A a, B b)
    {
        a.x = 1; // Error, must access through instance of B
        b.x = 1; // Ok
    }
}

内部A,可以通过两者的BA实例进行访问x,因为无论在哪种情况下,访问都通过派生自A的类的A实例进行。 但是,在内部B,无法通过实例A进行访问x,因为A无法从中B派生。

end 示例

示例:

class C<T>
{
    protected T x;
}

class D<T> : C<T>
{
    static void F()
    {
        D<T> dt = new D<T>();
        D<int> di = new D<int>();
        D<string> ds = new D<string>();
        dt.x = default(T);
        di.x = 123;
        ds.x = "test";
    }
}

在这里,允许这三个赋值 x ,因为它们都通过从泛型类型构造的类类型的实例进行。

end 示例

注意:在泛型类中声明的受保护成员的辅助功能域(§7.5.3)包括从该泛型类构造的任何类型派生的所有类声明的程序文本。 在示例中:

class C<T>
{
    protected static T x;
}

class D : C<string>
{
    static void Main()
    {
        C<int>.x = 5;
    }
}

即使类D派生自 C<string>.,对成员C<int>.xD引用protected也是有效的。 end note

7.5.5 辅助功能约束

C# 语言中的多个构造要求类型至少可以像成员或其他类型一样易于访问。 T如果辅助功能域是辅助功能域TM的超集,则类型至少与成员或类型M一样可访问。 换句话说,T至少可以像在可访问的所有上下文M中一样MT易于访问。

存在以下辅助功能约束:

  • 类类型的直接基类应至少与类类型本身一样可访问。
  • 接口类型的显式基接口应至少与接口类型本身一样可访问。
  • 委托类型的返回类型和参数类型应至少与委托类型本身一样可访问。
  • 常量的类型应至少与常量本身一样可访问。
  • 字段的类型应至少与字段本身一样易于访问。
  • 方法的返回类型和参数类型应至少与方法本身一样易于访问。
  • 属性的类型应至少与属性本身一样可访问。
  • 事件的类型应至少与事件本身一样可访问。
  • 索引器的类型和参数类型应至少与索引器本身一样可访问。
  • 运算符的返回类型和参数类型应至少与运算符本身一样可访问。
  • 实例构造函数的参数类型至少应与实例构造函数本身一样可访问。
  • 类型参数上的接口或类类型约束应至少与声明约束的成员一样可访问。

示例:在以下代码中

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

B类导致编译时错误,因为A至少不可访问。B

end 示例

示例:同样,在以下代码中

class A {...}

public class B
{
    A F() {...}
    internal A G() {...}
    public A H() {...}
}

B该方法H会导致编译时错误,因为返回类型A至少不能像该方法一样易于访问。

end 示例

7.6 签名和重载

方法、实例构造函数、索引器和运算符的特点是其 签名

  • 方法的签名包括方法的名称、类型参数的数量以及其每个参数的类型和参数传递模式,以从左到右的顺序考虑。 出于这些目的,在参数类型中发生的方法的任何类型参数都不是按名称来标识的,而是按方法的类型参数列表中的序号位置进行标识。 方法的签名具体不包括返回类型、参数名称、类型参数名称、类型参数约束、 params 参数 this 修饰符,或者参数是必需参数还是可选参数。
  • 实例构造函数的签名由其每个参数的类型和参数传递模式组成,按从左到右的顺序考虑。 实例构造函数的签名具体不包括可为最正确参数指定的修饰符,也不包括 params 参数是必需参数还是可选参数。
  • 索引器的签名由其每个参数的类型组成,这些参数按从左到右的顺序考虑。 索引器的签名特别不包括元素类型,也不包括可为最正确参数指定的修饰符,也不包括 params 参数是必需参数还是可选参数。
  • 运算符的签名由运算符的名称及其每个参数的类型组成,这些参数按从左到右的顺序考虑。 具体来说,运算符的签名不包括结果类型。
  • 转换运算符的签名由源类型和目标类型组成。 转换运算符的隐式或显式分类不是签名的一部分。
  • 如果同一成员类型(方法、实例构造函数、索引器或运算符)的两个签名具有相同的名称、类型参数数、参数数和参数传递模式,并且其相应参数的类型(§10.2.2.2)之间存在标识转换,则被视为相同的签名。

签名是用于 在类、结构和接口中重载 成员的启用机制:

  • 重载方法允许类、结构或接口声明具有相同名称的多个方法,前提是它们的签名在该类、结构或接口中是唯一的。
  • 实例构造函数的重载允许类或结构声明多个实例构造函数,前提是其签名在该类或结构中是唯一的。
  • 重载索引器允许类、结构或接口声明多个索引器,前提是其签名在该类、结构或接口中是唯一的。
  • 重载运算符允许类或结构声明具有相同名称的多个运算符,前提是其签名在该类或结构中是唯一的。

尽管in参数outref修饰符被视为签名的一部分,但在单个类型中声明的成员不能完全因签名而in有所不同,out而且ref。 如果在具有相同签名的同一类型中声明了两个成员,则会发生编译时错误,如果具有修饰符或in修饰符的两个方法out中的所有参数都ref更改为修饰符,则会发生相同的错误。 对于签名匹配的其他目的(例如隐藏或重写),inout并且ref被视为签名的一部分,并且彼此不匹配。

注意:此限制允许将 C# 程序轻松翻译为在公共语言基础结构(CLI)上运行,这不提供定义完全不同inout的方法的方法,以及refend note

object类型,dynamic在比较签名时不区分。 因此,在单个类型中声明的成员,其签名仅允许替换为objectdynamic不同的成员。

示例:以下示例显示了一组重载的方法声明及其签名。

interface ITest
{
    void F();                   // F()
    void F(int x);              // F(int)
    void F(ref int x);          // F(ref int)
    void F(out int x);          // F(out int) error
    void F(object o);           // F(object)
    void F(dynamic d);          // error.
    void F(int x, int y);       // F(int, int)
    int F(string s);            // F(string)
    int F(int x);               // F(int) error
    void F(string[] a);         // F(string[])
    void F(params string[] a);  // F(string[]) error
    void F<S>(S s);             // F<0>(0)
    void F<T>(T t);             // F<0>(0) error
    void F<S,T>(S s);           // F<0,1>(0)
    void F<T,S>(S s);           // F<0,1>(1) ok
}

请注意,任何in参数outref修饰符(§15.6.2)都是签名的一部分。 因此,F(int)F(in int)F(out int) F(ref int)都是唯一的签名。 但是, F(in int)F(out int) 并且F(ref int)不能在同一接口中声明,因为它们的签名完全不同inout并且ref。 此外,请注意,返回类型和 params 修饰符不是签名的一部分,因此不能仅基于返回类型或修饰符的 params 包含或排除重载。 因此,方法和F(int)F(params string[])上述标识的声明会导致编译时错误。 end 示例

7.7 范围

7.7.1 常规

名称 的范围 是程序文本的区域,其中可以引用由名称声明的实体,而无需限定名称。 范围可以 嵌套,内部范围可能会重新声明外部范围中名称的含义。 (但是,这并不能消除 §7.3 施加的限制,即在嵌套块中,不可能在封闭块中声明与局部变量或局部常量同名的局部变量或局部常量。然后,将外部范围的名称隐藏在内部作用域涵盖的程序文本区域中,并且只能通过限定名称来访问外部名称。

在命名空间、类、结构或枚举成员的作用域内,可以在成员声明之前的文本位置引用成员。

示例:

class A
{
    void F()
    {
        i = 1;
    }

    int i = 0;
}

在这里,在声明之前引用i它是有效的F

end 示例

在本地变量的作用域内,在声明符之前的文本位置引用局部变量是编译时错误。

示例:

class A
{
    int i = 0;

    void F()
    {
        i = 1;                // Error, use precedes declaration
        int i;
        i = 2;
    }

    void G()
    {
        int j = (j = 1);     // Valid
    }

    void H()
    {
        int a = 1, b = ++a; // Valid
    }
}

在上述方法中 F ,要专门分配的第一个赋值 i 不引用在外部作用域中声明的字段。 相反,它引用局部变量,并导致编译时错误,因为它在变量声明之前进行文本表示。 在 G 方法中, j 在初始值设定项中对声明 j 的使用有效,因为该用法不在声明符之前。 在方法中 H ,后续声明符正确地引用在同 一local_variable_declaration中早期声明符中声明的局部变量。

end 示例

注意:局部变量和局部常量的范围规则旨在保证表达式上下文中使用的名称的含义在块中始终相同。 如果局部变量的范围只从它的声明扩展到块的末尾,则在上面的示例中,第一个赋值将分配给实例变量,第二个赋值将分配给局部变量,这可能会导致编译时错误(如果块的语句稍后重新排列)。

块中名称的含义可能因使用该名称的上下文而异。 在示例中

class A {}

class Test
{
    static void Main()
    {
        string A = "hello, world";
        string s = A;                      // expression context
        Type t = typeof(A);                // type context
        Console.WriteLine(s);              // writes "hello, world"
        Console.WriteLine(t);              // writes "A"
    }
}

名称 A 用于表达式上下文中引用局部变量 A ,在类型上下文中引用类 A

end note

7.7.2 名称隐藏

7.7.2.1 常规

实体的范围通常包含比实体的声明空间更多的程序文本。 具体而言,实体的范围可能包括引入包含相同名称实体的新声明空间的声明。 此类声明会导致原始实体变得 隐藏。 相反,实体在未隐藏时将 可见

当作用域通过嵌套重叠以及范围通过继承重叠时,将发生名称隐藏。 以下子项描述了这两种类型的隐藏的特征。

7.7.2.2 隐藏嵌套

由于在类或结构中嵌套类型、本地函数或 lambda 的结果以及参数、局部变量和局部常量声明的结果,因此,名称隐藏可以通过嵌套在命名空间或命名空间中。

示例:在以下代码中

class A
{
    int i = 0;
    void F()
    {
        int i = 1;

        void M1()
        {
            float i = 1.0f;
            Func<double, double> doubler = (double i) => i * 2.0;
        }
    }

    void G()
    {
        i = 1;
    }
}

F 方法中,实例变量 i 由局部变量 i隐藏,但在该方法中 Gi 仍引用实例变量。 在本地函数 M1 内隐藏 float i 直接外部 i。 lambda 参数 i 隐藏 float i lambda 正文内部。

end 示例

当内部作用域中的名称隐藏外部范围内的名称时,它将隐藏该名称的所有重载事件。

示例:在以下代码中

class Outer
{
    static void F(int i) {}
    static void F(string s) {}

    class Inner
    {
        static void F(long l) {}

        void G()
        {
            F(1); // Invokes Outer.Inner.F
            F("Hello"); // Error
        }
    }
}

调用F(1)调用调用声明,FInner因为内部声明隐藏所有外部匹配项F。 出于同样的原因,调用 F("Hello") 会导致编译时错误。

end 示例

7.7.2.3 通过继承隐藏

当类或结构重新声明从基类继承的名称时,将发生通过继承隐藏的名称。 这种类型的名称隐藏采用以下形式之一:

  • 类或结构中引入的常量、字段、属性、事件或类型隐藏具有相同名称的所有基类成员。
  • 类或结构中引入的方法隐藏具有相同名称的所有非方法基类成员,以及具有相同签名的所有基类方法(§7.6)。
  • 类或结构中引入的索引器隐藏具有相同签名的所有基类索引器(§7.6)。

控制运算符声明(§15.10)的规则使得派生类不可能声明与基类中的运算符具有相同签名的运算符。 因此,运算符永远不会互相隐藏。

与从外部范围隐藏名称相反,将可见名称从继承的作用域中隐藏会导致报告警告。

示例:在以下代码中

class Base
{
    public void F() {}
}

class Derived : Base
{
    public void F() {} // Warning, hiding an inherited name
}

in Derived 声明F会导致报告警告。 隐藏继承的名称特别不是错误,因为这将排除基类的单独演变。 例如,上述情况可能会发生,因为较新版本 BaseF 引入方法在早期版本的类中不存在。

end 示例

通过使用 new 修饰符可以消除隐藏继承名称导致的警告:

示例:

class Base
{
    public void F() {}
}

class Derived : Base
{
    public new void F() {}
}

new修饰符指示 in F Derived 为“new”,并且它确实旨在隐藏继承的成员。

end 示例

新成员的声明仅在新成员的范围内隐藏继承的成员。

示例:

class Base
{
    public static void F() {}
}

class Derived : Base
{
    private new static void F() {} // Hides Base.F in Derived only
}

class MoreDerived : Derived
{
    static void G()
    {
        F();                       // Invokes Base.F
    }
}

在上面的示例中,in 的F声明隐藏F继承自Base的,但由于新FDerived具有私有访问权限,其范围不会扩展到MoreDerivedDerived 因此,调用 F()MoreDerived.G 有效的,并将调用 Base.F

end 示例

7.8 命名空间和类型名称

7.8.1 常规

C# 程序中的多个上下文需要指定 namespace_nametype_name

namespace_name
    : namespace_or_type_name
    ;

type_name
    : namespace_or_type_name
    ;
    
namespace_or_type_name
    : identifier type_argument_list?
    | namespace_or_type_name '.' identifier type_argument_list?
    | qualified_alias_member
    ;

namespace_name引用命名空间的namespace_or_type_name

按照如下所述解决方法,namespace_name的namespace_or_type_name应引用命名空间,否则会发生编译时错误。 namespace_name中没有类型参数(§8.4.2)(只有类型可以具有类型参数)。

type_name引用类型的namespace_or_type_name。 按照如下所述解决方法,type_name的namespace_or_type_name应引用类型,否则会发生编译时错误。

如果namespace_or_type_namequalified_alias_member其含义如 §14.8.1 中所述。 否则, namespace_or_type_name 有四种形式之一:

  • I
  • I<A₁, ..., Aₓ>
  • N.I
  • N.I<A₁, ..., Aₓ>

其中I是单个标识符,N是namespace_or_type_name<A₁, ..., Aₓ>是可选的type_argument_list。 如果未 指定type_argument_list ,请考虑 x 为零。

namespace_or_type_name的含义如下:

  • 如果namespace_or_type_namequalified_alias_member,则含义如 §14.8.1 中所述
  • 否则,如果namespace_or_type_name为窗体I或窗体I<A₁, ..., Aₓ>
    • 如果x为零且namespace_or_type_name出现在泛型方法声明(§15.6)中,但在其方法标头的属性之外如果该声明包含名称I的类型参数(§15.2.3),则namespace_or_type_name引用该类型参数。
    • 否则,如果 namespace_or_type_name 出现在类型声明中,则对于每个实例类型 T§15.3.2),从该类型声明的实例类型开始,并继续每个封闭类或结构声明的实例类型(如果有):
      • 如果x为零且包含名称I的类型参数的声明T,则namespace_or_type_name引用该类型参数。
      • 否则,如果namespace_or_type_name出现在类型声明的正文中,或其T任何基类型都包含具有名称和xI类型参数的嵌套可访问类型,则namespace_or_type_name引用使用给定类型参数构造的类型。 如果有多个此类类型,则会选择在更多派生类型中声明的类型。

      注意:确定namespace_or_type_name的含义时,将忽略非类型成员(常量、字段、方法、属性、索引器、运算符、实例构造函数、终结器和静态构造函数)和类型成员。 end note

    • 否则,对于每个命名空间N,从发生namespace_or_type_name命名空间开始,继续每个封闭命名空间(如果有),以全局命名空间结尾,将计算以下步骤,直到实体所在的位置:
      • 如果 x 为零并且 I 是命名空间的名称 N,则:
        • 如果发生namespace_or_type_name的位置由命名空间声明括起来,并且命名空间声明N包含将名称I与命名空间或类型关联的extern_alias_directiveusing_alias_directive,则namespace_or_type_name不明确且发生编译时错误。
        • 否则,namespace_or_type_name引用名为 I N命名空间。
      • 否则,如果N包含具有名称和Ix类型参数的可访问类型,则:
        • 如果x为零,并且发生namespace_or_type_name的位置由命名空间声明括起来,并且命名空间声明N包含一个extern_alias_directive或using_alias_directive,该I名称与命名空间或类型相关联,则namespace_or_type_name不明确且发生编译时错误。
        • 否则, namespace_or_type_name 引用使用给定类型参数构造的类型。
      • 否则,如果发生namespace_or_type_name的位置由命名空间声明括起来,则为 N
        • 如果x为零,并且命名空间声明包含将名称I与导入的命名空间或类型关联的extern_alias_directiveusing_alias_directive,则namespace_or_type_name引用该命名空间或类型。
        • 否则,如果命名空间声明的 using_namespace_directive 导入的命名空间仅包含一种具有名称和xI类型参数的类型,则namespace_or_type_name引用使用给定类型参数构造的类型。
        • 否则,如果命名空间声明的 using_namespace_directive导入的命名空间包含多个具有名称和Ix类型参数的类型,则namespace_or_type_name不明确且发生错误。
    • 否则, namespace_or_type_name 未定义且发生编译时错误。
  • 否则,namespace_or_type_name为窗体N.I或窗体N.I<A₁, ..., Aₓ>N 首先解析为 namespace_or_type_name。 如果解决方法 N 不成功,则会发生编译时错误。 否则, N.IN.I<A₁, ..., Aₓ> 解析如下:
    • 如果 x 为零并引用命名空间, N 并且 N 包含名称为 I嵌套命名空间,则 namespace_or_type_name 引用该嵌套命名空间。
    • 否则,如果N引用命名空间并N包含具有名称和Ix类型参数的可访问类型,则namespace_or_type_name引用使用给定类型参数构造的类型。
    • 否则,如果N引用(可能构造的)类或结构类型,或其N任何基类包含具有名称和xI类型参数的嵌套可访问类型,则namespace_or_type_name引用使用给定类型参数构造的类型。 如果有多个此类类型,则会选择在更多派生类型中声明的类型。

      注意:如果将 N.I 确定为解析基类规范 N 的一部分,则直接基类 N 被视为 object§15.2.4.2)。 end note

    • 否则, N.I 是无效 的namespace_or_type_name,并且会发生编译时错误。

仅当允许引用静态类(§15.2.2.4)时,才允许namespace_or_type_name

  • namespace_or_type_nameT窗体T.I的namespace_or_type_name,或者
  • namespace_or_type_name是窗体的typeof_expression§12.8.17T typeof(T)

7.8.2 未限定的名称

每个命名空间声明和类型声明都有一个 未限定的名称 ,如下所示:

  • 对于命名空间声明,非限定名称是 声明中指定的qualified_identifier
  • 对于没有 type_parameter_list的类型声明,未限定的名称是在 声明中指定的标识符
  • 对于具有 K 类型参数的类型声明,非限定名称是 声明中指定的标识符 ,后跟 K 类型参数的 generic_dimension_specifier§12.8.17)。

7.8.3 完全限定的名称

每个命名空间和类型声明都有一个完全限定的名称,该名称唯一标识程序中所有其他命名空间或类型声明。 命名空间或类型声明的完全限定名称(未限定名称 N )确定如下:

  • 如果 N 全局命名空间的成员,则其完全限定名称为 N
  • 否则,它的完全限定名称是 S.N,其中 S 命名空间或声明的类型 N 声明的完全限定名称。

换句话说,完全限定的名称N是从全局命名空间开始的标识符的完整分层路径,generic_dimension_specifierN。 由于命名空间或类型的每个成员应具有唯一名称,因此,命名空间或类型声明的完全限定名称始终是唯一的。 对于引用两个不同的实体,同一个完全限定的名称是编译时错误。 具体而言:

  • 命名空间声明和类型声明具有相同的完全限定名称是错误的。
  • 两种不同类型的类型声明具有相同的完全限定名称是错误的(例如,如果结构声明和类声明具有相同的完全限定名称)。
  • 没有部分修饰符的类型声明与另一个类型声明具有相同的完全限定名称(§15.2.7)是错误的。

示例:以下示例显示了多个命名空间和类型声明及其关联的完全限定名称。

class A {}                 // A
namespace X                // X
{
    class B                // X.B
    {
        class C {}         // X.B.C
    }
    namespace Y            // X.Y
    {
        class D {}         // X.Y.D
    }
}
namespace X.Y              // X.Y
{
    class E {}             // X.Y.E
    class G<T>             // X.Y.G<>
    {           
        class H {}         // X.Y.G<>.H
    }
    class G<S,T>           // X.Y.G<,>
    {         
        class H<U> {}      // X.Y.G<,>.H<>
    }
}

end 示例

7.9 自动内存管理

C# 采用自动内存管理,使开发人员无需手动分配和释放对象占用的内存。 自动内存管理策略由垃圾回收器实现。 对象的内存管理生命周期如下所示:

  1. 创建对象时,会为其分配内存,运行构造函数,并将该对象视为 实时对象。
  2. 如果对象及其任何实例字段都不能通过执行的任何可能延续来访问,但除了运行终结器之外,该对象 被视为不再使用 ,并且它有资格进行最终化。

    注意:C# 编译器和垃圾回收器可能选择分析代码,以确定将来可能使用对对象的引用。 例如,如果作用域中的局部变量是对象的唯一现有引用,但从未在过程中当前执行点执行的任何可能延续中引用该局部变量,则垃圾回收器(但不需要)将对象视为不再使用。 end note

  3. 一旦对象有资格进行最终化,在一些未指定的后期,该对象的终结器 (§15.13) (如果有的话)运行。 在正常情况下,对象的终结器仅运行一次,但实现定义的 API 可能允许重写此行为。
  4. 运行对象的终结器后,如果任何可能的继续执行(包括终结器运行)都无法访问该对象及其任何实例字段,则对象被视为不可访问,并且该对象有资格收集。

    注意:由于其终结器,以前无法访问的对象可能会再次变得可访问。 下面提供了一个示例。 end note

  5. 最后,在对象符合回收条件的某个时间,垃圾回收器释放与该对象关联的内存。

垃圾回收器维护有关对象使用情况的信息,并使用此信息做出内存管理决策,例如内存中查找新创建的对象的位置、重新定位对象的时间以及对象不再使用或无法访问时。

与假定存在垃圾回收器的其他语言一样,C# 的设计旨在使垃圾回收器可以实现广泛的内存管理策略。 C# 既不指定该范围内的时间约束,也不指定运行终结器的顺序。 是否在应用程序终止过程中运行终结器是实现定义的(§7.2)。

垃圾回收器的行为可以通过类 System.GC上的静态方法进行某种程度的控制。 此类可用于请求集合发生、要运行的终结器(或未运行),等等。

示例:由于垃圾回收器在决定何时收集对象和运行终结器时允许使用宽纬度,因此一个符合性的实现可能会生成与以下代码所示不同的输出。 程序

class A
{
    ~A()
    {
        Console.WriteLine("Finalize instance of A");
    }
}

class B
{
    object Ref;
    public B(object o)
    {
        Ref = o;
    }

    ~B()
    {
        Console.WriteLine("Finalize instance of B");
    }
}

class Test
{
    static void Main()
    {
        B? b = new B(new A());
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

创建类 A 的实例和类 B的实例。 当变量 b 赋值 null时,这些对象有资格进行垃圾回收,因为在此之后,任何用户编写的代码都不可能访问它们。 输出可以是任一

Finalize instance of A
Finalize instance of B

Finalize instance of B
Finalize instance of A

因为语言对垃圾回收的顺序没有约束。

在微妙的情况下,“符合最终决定条件”和“符合集合条件”之间的区别可能很重要。 例如,

class A
{
    ~A()
    {
        Console.WriteLine("Finalize instance of A");
    }

    public void F()
    {
        Console.WriteLine("A.F");
        Test.RefA = this;
    }
}

class B
{
    public A? Ref;

    ~B()
    {
        Console.WriteLine("Finalize instance of B");
        Ref?.F();
    }
}

class Test
{
    public static A? RefA;
    public static B? RefB;

    static void Main()
    {
        RefB = new B();
        RefA = new A();
        RefB.Ref = RefA;
        RefB = null;
        RefA = null;
        // A and B now eligible for finalization
        GC.Collect();
        GC.WaitForPendingFinalizers();
        // B now eligible for collection, but A is not
        if (RefA != null)
        {
            Console.WriteLine("RefA is not null");
        }
    }
}

在上述程序中,如果垃圾回收器选择在终结器之前运行终结器AB,则此程序的输出可能是:

Finalize instance of A
Finalize instance of B
A.F
RefA is not null

请注意, A 虽然实例未使用并且 A“终结器正在运行”,但仍有可能 A 从另一个终结器调用(在本例中)。 F。 此外,请注意,运行终结器可能会导致对象再次可从主线程序使用。 在这种情况下,运行 B终结器会导致以前未使用的实例 A 从实时引用 Test.RefA访问。 调用 WaitForPendingFinalizers后,实例 B 符合收集条件,但 A 实例不是,因为引用 Test.RefA

end 示例

7.10 执行顺序

执行 C# 程序会继续执行,使每个执行线程的副作用保留在关键执行点。 副作用定义为可变字段的读取或写入、对非易失变量的写入、对外部资源的写入以及引发异常。 应保留这些副作用顺序的关键执行点是对可变字段(§15.5.4)、语句(§13.13)和线程创建和终止的引用。 lock 执行环境可以自由地更改 C# 程序的执行顺序,但有以下约束:

  • 数据依赖保留在执行线程内。 也就是说,将计算每个变量的值,就好像线程中的所有语句都以原始程序顺序执行。
  • 初始化排序规则将保留 (§15.5.5§15.5.6)。
  • 在易失性读取和写入(§15.5.4)方面,保留副作用的顺序。 此外,如果执行环境可以推断该表达式的值未使用,并且不需要产生任何副作用(包括调用方法或访问可变字段引起的任何影响),则不需要计算表达式的一部分。 当程序执行被异步事件(如另一个线程引发的异常)中断时,不能保证可观测的副作用在原始程序顺序中可见。