C# 语言规范_版本5.0 (第3章 基本概念)

时间:2022-05-23 03:28:07

1. 基本概念

1.1 应用程序启动

具有入口点 (entry point) 的程序集称为应用程序 (application)。应用程序运行时,将创建新的应用程序域 (application domain)。同一台计算机上可能会同时运行着同一个应用程序的若干个实例,此时,每一个实例都拥有各自的应用程序域。

应用程序域用作应用程序状态的容器,以此隔离应用程序。应用程序域作为应用程序中和它使用的类库中所定义的类型的容器和边界。同一个类型若被加载到不同的应用程序域中就成为各自独立的客体,由它们在各自应用程序域中产生的实例亦不可直接共享。例如,对于这些类型的静态变量,每个应用程序域都有自己的副本,并且这些类型的静态构造函数在每个应用程序域中也要(最多)运行一次。关于如何处理程序域的创建和销毁,各实现可以按具体情况确定自己的策略或机制。

当执行环境调用指定的方法(称为应用程序的入口点)时发生应用程序启动 (application startup)。此入口点方法总是被命名为 Main,可以具有下列签名之一:

static void Main() {...}

static void Main(string[] args) {...}

static int Main() {...}

static int Main(string[] args) {...}

如上所示,入口点可以选择返回一个 int 值。此返回值用于应用程序终止(第 3.2 节)。

入口点可以包含一个形参(可选)。该参数可以具有任意名称,但参数的类型必须为 string[]。如果存在形参,执行环境会创建并传递一个包含命令行实参的 string[] 实参,这些命令行实参是在启动应用程序时指定的。string[] 参数永远不能为 null,但如果没有指定命令行参数,它的长度可以为零。

由于 C# 支持方法重载,因此类或结构可以包含某个方法的多个定义(前提是每个定义有不同的签名)。但在一个程序内,没有任何类或结构可以包含一个以上的名为 Main 的方法,因为 Main 的定义限定它只能被用作应用程序的入口点。允许使用 Main 的其他重载版本,前提是它们具有一个以上的参数,或者它们的唯一参数的类型不是 string[]。

应用程序可由多个类或结构组成。在这些类或结构中,可能会有若干个拥有自己的 Main 方法,因为 Main 的定义限定它只能被用作应用程序的入口点。这样的情况下,必须利用某种外部机制(如命令行编译器的选项)来选择其中一个 Main 方法用作入口点。

在 C# 中,每个方法都必须定义为类或结构的成员。通常,方法的已声明可访问性(第 3.5.1 节)由其声明中指定的访问修饰符(第 10.3.5 节)确定。同样,类型的已声明可访问性由其声明中指定的访问修饰符确定。为了能够调用给定类型的给定方法,类型和成员都必须是可访问的。然而,应用程序入口点是一种特殊情况。具体而言,执行环境可以访问应用程序的入口点,无论它本身的可访问性和封闭它的类型的可访问性是如何在声明语句中设置的。

应用程序入口点方法不能位于泛型类声明中。

在所有其他方面,入口点方法的行为与非入口点方法类似。

1.2 应用程序终止

应用程序终止 (application termination) 将控制返回给执行环境。

如果应用程序的入口点 (entry point) 方法的返回类型为 int,则返回的值用作应用程序的终止状态代码 (termination status code)。此代码的用途是允许与执行环境进行关于应用程序运行状态(成功或失败)的通信。

如果入口点方法的返回类型为 void,那么在到达终止该方法的右大括号 (}),或者执行不带表达式的 return 语句时,将产生终止状态代码 0。

在应用程序终止之前,将调用其中还没有被垃圾回收的所有对象的析构函数,除非已将这类清理功能设置为取消使用(例如,通过调用库方法 GC.SuppressFinalize)。

1.3 声明

C# 程序中的声明定义程序的构成元素。C# 程序是用命名空间(第 9 章)组织起来的,一个命名空间可以包含类型声明和嵌套的命名空间声明。类型声明(第 9.6 节)用于定义类(第 10 章)、结构(第 10.14 节)、接口(第 13 章)、枚举(第 14 章)和委托(第 15章)。在一个类型声明中可以使用哪些类型作为其成员,取决于该类型声明的形式。例如,类声明可以包含常量声明(第 10.4 节)、字段声明(第 10.5 节)、方法声明(第 10.6 节)、属性声明(第 10.7 节)、事件声明(第 10.8 节)、索引器声明(第 REF10.9节)、运算符声明(第 10.10 节)、实例构造函数声明(第 10.11 节)、静态构造函数声明(第 10.12 节)、析构函数声明(第 10.13 节)和嵌套类型声明(第 10.3.8 节)。

一个声明在它自已所属的那个声明空间 (declaration space) 中定义一个名称。除非是重载成员(第 3.6 节),否则,在同一个声明空间下若有两个以上的声明语句声明了具有相同名称的成员,就会产生编译时错误。同一个声明空间内绝不能包含不同类型的同名成员。例如,声明空间绝不能包含同名的字段和方法。

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

  • 在程序的所有源文件中,namespace-member-declaration 若没有被置于任何一个 namespace-declaration 下,则属于一个称为全局声明空间 (global declaration space) 的组合声明空间。
  • 在程序的所有源文件中,一个 namespace-member-declaration 若在 namespace-declaration 中具有相同的完全限定的命名空间名称,它就属于一个组合声明空间。
  • 每个类、结构或接口声明创建一个新的声明空间。名称将通过 class-member-declaration、struct-member-declaration、interface-member-declaration 或 type-parameter 引入此声明空间。除了重载实例构造函数声明和静态构造函数声明外,类或结构不能包含与该类或结构同名的成员声明。类、结构或接口允许声明重载方法和索引器。另外,类或结构允许重载实例构造函数和运算符的声明。例如,类、结构或接口可以包含多个同名的方法声明,前提是这些方法声明的签名(第 3.6 节)不同。注意,基类与类的声明空间无关,基接口与接口的声明空间无关。因此,允许在派生类或接口内声明与所继承的成员同名的成员。我们说这类成员隐藏 (hide) 了它们继承的那些成员。
  • 每个委托声明创建一个新的声明空间。名称通过形参(fixed-parameter 和 parameter-array)和 type-parameter 引入此声明空间。
  • 每个枚举声明创建一个新的声明空间。名称通过 enum-member-declarations 引入此声明空间。
  • 每个方法声明、索引器声明、运算符声明、实例构造函数声明和匿名函数均创建一个称为局部变量声明空间 (local variable declaration space) 的新声明空间。名称将通过形参(fixed-parameter 和 parameter-array)和 type-parameter 引入此声明空间。函数成员或匿名函数的主体(如果有)将视为嵌套在局部变量声明空间中。如果局部变量声明空间和嵌套的局部变量声明空间包含具有相同名称的元素,则会发生错误。因此,在嵌套声明空间中不可能声明与封闭它的声明空间中的局部变量或常量同名的局部变量或常量。只要两个声明空间彼此互不包含,这两个声明空间就可以包含同名的元素。
  • 每个 block \b  或 switch-block \b  \t "See declaration space, block and"  \b  \t "See declaration space, block and"  \b  以及 forforeachusing 语句都会为局部变量和局部常量创建一个局部变量声明空间 \b  \t "See declaration space, block and"  \b  \t "See declaration space, switch block" 。名称将通过 local-variable-declaration 和 local-constant-declaration 引入此声明空间。请注意,作为函数成员的主体或匿名函数的主体出现或出现在该主体之中的块将嵌套在这些函数为其参数声明的局部变量声明空间中。因此,如果某个方法的局部变量和参数具有相同名称,则会发生错误。
  • 每个 block 或 switch-block 都为标签创建一个单独的声明空间。名称将通过 labeled-statement 引入此声明空间,并通过 goto-statement 进行引用。块的标签声明空间可包含任何嵌套块。因此,在嵌套块中不可能声明与封闭它的块中的标签同名的标签。

声明名称的文本顺序通常不重要。具体而言,声明和使用命名空间、常量、方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数和类型时,文本顺序并不重要。在下列情况下声明顺序非常重要:

  • 字段声明和局部变量声明的声明顺序确定其初始值设定项(如果有)的执行顺序。
  • 在使用局部变量前必须先定义它们(第 3.7 节)。
  • 当省略 constant-expression 值时,枚举成员声明(第 14.3 节)的声明顺序非常重要。

命名空间的声明空间是“开放式的”,两个具有相同的完全限定名的命名空间声明共同构成同一个声明空间。例如

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

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

上面的两个命名空间声明为同一声明空间提供了成员,在本例中它们分别声明了具有完全限定名 Megacorp.Data.Customer 和 Megacorp.Data.Order 的两个类。由于两个声明共同构成同一个声明空间,因此如果每个声明中都包含一个同名类的声明,则将导致编译时错误。

正如上面所述,块的声明空间包括所有嵌套块。因此,在下面的示例中,F 和 G 方法导致编译时错误,因为名称 i 是在外部块中声明的,不能在内部块中重新声明。但方法 H 和 I 都是有效的,因为这两个 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();
}
}

1.4 成员

命名空间和类型具有成员
(member)。通常可以通过限定名来访问实体的成员。限定名以对实体的引用开头,后跟一个“.”标记,再接成员的名称。

类型的成员或者是在该类型声明中声明的,或者是从该类型的基类继承 (inherit) 的。当类型从基类继承时,基类的所有成员(实例构造函数、析构函数和静态构造函数除外)都成为派生类型的成员。基类成员的声明可访问性并不控制该成员是否可继承:继承性可扩展到任何成员,只要它们不是实例构造函数、静态构造函数或析构函数。然而,在派生类型中可能不能访问已被继承的成员,原因或者是因为其已声明可访问性(第 3.5.1 节),或者是因为它已被类型本身中的声明所隐藏(第
3.7.1.2 节)。

1.4.1 命名空间成员

命名空间和类型若没有封闭它的命名空间,则属于全局命名空间 (global namespace) 的成员。这直接对应于全局声明空间中声明的名称。

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

命名空间没有访问限制。不可能把命名空间设置成私有的、受保护的或内部的,命名空间名称始终是可公开访问的。

1.4.2 结构成员

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

简单类型的成员直接对应于结构类型的成员,此简单类型正是该结构的化名:

  • sbyte 的成员是 System.SByte 结构的成员。
  • byte 的成员是 System.Byte 结构的成员。
  • short 的成员是 System.Int16 结构的成员。
  • ushort 的成员是 System.UInt16 结构的成员。
  • int 的成员是 System.Int32 结构的成员。
  • uint 的成员是 System.UInt32 结构的成员。
  • long 的成员是 System.Int64 结构的成员。
  • ulong 的成员是 System.UInt64 结构的成员。
  • char 的成员是 System.Char 结构的成员。
  • float 的成员是 System.Single 结构的成员。
  • double 的成员是 System.Double 结构的成员。
  • decimal 的成员是 System.Decimal 结构的成员。
  • bool 的成员是 System.Boolean 结构的成员。

1.4.3 枚举成员

枚举的成员是在枚举中声明的常量以及继承自枚举的直接基类 System.Enum 和间接基类 System.ValueType 和 object 的成员。

1.4.4 类成员

类的成员是在类中声明的成员和从该类的基类(没有基类的
object 类除外)继承的成员。从基类继承的成员包括基类的常量、字段、方法、属性、事件、索引器、运算符和类型,但不包括基类的实例构造函数、析构函数和静态构造函数。基类成员被是否继承与它们的可访问性无关。

类声明可以包含以下对象的声明:常量、字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数和类型。

object 和 string
的成员直接对应于它们所化名的类类型的成员:

  • object 的成员是 System.Object 类的成员。
  • string 的成员是 System.String 类的成员。

1.4.5 接口成员

接口的成员是在接口中和该接口的所有基接口中声明的成员。严格地说,类 object 中的成员不是任何接口的成员(第 13.2 节)。但是,通过在任何接口类型中进行成员查找,可获得类 object 中的成员(第 7.4 节)。

1.4.6 数组成员

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

1.4.7 委托成员

委托的成员是从类 System.Delegate 继承的成员。

1.5 成员访问

成员的声明可用于控制对该成员的访问。成员的可访问性是由该成员的声明可访问性(第 3.5.1 节)和直接包含它的那个类型的可访问性(若它存在)结合起来确定的。

如果允许访问特定成员,则称该成员是可访问的 (accessible)。相反,如果不允许访问特定成员,则称该成员是不可访问的 (inaccessible)。当引发访问的源代码的文本位置在某成员的可访问域(第 3.5.2 节)中时,允许对该成员进行访问。

1.5.1 已声明可访问性

成员的已声明可访问性
(declared accessibility) 可以是下列类型之一:

  • Public,选择它的方法是在成员声明中包括 public
    修饰符。public
    的直观含义是“访问不受限制”。
  • Protected,选择它的方法是在成员声明中包括 protected 修饰符。protected 的直观含义是“访问范围限定于它所属的类或从该类派生的类型”。
  • Internal,选择它的方法是在成员声明中包括 internal
    修饰符。internal
    的直观含义是“访问范围限定于此程序”。
  • Protected internal(意为受保护或内部的),选择它的方法是在成员声明中包括 protected 和 internal 修饰符。protected internal 的直观含义是“访问范围限定于此程序或那些由它所属的类派生的类型”。
  • Private,选择它的方法是在成员声明中包括 private
    修饰符。private
    的直观含义是“访问范围限定于它所属的类型”。

声明一个成员时所能选择的已声明可访问性的类型,依赖于该成员声明出现处的上下文。此外,当成员声明不包含任何访问修饰符时,声明发生处的上下文会为该成员选择一个默认的已声明可访问性。

  • 命名空间隐式具有 public 已声明可访问性。在命名空间声明中不允许使用访问修饰符。
  • 编译单元或命名空间中声明的类型可以具有 public 或 internal
    已声明可访问性,默认的已声明可访问性为 internal。
  • 类成员可具有五种已声明可访问性中的任何一种,默认为 private 已声明可访问性。(请注意,声明为类成员的类型可具有五种已声明可访问性中的任何一种,而声明为命名空间成员的类型只能具有 public 或 internal 已声明可访问性。)
  • 结构成员可以具有 public、internal 或 private 已声明可访问性并默认为 private 已声明可访问性,这是因为结构是隐式密封的。结构的成员若是在此结构中声明的(也就是说,不是由该结构从它的基类中继承的),则不能具有 protected 或 protected internal 已声明可访问性。(请注意,声明为结构成员的类型可具有 public、internal
    或 private 已声明可访问性,而声明为命名空间成员的类型只能具有 public 或 internal 已声明可访问性。)
  • 接口成员隐式地具有 public 已声明可访问性。在接口成员声明中不允许使用访问修饰符。
  • 枚举成员隐式地具有 public 已声明可访问性。在枚举成员声明中不允许使用访问修饰符。

1.5.2 可访问域

一个成员的可访问域
(accessibility domain) 由(可能是不连续的)程序文本节组成,从该域中可以访问该成员。出于定义成员可访问域的目的,如果成员不是在某个类型内声明的,就称该成员是* (top-level) 的;如果成员是在其他类型内声明的,就称该成员是嵌套 (nested) 的。此外,程序的程序文本 (program text) 定义为包含在该程序的所有源文件中的全部程序文本,而类型的程序文本定义为包含在该类型(可能还包括该类型中的嵌套类型)的 type-declaration 中的所有程序文本。

预定义类型(如 object、int 或 double)的可访问域无限制。

在程序 P 中声明的*未绑定类型 T(第 4.4.3 节)的可访问域定义如下:

  • 如果 T 的已声明可访问性为 public,则 T 的可访问域是 P 以及引用 P 的任何程序的程序文本。
  • 如果 T 的已声明可访问性为 internal,则 T 的可访问域就是 P 的程序文本。

从这些定义可以推断出:*未绑定类型的可访问域始终至少是声明了该类型的程序的程序文本。

构造类型 T<A1, ...,AN>  的可访问域是未绑定的泛型类型 T 的可访问域和类型实参 A1, ...,AN 的可访问域的交集。

在程序 P 内的类型 T 中声明的嵌套成员 M 的可访问域定义如下(注意 M 本身可能就是一个类型):

  • 如果 M 的已声明可访问性为 public,则 M 的可访问域就是 T 的可访问域。
  • 如果 M 的已声明可访问性是 protected internal,则设 D 表示 P 的程序文本和从 T(在 P 外部声明)派生的任何类型的程序文本的并集。M 的可访问域是 T 与 D 的可访问域的交集。
  • 如果 M 的已声明可访问性是 protected,则设 D 表示 T 的程序文本和从 T 派生的任何类型的程序文本的并集。M 的可访问域是 T 与 D 的可访问域的交集。
  • 如果 M 的已声明可访问性为 internal,则 M 的可访问域就是 T 的可访问域与 P 的程序文本之间的交集。
  • 如果 M 的已声明可访问性为 private,则 M 的可访问域就是 T 的程序文本。

从这些定义可以看出:嵌套成员的可访问域总是至少为声明该成员的类型的程序文本。还可以看出:成员的可访问域包含的范围决不会比声明该成员的类型的可访问域更广。

直观地讲,当访问类型或成员 M 时,按下列步骤进行计算以确保允许进行访问:

  • 首先,如果
    M 是在某个类型(相对于编译单元或命名空间)内声明的,则当该类型不可访问时将会发生编译时错误。
  • 然后,如果
    M 为 public,则允许进行访问。
  • 否则,如果
    M 为 protected internal,则当访问发生在声明了 M 的程序中,或发生在从声明 M 的类派生的类中并通过派生类类型(第 3.5.3 节)进行访问时,允许进行访问。
  • 否则,如果
    M 为 protected,则当访问发生在声明了 M 的类中,或发生在从声明 M 的类派生的类中并通过派生类类型(第 3.5.3 节)进行访问时,允许进行访问。
  • 否则,如果
    M 为 internal,则当访问发生在声明了 M 的程序中时允许进行访问。
  • 否则,如果
    M 为 private,则当访问发生在声明了 M 的类型中时允许进行访问。
  • 否则,类型或成员不可访问,并发生编译时错误。

在下面的示例中

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;
}
}

类和成员具有下列可访问域:

  • A 和 A.X 的可访问域无限制。
  • A.Y、B、B.X、B.Y、B.C、B.C.X 和 B.C.Y 的可访问域是包含程序的程序文本。
  • A.Z 的可访问域是 A 的程序文本。
  • B.Z 和 B.D 的可访问域是 B 的程序文本,包括 B.C 和 B.D 的程序文本。
  • B.C.Z 的可访问域是 B.C 的程序文本。
  • B.D.X 和 B.D.Y 的可访问域是 B 的程序文本,包括 B.C 和 B.D 的程序文本。
  • B.D.Z 的可访问域是 B.D 的程序文本。

如示例所示,成员的可访问域绝不会大于包含它的类型的可访问域。例如,即使所有的 X 成员都具有公共级的已声明可访问性,但除了 A.X 外,所有其他成员的可访问域都受包含类型的约束。

如第 3.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。因为该成员是私有的,所以只能在 A 的 class-body 中对它进行访问。因此,对 b.x 的访问在 A.F 方法中取得了成功,在 B.F 方法中却失败了。

1.5.3 实例成员的受保护访问

当在声明了某个 protected 实例成员的类的程序文本之外访问该实例成员时,以及当在声明了某个 protected internal 实例成员的程序的程序文本之外访问该实例成员时,这种访问必须发生在声明了该成员的类的一个派生类的类声明中。而且,要求这种访问通过该成员所属类的派生类类型的实例或从它构造的类类型的实例发生。此限制阻止一个派生类访问其他派生类的受保护成员,即使成员继承自同一个基类也是如此。

假定 B 是一个基类,它声明了一个受保护的实例成员 M,而 D 是从 B 派生的类。在 D 的 class-body 中,对 M 的访问可采取下列形式之一:

  • M 形式的非限定 type-name 或 primary-expression。
  • E.M 形式的 primary-expression,假定
    E 的类型是 T 或从 T 派生的类,其中 T 为类类型 D 或从 D 构造的类类型
  • base.M 形式的 primary-expression。

除了上述访问形式外,派生类还可以在 constructor-initializer 中(第 10.11.1 节)访问基类的受保护的实例构造函数。

在下面的示例中

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 中可以通过 A 和 B 的实例访问 x,这是因为在两种情况下访问都通过 A 的实例或从 A 派生的类发生。但是在 B 中,由于 A 不从 B 派生,所以不可能通过 A 的实例访问 x。

在下面的示例中

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 的三个赋值是允许的,因为它们全都通过从该泛型类型构造的类类型的实例进行。

1.5.4 可访问性约束

C# 语言中的有些构造要求某个类型至少与某个成员或其他类型具有同样的可访问性 (at least as accessible as)。如果 T 的可访问域是 M 可访问域的超集,我们就说类型 T 至少与成员或类型 M 具有同样的可访问性。换言之,如果 T 在可访问 M 的所有上下文中都是可访问的,则 T 至少与 M 具有同样的可访问性。

存在下列可访问性约束:

  • 类类型的直接基类必须至少与类类型本身具有同样的可访问性。
  • 接口类型的显式基接口必须至少与接口类型本身具有同样的可访问性。
  • 委托类型的返回类型和参数类型必须至少与委托类型本身具有同样的可访问性。
  • 常量的类型必须至少与常量本身具有同样的可访问性。
  • 字段的类型必须至少与字段本身具有同样的可访问性。
  • 方法的返回类型和参数类型必须至少与方法本身具有同样的可访问性。
  • 属性的类型必须至少与属性本身具有同样的可访问性。
  • 事件的类型必须至少与事件本身具有同样的可访问性。
  • 索引器的类型和参数类型必须至少与索引器本身具有同样的可访问性。
  • 运算符的返回类型和参数类型必须至少与运算符本身具有同样的可访问性。
  • 实例构造函数的参数类型必须至少与实例构造函数本身具有同样的可访问性。

在下面的示例中

class A {...}

public class
B: A {...}

B 类导致编译时错误,因为 A 并不具有至少与 B 相同的可访问性。

同样,在示例中

class A {...}

public class
B
{
A F() {...}

internal A G() {...}

public A H() {...}
}

B 中的方法
H 导致编译时错误,因为返回类型 A 并不具有至少与该方法相同的可访问性。

1.6 签名和重载

方法、实例构造函数、索引器和运算符是由它们的签名 (signature) 来刻画的:

  • 方法签名由方法的名称、类型形参的个数和它的每一个形参的类型和形参传递模式(值、引用或输出)组成(按从左到右的顺序)。为了实现这些目的,形参的类型中出现的方法的任何类型形参都不是由其名称标识的,而是由它在方法的类型实参列表中的序号位置标识的。需注意的是,方法签名既不包含返回类型和 params 修饰符(它可用于指定最右边的形参),也不包含可选类型形参约束。
  • 实例构造函数签名由它的每一个形参的类型和形参传递模式(值、引用或输出)组成(按从左到右的顺序)。具体而言,实例构造函数的签名不包含可为最右边的参数指定的 params 修饰符。
  • 索引器签名由它的每一个形参的类型组成(按从左到右的顺序)。具体而言,索引器签名既不包含元素类型,也不包含可为最右边的形参指定的 params 形参数组签名和 修饰符。
  • 运算符签名由运算符的名称和它的每一个形参的类型组成(按从左到右的顺序)。具体而言,运算符的签名不包含结果类型。

如果同一成员类型的两个签名具有相同的名称、类型形参的个数和形参传递模式,并且其对应的类型之间存在标识转换,则会将这两个签名视为同一签名(第 6.1.1 节)。

签名是对类、结构和接口的成员实施重载 (overloading) 的机制:

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

虽然 out 和 ref 参数修饰符被视为签名的一部分,但是在同一个类型中声明的成员不能仅通过 ref 和 out 在签名上加以区分。在同一类型中声明了两个成员时,如果将这两个方法中带有 out 修饰符的所有形参更改为 ref 修饰符会使这两个成员的签名相同,则会发生编译时错误。出于签名匹配的其他目的(如隐藏或重写),ref 和 out 被视为签名的组成部分,并且互不匹配。(此限制使 C# 程序能够方便地进行转换,以便能在公共语言基础结构 (CLI) 上运行,CLI 并未提供任何方式来定义仅通过 ref 和 out 就能加以区分的方法。)

下面的示例演示一组重载方法声明及其签名。

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(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
}

请注意,任何 ref 和 out 参数修饰符(第 10.6.1 节)都是签名的一部分。因此,F(int) 和 F(ref int) 是唯一的签名。但是,F(ref int) 和 F(out int) 不能在同一个接口中声明,因为它们的签名仅 ref 和 out 不同。此外,返回类型和 params
修饰符不是签名的组成部分,所以不可能仅基于返回类型或是否存在 params 修饰符来实施重载。因此,上面列出的关于方法 F(int) 和 F(params string[]) 的声明会导致编译时错误。

1.7 范围

名称的范围 (scope)
是一个程序文本区域,在其中可以引用由该名称声明的实体,而不对该名称加以限定。范围可以嵌套 (nested),并且内部范围可以重新声明外部范围中的名称的含义(但这并不会取消第 3.3 节强加的限制,即在嵌套块中不可能声明与它的封闭块中的局部变量同名的局部变量)。因此,我们就说,外部范围中的这个同名的名称在由内部范围覆盖的程序文本区域中是隐藏的 (hidden),对外部名称只能通过它的限定名才能从内部范围来访问。

  • 由 namespace-member-declaration(第 9.5 节)所声明的命名空间成员的范围,如果没有其他封闭它的
    namespace-declaration,则它的范围是整个程序文本。
  • namespace-declaration 中 namespace-member-declaration 所声明的命名空间成员的范围是这样定义的,如果该命名空间成员声明的完全限定名为 N,则其声明的命名空间成员的范围是,完全限定名为 N 或以 N 开头后跟句点的每个 namespace-declaration 的 namespace-body。
  • extern-alias-directive  范围定义的名称范围扩展到直接包含其的编译单元或命名空间体的 using-directives、global-attributes 和 namespace-member-declarations。extern-alias-directive 不会把任何新成员提供给基础声明空间。换言之,extern-alias-directive 不具传递性,它仅影响它在其中出现的编译单元或命名空间体。
  • 由 using-directive(第 9.4 节)定义或导入的名称的范围扩展到出现 using-directive 的 compilation-unit 或 namespace-body 内的整个 namespace-member-declarations 中。using-directive 可以使零个或更多个命名空间或者类型名称在特定的 compilation-unit 或 namespace-body 中可用,但不会把任何新成员提供给基础声明空间。换言之,using-directive 不具传递性,它仅影响它在其中出现的 compilation-unit 或 namespace-body。
  • 由  class-declaration(第 10.1 节)的 type-parameter-list 声明的类型形参的范围是该  class-declaration 的  class-base、type-parameter-constraints-clauses 和 class-body。
  • 由  struct-declaration(第 11.1 节)的 type-parameter-list 声明的类型形参的范围是该  struct-declaration 的  struct-interfaces、 type-parameter-constraints-clauses 和 struct-body。
  • 由  interface-declaration(第 13.1 节)的 type-parameter-list 声明的类型形参的范围是该
    interface-declaration 的  interface-base、type-parameter-constraints-clauses 和 interface-body。
  • 由  delegate-declaration(第 15.1 节)的 type-parameter-list 声明的类型形参的范围是该  delegate-declaration 的  return-type、formal-parameter-list 和 type-parameter-constraints-clauses。
  • 由 class-member-declaration(第 10.1.6 节)所声明的成员范围是该声明所在的那个 class-body。此外,类成员的范围扩展到该成员的可访问域(第 3.5.2 节)中包含的那些派生类的 class-body。
  • 由 struct-member-declaration(第 11.2 节)声明的成员范围是该声明所在的 struct-body。
  • 由 enum-member-declaration (第 14.3 节)声明的成员范围是该声明所在的 enum-body。
  • 在 method-declaration(第 10.6 节)中声明的形参范围是该 method-declaration 的 method-body。
  • 在 indexer-declaration(第 10.9 节)中声明的形参范围是该 indexer-declaration 的 accessor-declarations。
  • 在 operator-declaration(第 10.10 节)中声明的形参范围是该 operator-declaration 的 block。
  • 在 constructor-declaration(第 10.11 节)中声明的形参范围是该 constructor-declaration 的 constructor-initializer 和 block。
  • 在 lambda-expression(第 7.15 节)中声明的形参范围是该 lambda-expression 的 lambda-expression-body。
  • 在 anonymous-method-expression(第 7.15 节)中声明的形参范围为该 anonymous-method-expression 的 block。
  • 在 labeled-statement(第 8.4 节)中声明的标签范围是该声明所在的 block。
  • 在 local-variable-declaration(第 8.5.1 节)中声明的局部变量范围是该声明所在的块。
  • 在 switch
    语句(第 8.7.2 节)的 switch-block 中声明的局部变量范围是该 switch-block。
  • 在 for 语句(第 8.8.3 节)的 for-initializer 中声明的局部变量范围是该 for 语句的 for-initializer、for-condition、for-iterator 以及所包含的 statement。
  • 在 local-constant-declaration(第 8.5.2 节)中声明的局部常量范围是该声明所在的块。在某局部常量 constant-declarator 之前的文本位置中引用该局部常量是编译时错误。
  • 作为 foreach-statement、using-statement、lock-statement 或 query-expression 一部分声明的变量的范围由给定构造的扩展确定。

在命名空间、类、结构或枚举成员的范围内,可以在位于该成员的声明之前的文本位置引用该成员。例如

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

int i = 0;
}

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

在局部变量的范围内,在位于该局部变量的 local-variable-declarator 之前的文本位置引用该局部变量是编译时错误。例如

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 赋值时,并未引用在外部范围声明的字段。相反,它所引用的是局部变量 i,这会导致编译时错误,因为它在文本上位于该变量的声明之前。在方法 G 中,在 j 的声明初始值设定项中使用 j 是有效的,因为并未在 local-variable-declarator 之前使用。在方法 H 中,后面的 local-variable-declarator 正确引用在同一 local-variable-declaration 内的前面的 local-variable-declarator 中声明的局部变量。

局部变量的范围规则旨在保证表达式上下文中使用的名称的含义在块中总是相同。如果局部变量的范围仅从它的声明扩展到块的结尾,则在上面的示例中,第一次赋值将会分配给实例变量,第二次赋值将会分配给局部变量,如果后来重新排列块的语句,则可能会导致编译时错误。

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

using System;

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。

1.7.1 名称隐藏

实体的范围通常比该实体的声明空间包含更多的程序文本。具体而言,实体的范围可能包含一些声明,它们会引入一些新的声明空间,其中可能含有与该实体同名的新实体。这类声明导致原始的实体变为隐藏的 (hidden)。相反,当实体不是隐藏的时,就说它是可见的 (visible)。

当范围之间相重叠(或通过嵌套重叠,或通过继承重叠)时会发生名称隐藏。以下各节介绍这两种隐藏类型的特性。

1.7.1.1 通过嵌套隐藏

以下各项活动会导致发生通过嵌套的名称隐藏:在命名空间内嵌套其他命名空间或类型;在类或结构中的嵌套类型;声明形参和局部变量。

在下面的示例中

class A
{
int i = 0;

void F() {
     int i = 1;
}

void G() {
     i = 1;
}
}

在方法 F 中,实例变量 i 被局部变量 i 隐藏,但在方法 G 中,i 仍引用该实例变量。

当内部范围中的名称隐藏外部范围中的名称时,它隐藏该名称的所有重载匹配项。在下面的示例中

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

static void F(string s) {}

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

static void F(long l) {}
}
}

由于 F 的所有外部匹配项都被内部声明隐藏,因此调用 F(1) 将调用在 Inner 中声明的 F。由于同样的原因,调用 F("Hello") 将导致编译时错误。

1.7.1.2 通过继承隐藏

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

  • 类或结构中引入的常量、字段、属性、事件或类型会把所有同名的基类成员隐藏起来。
  • 类或结构中引入的方法将隐藏所有同名的非方法基类成员以及所有具有相同签名的基类方法(第 3.6 节)。
  • 类或结构中引入的索引器将隐藏具有相同签名的所有基类索引器(第 3.6 节)。

管理运算符声明(第 10.10 节)的规则使派生类不可能声明与基类中的运算符具有相同签名的运算符。因此,运算符从不相互隐藏。

与隐藏外部范围中的名称相反,隐藏继承范围中的可访问名称会导致发出警告。在下面的示例中

class Base
{
public void F() {}
}

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

在 Derived
中声明 F 会导致报告一个警告。准确地说,隐藏继承的名称不是一个错误,因为这会限制基类按自身情况进行改进。例如,由于更高版本的 Base 引入了该类的早期版本中不存在的 F 方法,可能会发生上述情况。如果上述情况是一个错误,当基类属于一个单独进行版本控制的类库时,对该基类的任何 更改都有可能导致它的派生类变得无效。

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

class Base
{
public void F() {}
}

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

new 修饰符指示
Derived 中的 F 是“新的”,并且实际上是有意隐藏继承的成员。

在声明一个新成员时,仅在该新成员的范围内隐藏被继承的成员。

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

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

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

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

1.8 命名空间和类型名称

C# 程序中的若干上下文要求指定 namespace-name  或 type-name。

namespace-name:
namespace-or-type-name

type-name:
namespace-or-type-name

namespace-or-type-name:
identifier   type-argument-listopt
namespace-or-type-name   .   identifier   type-argument-listopt
qualified-alias-member

namespace-name 是引用一个命名空间的 namespace-or-type-name。根据如下所述的解析过程,namespace-name 的 namespace-or-type-name 必须引用一个命名空间,否则将发生编译时错误。namespace-name 中不能存在任何类型实参(第 4.4.1 节),只有类型才能具有类型实参。

type-name 是引用一个类型的 namespace-or-type-name。根据如下所述的解析过程,type-name 的 namespace-or-type-name 必须引用一个类型,否则将发生编译时错误。

如果 namespace-or-type-name 是 qualified-alias-member,则其含义如第 9.7 节中所述。否则,namespace-or-type-name 具有下列四种形式之一:

  • I
  • I<A1, ...,AK>
  • N.I
  • N.I<A1, ...,AK>

其中 I 是单个标识符,N 是 namespace-or-type-name,<A1, ..., AK> 是可选的 type-argument-list。如果未指定 type-argument-list 时,则可将 K 视为零。

namespace-or-type-name 的含义按下述步骤确定:

  • 如果 namespace-or-type-name 的形式为 I 或 I<A1, ...,AK>:
  • 如果 K 为零,namespace-or-type-name 出现在泛型方法声明(第 10.6 节)中,且该声明包含名为 I 的类型形参(第 10.1.3 节),则 namespace-or-type-name 引用该类型形参。
  • 否则,如果
    namespace-or-type-name 出现在类型声明中,则对于每个实例类型 T(第 10.3.1 节),从该类型声明的实例类型开始,对每个封闭类或结构声明(如果有)的实例类型继续如下过程:
  • 如果 K 为零,并且 T 的声明包含名为 I 的类型形参,则 namespace-or-type-name 引用该类型形参。
  • 否则,如果
    namespace-or-type-name 出现在该类型声明的体中,且 T 或其任一基类型包含具有名称 I 和 K 个类型形参的嵌套可访问类型,则 namespace-or-type-name 引用利用给定类型实参构造的该类型。如果存在多个这样的类型,则选择在派生程度较大的类型中声明的类型。请注意,在确定 namespace-or-type-name 的含义时,将忽略非类型成员(常量、字段、方法、属性、索引器、运算符、实例构造函数、析构函数和静态构造函数)和具有不同数目的类型形参的类型成员。
  • 如果当时前面的步骤不成功,则对于每个命名空间 N,从出现 namespace-or-type-name 的命名空间开始,继续到每个封闭命名空间(如果有)且到全局命名空间结束,对下列步骤进行计算直到找到实体:
  • 如果 K 为零,并且 I 为 N 中的命名空间的名称,则:
  • 如果出现
    namespace-or-type-name 的位置包含在 N 的命名空间声明中,并且该命名空间声明包含将名称 I 与某个命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 namespace-or-type-name 是不明确的,并将发生编译时错误。
  • 否则,namespace-or-type-name 引用 N 中名为 I 的命名空间。
  • 否则,如果
    N 包含一个具有名称 I 且有 K 个类型形参的可访问类型,则:
  • 如果 K 为零,并且出现 namespace-or-type-name 的位置包含在 N 的命名空间声明中,并且该命名空间声明包含将名称 I 与某个命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 namespace-or-type-name 是不明确的,并将发生编译时错误。
  • 否则,namespace-or-type-name 引用利用给定类型实参构造的该类型。
  • 否则,如果出现 namespace-or-type-name 的位置包含在 N 的命名空间声明中:
  • 如果 K 为零,并且该命名空间声明包含一个将名称 I 与一个导入的命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 namespace-or-type-name 引用该命名空间或类型。
  • 否则,如果该命名空间声明的 using-namespace-directive 导入的命名空间恰好包含一个具有名称 I 且有 K 个类型形参的类型,则 namespace-or-type-name 引用利用给定类型实参构造的该类型。
  • 否则,如果该命名空间声明的 using-namespace-directives 导入的命名空间包含多个具有名称 I 且有 K 个类型形参的类型,则 namespace-or-type-name 是不明确的,并将导致发生错误。
  • 否则,namespace-or-type-name 未定义,并将导致发生编译时错误。
  • 否则,namespace-or-type-name 的形式为 N.I 或 N.I<A1, ...,AK>.N 首先解析为 namespace-or-type-name。如果对 N 的解析不成功,则发生编译时错误。否则,N.I 或 N.I<A1, ..., AK> 按如下方式进行解析:
  • 如果 K 为零,N 引用一个命名空间,并且 N 包含名为 I 的嵌套命名空间,则 namespace-or-type-name 引用该嵌套命名空间。
  • 否则,如果
    N 引用一个命名空间,并且 N 包含一个具有名称 I 且有 K 个类型形参的可访问类型,则 namespace-or-type-name 引用利用给定类型实参构造的该类型。
  • 否则,如果
    N 引用一个(可能是构造的)类或结构类型,并且 N 或其任一基类包含一个具有名称 I 且有 K 个类型形参的嵌套可访问类型,则 namespace-or-type-name 引用利用给定类型实参构造的该类型。如果存在多个这样的类型,则选择在派生程度较大的类型中声明的类型。请注意,如果要将 N.I的含义确定为解析 N 的基类指定的一部分,则将 N 的直接基类视为对象(第 10.1.4.1 节)。
  • 否则,N.I 是无效的  namespace-or-type-name 并将发生编译时错误。

仅当下列条件成立时才允许 namespace-or-type-name 引用静态类(第 10.1.1.3 节)

  • namespace-or-type-name 是 T.I 形式的 namespace-or-type-name 中的 T,或者
  • namespace-or-type-name 是 typeof(T) 形式的
    typeof-expression(第 7.5.11 节)中的 T。

1.8.1 完全限定名

每个命名空间和类型都具有一个完全限定名 (fully qualified name),该名称在所有其他命名空间或类型中唯一标识该命名空间或类型。命名空间或类型 N 的完全限定名按下面这样确定:

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

换言之,N 的完全限定名是从全局命名空间开始通向 N 的标识符的完整分层路径。由于命名空间或类型的每个成员都必须具有唯一的名称,因此,如果将这些成员名称置于命名空间或类型的完全限定名之后,这样构成的成员完全限定名一定符合唯一性。

下面的示例演示了若干命名空间和类型声明及其关联的完全限定名。

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
}

1.9 自动内存管理

C# 使用自动内存管理,它使开发人员不再需要以手动方式分配和释放对象占用的内存。自动内存管理策略由垃圾回收器 (garbage collector) 实现。一个对象的内存管理生存周期如下所示:

  1. 当创建对象时,为其分配内存,运行构造函数,将该对象被视为活对象。
  2. 在后续执行过程中,如果不会再访问该对象或它的任何部分(除了运行它的析构函数),则将该对象视为不再使用,可以销毁。C# 编译器和垃圾回收器可以通过分析代码,确定哪些对象引用可能在将来被使用。例如,如果范围内的某个局部变量是现有的关于此对象的唯一引用,但在当前执行点之后的任何后续执行过程中,该局部变量都不会再被引用,那么垃圾回收器可以(但不是必须)认为该对象不再被使用。
  3. 一旦对象符合销毁条件,在稍后某个时间将运行该对象的析构函数(第 10.13 节)(如果有)。在普通情况下,对象的析构函数仅运行一次,但特定于实现的 API 可允许忽略此行为。
  4. 一旦运行对象的析构函数,如果该对象或它的任何部分无法由任何可能的执行继续(包括运行析构函数)访问,则该对象被视为不可访问,可以回收。
  5. 最后,在对象变得符合回收条件后,垃圾回收器将释放与该对象关联的内存。

垃圾回收器维护对象的使用信息,并利用此信息做出内存管理决定,如在内存中的何处安排一个新创建的对象、何时重定位对象以及对象何时不再被使用或不可访问。

与其他假定存在垃圾回收器的语言一样,C# 也旨在使垃圾回收器可以实现广泛的内存管理策略。例如,C# 并不要求一定要运行析构函数,不要求对象一符合条件就被回收,也不要求析构函数以任何特定的顺序或在任何特定的线程上运行。

垃圾回收器的行为在某种程度上可通过类 System.GC 的静态方法来控制。该类可用于请求执行一次回收操作、运行(或不运行)析构函数,等等。

由于垃圾回收器在决定何时回收对象和运行析构函数方面可以有很大的选择范围,它的一个符合条件的实现所产生的输出可能与下面的代码所显示的不同。程序

using System;

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

class B
{
object Ref;

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

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

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

创建类 A 的一个实例和类 B 的一个实例。当给变量 b 赋值 null 后,这些对象变得符合垃圾回收条件,这是因为从此往后,任何用户编写的代码不可能再访问这些对象。输出可以为

Destruct instance of A
Destruct instance of B

Destruct instance of B
Destruct instance of A

这是因为该语言对于对象的垃圾回收顺序没有强加约束。

在一些微妙的情况下,“符合销毁条件”和“符合回收条件”之间的区别会很重要。例如,

using System;

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

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

class B
{
public A Ref;

~B() {
     Console.WriteLine("Destruct
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 destruction
     GC.Collect();
     GC.WaitForPendingFinalizers();

//
B now eligible for collection, but A is not
     if (RefA != null)
        Console.WriteLine("RefA is
not null");
}
}

在上面的程序中,如果垃圾回收器选择在 B 的析构函数之前运行 A 的析构函数,则该程序的输出可能是:

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

请注意,虽然 A 的实例没有使用,并且 A 的析构函数已被运行过了,但仍可能从其他析构函数调用 A 的方法(此例中是指 F)。还请注意,运行析构函数可能导致对象再次从主干程序中变得可用。在此例中,运行 B 的析构函数导致了先前没有被使用的 A 的实例变得可从当前有效的引用 Test.RefA 访问。调用 WaitForPendingFinalizers 后,B 的实例符合回收条件,但由于引用 Test.RefA 的缘故,A 的实例不符合回收条件。

为了避免混淆和意外的行为,好的做法通常是让析构函数只对存储在它们对象本身字段中的数据执行清理,而不对它所引用的其他对象或静态字段执行任何操作。

另一种使用析构函数的方法是允许类实现 System.IDisposable 接口。这样的话,对象的客户端就可以确定何时释放该对象的资源,通常是通过在 using 语句(第 8.13 节)中以资源形式访问该对象。

1.10 执行顺序

C# 程序执行时,在临界执行点保留每个执行线程的副作用。副作用 (side
effect) 副作用 定义为对可变字段的读取或写入、对非可变变量的写入、对外部资源的写入以及异常的引发。临界执行点(这些副作用的顺序必须保存在其中)是指下列各活动:引用一些可变字段(第 10.5.3 节);引用 lock 语句(第 8.12 节);引用线程的创建与终止。执行环境可以随便更改 C# 程序的执行顺序,但受下列约束限制:

  • 在执行线程中需保持数据依赖性。就是说,在计算每个变量的值时,就好像线程中的所有语句都是按原始程序顺序执行的。
  • 保留初始化的排序规则(第 10.5.4 节和第 10.5.5 节)。
  • 对于不稳定读写(第 10.5.3 节),副作用的顺序需保持不变。此外,执行环境甚至可以不需要计算一个表达式的各个部分,如果它能推断出表达式的值是“不会被使用的”而且不会产生有效的副作用(包括由调用方法或访问不稳定字段导致的任何副作用)。当程序执行被异步事件(例如其他线程引发的异常)中断时,它不保证可观察到的副作用以原有的程序顺序出现。