C# Language Specification 5.0 第三章 基本概念

C# Language Specification 5.0 第三章 基本概念_第1张图片

应用程序启动

拥有进入点(entry point)的程序集称应用程序(application)。当运行一应用程序时,将创建一新应用程序域(application domain)。同一个应用程序可在同一台机器(machine)上同时运行多个实例,并且每个实例都有自己的应用程序域。

应用程序域作为应用程序状态(application state)之容器(container),使应用程序相互隔离(isolation)。应用程序域是定义于应用及所用类库的类型之容器与边界。加载入不同的应用程序域的同一类型是相互泾渭分明的,而其实例化的对象也不会在应用程序域之间直接共享。比方说,对于这些类型的静态变量,每个应用程序域都自有一份其副本,同时这些类型的静态构造函数在应用程序域中至多运行一次。实现可以自由为应用程序域的创建和销毁提供指定实现(implementation-specific)策略或机制。

应用程序启动(Application startup)时,执行环境会调用一个特指的方法作为应用程序的进入点(entry point)。入口点方法一贯称为 Main,且可为下列签名中的一种:

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

如上所示,进入点可以选择 int 为其返回值。这个返回值通常被用在应用程序终止(application termination,第三章第二节)时。

进入点有个可选形参。这个参数可以用任何名称,但它的类型必须是 string[]。如果出现形参,那么当应用程序启动时,执行环境将通过命令行参数(command-line arguments)的方式创建并传递指定的 string[] 实参。实参 string[] 永不为空(null),但它可能长度为零(如果命令行没有指定实参的话)。

由于 C# 支持方法重构(overload),类或结构可以包含一个方法的多个定义,前提是每个重载版本都有不同的方法签名。然而,在一个程序内,类或结构内不能同时存在多个叫做 Main 的方法,因为 Main 被限定为只能作为应用程序的入口点。如果提供超过一个参数或者唯一参数类型不是 string[],则允许重载多个 Main 版本。

应用程序可由多个类与结构组成,也许会有多个 Main 方法在这些类或结构内,由于 Main 的定义限定它只能是应用程序入口点,所以在这种情况下,外部机制(诸如命令行编译器选项)必须从中选择其一作为应用程序的进入点。

在 C# 中每一个方法都必须定义为类或结构的成员,一般情况下方法的声明可访问性(declared accessibility,第三章第 5.1 节)是由访问控制修饰符(access modifiers,第十章第 3.5 节)所决定的;同样,类型的声明声明可访问性也是由其访问控制修饰符所决定的。为了调用所给定类型的给定方法,类型与成员都必须可被访问(accessible),然应用程序进入点是特例。不管应用程序进入点的声明可访问性及其闭包类型的声明可访问性,执行环境总能访问到它们。

应用程序进入点方法(entry point method)不能放入泛型类声明内。

在其它各方面,进入点方法与非进入点方法的行为类似。

应用程序终止

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

应用程序进入点方法的返回类型是 int,该值作为应用程序终止状态代码(application's termination status code)返回。该代码的目的是允许与执行环境通讯以告知成功或失败。

如果进入点方法的返回类型是 void,遇到该方法的关门大括号 } 或执行到一个没有表达式的返回语句(return statement),则终止状态代码为 0。

先于应用程序的终止,未被垃圾回收的对象的析构函数将被调用,除非这类清理工作已被取消(suppressed)(比方说通过调用库方法GC.SuppressFinalize)。

声明

C# 程序中的声明定义了程序的组成元素。C# 程序有系统地使用了命名空间(namespace,第九章),命名空间可以包含类型声明(type declarations)嵌套命名空间声明(nested namespace declarations)。类型声明(type declarations,第九章第六节)用于定义类(class,第十章)、结构(struct,第十章第十四节)、接口(interface,第十三章)、枚举(enum,第十四章)以及委托(delegates,第十五章)。类型声明中允许的成员种类取决于类型声明的形式。比方说类声明可以包含声明常量(constants,第十章第四节)、字段(fields,第十章第五节)、方法(methods,第十章第六节)、属性(properties,第十章第七节)、事件(events,第十章第八节)、索引器(indexers,第十章第九节)、操作符(operators,第十章第十节)、实例构造函数(instance constructors,第十章第十一节)、静态构造函数(static constructors,第十章第十二节)、析构函数(destructors,第十章第十三节)和嵌套类型(nested types,第十章第 3.8 节)。

声明在其所属声明空间(declaration space)内定义一个名称。除重载成员(第三章第六节)外,同一声明空间(declaration space)内出现两个以上同名成员会引发「编译时错误(compile-time error)」。同一声明空不可同时包含不同类型的同名方法。比方说,声明空间绝无包含同名字段与方法之可能。

有数种不同的声明空间,其具体描述如下:

  • 在程序的所有源文件内,未闭包于 namespace-declaration 内的 namespace-member-declarations 是属于一个叫做全局声明空间(global declaration space)的组合声明空间内的成员;
  • 在程序的所有源文件内,位于 namespace-declarations 内的 namespace-member-declarations 若拥有相同的完全限定命名空间名,则其属于同一个组合声明空间内;
  • 每个类、结构或接口的声明都将创架一个新的声明空间。名称将通过 class-member-declarationsstruct-member-declarationsinterface-member-declarationstype-parameters 引入此声明空间。除了重载实例构造函数和静态构造函数,类或结构不允许声明包含一个与其类名或结构名同名的成员。类、结构或结构允许重载声明方法和索引器。此外类或结构允许重载声明实例构造函数与操作符。比方说类、结构或接口可以包含多个声明为同名的方法,这些所声明的方法拥有不同的签名(signature,第三章第六节)。注意,基类并不在该类的声明空间内,基接口也不在该接口的声明空间内,所以派生类或接口允许声明一个与继承成员同名的成员。这类成员隐藏(hide)了它们继续的那些成员;
  • 每一个委托声明都会创建一个新的声明空间。名称通过形参(formal parameters)fixed-parametersparameter-arrays 以及 type-parameters 引入这个声明空间;
  • 每个枚举声明都会创建一个新的声明空间。名称通过 enum-member-declarations 引入这个声明空间;
  • 每一个方法声明(method declaration)、索引器声明(indexer declaration)、操作符声明(operator declaration)、实例构造函数声明(instance constructor declaration)和匿名函数(anonymous function)都会创建一个名曰局部变量声明空间(local variable declaration space)的新声明空间。名称通过形参 fixed-parametersparameter-arrays 以及 type-parameters 引入这个声明空间。如果有函数成员或匿名函数成员的函数体,则将会被是为嵌套于局部变量声明空间内。局部变量声明控件和嵌套局部变量声明空间内包含同名元素将引发错误。因此在一个闭包声明空间内,如果包含嵌套声明空间的话,那么不能在其内部声明一个与闭包声明空间内变量或常量同名的局部变量或常量。只有一种可能使得两个声明空间内包含同名元素,那就是这两个声明空间彼此不相互包含。
  • 每个 blockswitch-block,以及 forforeachusing 语句将为局部变量和局部常量创建局部变量声明空间。名称将通过 local-variable-declarationslocal-constant-declarations 引入这个声明空间。注意,作为函数成员主体或匿名函数主体的块,或是位于函数成员主体或匿名函数主体内的块,将嵌套在本地变量声明空间内(这个本地变量声明空间是为其参数所声明的)。因此,如果某个方法的局部变量和参数同名,那么将引发一个错误。
  • 每个 blockswitch-block 为每个标签(label)创建相互隔离的声明空间。名称将通过 labeled-statements 引入该声明空间,并通过 goto-statements 来引用。块的标签声明空间(label declaration space)可包含任何嵌套块。因此在嵌套块内不能声明一个与其所在闭包块(enclosing block)标签同名的标签。

声明名称的文本顺序一般而言是不重要的(no significance)。具体而言,声明并使用命名空间、常量、方法、属性、事件、索引器、操作符、实例构造函数、析构函数、静态构造函数以及类型,其文本顺序并不重要。在以下情况下声明顺序比较重要:

  • 字段声明和局部变量声明的声明顺序意味着其初始化器(如果有的话)的执行顺序;
  • 局部变量必须在使用前定义(第三章第七节);
  • when constant-expression 被省略时,枚举成员声明(第十四章第三节)的顺序是重要的。

命名空间的声明空间是开放的(open ended),两个相同完全限定名(fully qualified name)的命名空间拥有同一个声明空间。比方说:

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

这两个命名空间位于同一个声明空间内,在此例中两个类的完全限定名分别是 Megacorp.Data.CustomerMegacorp.Data.Order。因为这两个声明位于同一个声明空间内,所以如果这两个类的完全限定名是一样的话,会引发一个「编译时错误」。

如上文所指定,块(block)的声明空间可以嵌套任意块。因此,在下例中,F 方法和 G 方法会引发「编译时错误」,因为外部块中声明了 i,那么内部块就不能重复声明(redeclared)。然而 H 方法和 I 方法是合法的,这是由于两个 i 的声明都位于相互独立的非嵌套块(separate non-nested blocks)内。

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

成员

命名空间和类型都拥有成员。在实体开始被引用时,实体成员一般都可通过限定名称(qualified name)引入其中,通过标记(token). 引出成员的名字。

类型的成员既可在类型声明中声明,也可从其基类中继承。当一个类型继承自其基类时,所有基类成员(除了实例构造函数、析构函数以及静态构造函数)都将成为派生类型的成员。基类成员的声明可访问性并不控制成员是否可被继承——继承可拓展到除实例构造函数、析构函数和静态构造函数之外的任意成员。然而。也有可能派生类型无法访问到所继承的成员,比方说因为其声明可访问性(第三章第 5.1 节)或是因为其通过类型自身声明隐藏(第三章第 7.1.2 节)。

命名空间成员

如果命名空间与类型没有闭包于一个命名空间,则它们将是全局命名空间(global namespace)的成员。这相当于名字直接在全局声明空间内声明了。

如果命名空间与类型在一个命名空间内,那么命名空间和类型将是这个外部命名空间的成员。这意味着名字直接在这个命名空间的声明空间内声明了。

命名空间没有访问限制(access restrictions)。不可以为命名空间声明为 private、 protected 或 internal,命名空间永远是可公开取得的(publicly accessible)。

结构成员

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

简单类型的成员通过类型别名(alias)直接对应结构类型的成员:

  • 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 结构的成员。

枚举成员

枚举内的成员是枚举声明的常量以及直接继承自枚举基类 System.Enum 与间接继承自基类 System.ValueTypeobject 的成员。

类成员

在一个类中声明的成员与继承自基类的成员都是这个类的成员(除了没有基类的 object 类)。继承自基类的成员包括常量、字段、方法、属性、事件、索引器、操作符以及基类类型,但不包括基类的实例构造函数、析构函数和静态构造函数。基类成员的继承并不关心它们的可访问性。

类声明可以包含常量、字段、方法、属性、事件、索引器、操作符、实例构造函数、析构函数、静态构造函数和类型。

objectstring 的成员通过别名直接对应它们的类型:

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

接口成员

接口成员声明于接口及其所有基接口内。严格来讲,类 object 内的成员不是任何接口的成员(见第十三章第二节),但通过接口类型成员查找到类 object 的成员(第七章第四节)。

数组成员

数组成员继承自类 System.Array

委托成员

委托成员继承自类 System.Delegate

成员访问

成员声明可用于控制对其的访问。成员可访问性由该成员的声明的可访问性(declared accessibility,第三章第 5.1 节)以及包含该成员的类型的可访问性(如果存在的话)来确定的。

当允许访问指定成员时,我们称该成员是可访问的(accessible)。相反,当不允许访问指定成员时,我们称该成员是不可访问的(inaccessible)

当引发访问的文本位于成员的可访问域(accessibility domain,第三章第 5.2 节)内,则该成员允许被访问。

声明的可访问性

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

  • Public,通过在成员声明时使用 public 修饰符来选择之,其直观含义为「不受限制的访问(access not limited)」;
  • Protected,通过在成员声明时使用 protected 修饰符来选择之,其直观含义为「仅限该类及其派生类型内可访问(access limited to the containing class or types derived from the containing class)」;
  • Internal,通过在成员声明时使用 internal 修饰符来选择之,其直观含义为「仅限本程序内可访问(access limited to this program)」;
  • Protected internal(表示 Protected 或 Internal),通过在成员声明时同时使用 protectedinternal 修饰符来选择之,其直观含义为「仅限本程序内或该类及其派生类性内可访问(access limited to this program or types derived from the containing class)」;
  • Private,通过在成员声明时使用 private 修饰符来选择之,其直观含义为「仅限于该类型内部访问(access limited to the containing type)」。

声明成员时被允许使用的可访问性类型取决于该成员所处之上下文。而且当成员在声明时不带任何访问控制修饰符(access modifiers),那么声明所在的上下文会为其选择一个默认的声明可访问性。

  • 命名空间隐式的为 public 声明可访问性。命名空间声明不允许有访问控制修饰符。
  • 编译单元或命名空间内的类型声明允许使用 publicinternal 声明可访问性,且默认的声明可访问性为 internal
  • 类成员可以从这五种声明可访问性中挑选一个,且默认的声明可访问性为 private。(注意,类中声明一个某类型的成员也可以从五种声明可访问性中挑选一个,尽管这个类型作为命名空间下的成员在声明时只有 publicinternal 可选。)
  • 结构成员拥有 publicinternalprivate 声明可访问性,且默认声明可访问性为 private(这是因为结构隐式密封 sealed)。结构成员如果是在这个结构内声明的(也就是说不是继承自其基结构),那么其声明可访问性不可是 protectedprotected internal。(注意,当一个类型声明为某结构的成员时,可以从 publicinternalprivate 中选择一个声明可访问性,尽管这个类型在命名空间中的声明可访问性只能是 publicinternal 的。)
  • 接口成员隐式的为 public 声明可访问性。接口成员声明不允许有访问控制修饰符。
  • 枚举成员隐式的为 public 声明可访问性。枚举成员声明不允许有访问控制修饰符。

可访问域

成员可访问域(accessibility domain)由其所在程序文本之区段(sections,不一定是连续的)所组成,于该域之内可访问成员。为了定义成员的可访问域,若成员未被声明于一个类型内则谓之「顶级(top-level)」,若成员被声明于另一个类型内则谓之「嵌套(nested)」。此外,程序的程序文本(program text)被定义为其所有源文件中的全部文本,而类型的程序文本也被定义为其类型(及其嵌套类型)声明中的全部文本。

预定义类型(predefined type,诸如 objectintdouble)的可访问域是没有限制的(unlimited)。

顶级未绑定类型 T(第四章第 4.3 节)的可访问域被声明在程序 P 中应被如下定义:

  • 如果 T 的声明可访问性是 public 的,则其可访问域是 P 的整个程序文本以及所有引用了 P 的程序;
  • 如果 T 的声明可访问性是 internal 的,则其可访问域是 P 的整个程序文本。

从这些定义可知,顶级未绑定类型的可访问域至少是声明了该类型的程序的程序文本。

已创建的类型 T<A1, ..., AN> 的可访问域是未绑定泛型类 T 的作用域与类型实参 A1, ..., AN 的作用域的交集(intersection)。

声明在程序 P 的类型 T 内的嵌套成员 M 的可访问类型的定义,遵照以下规则(注意,M 自身可能也是一个类型):

  • 如果 M 的声明可访问性是 public,那么 M 的可访问域与 T 的可访问域一致;
  • 如果 M 的声明可访问性是 protected internal,设 D 为程序文本 P 与所有派生自 T 类型的程序文本(在 P 外部声明的)的并集(union),则 M 的可访问域为 T 的可访问域与 D 的交集(intersection);
  • 如果 M 的声明可访问性是 protected,设 D 为 T 的程序文本与所有派生自 T 类型的程序文本的并集(union),则 M 的可访问域为 T 的可访问域与 D 的交集(intersection);
  • 如果 M 的声明可访问性是 internal,则 M 的可访问域为 T 的可访问域与 P 的程序文本的交集(intersection);
  • 如果 M 的声明可访问性是 private,则 M 的可访问域就是 T 的程序文本。

由这些定义可以得知,嵌套成员的可访问域至少是该成员声明所在的类型的程序文本。此外还能发现这么一点,成员的可访问域绝不比该成员声明所在类型的可访问域更广。

直观地说,当访问类型或成员 M 时,遵循以下步骤进行计算以确保其可被访问到:

  • 首先,如果 M 声明于一个类型(相反于编译单元或命名空间)内,则当该类型不可被访问将引发一个「编译时错误」;
  • 其次,如果 M 是 piblic,则允许其被访问;
  • 再次,如果 M 是 protected internal,那么访问发生在 M 声明所在的程序内或访问发生在派生自 M 的类型(第三章第 5.3 节)进行访问时,允许其被访问;
  • 第四,如果 M 是 protected,那么访问发生在 M 声明所在的类内或访问发生在派生自 M 的类型(第三章第 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;
    }
}

类和成员拥有如下可访问域:

  • AA.X 的可访问域不受限制;
  • A.YBB.XB.YB.CB.C.X 以及 B.C.Y 的可访问域是程序文本所在的程序内;
  • A.Z 的可访问域是 A 的程序文本;
  • B.ZB.D 的可访问域是 B 的程序文本,包括 B.CB.D 的程序文本;
  • B.C.Z 的可访问域是 B.C 的程序文本;
  • B.D.XB.D.Y 的可访问域是 B 的程序文本,包括 B.CB.D 的程序文本;
  • B.D.Z 的可访问域是 B.D 的程序文本;

如示例所示,成员的可访问域永不会大于其所在类型(的可访问域)。比方说即便成员 X 的声明可访问性都是 public,但除了 A.X 外其余成员都受制于其所在类型。

如第三章第四节所解释的,所有来自基类的成员(除了实例构造函数、析构函数和静态构析函数)由派生类型所继承。这甚至包括了基类的私有成员。然而,所包含的私有成员的可访问域只能在其声明的类型内部的程序文本内。比方说:

class A
{
    int x;
    static void F(B b) {
        b.x = 1;        // 正确
    }
}
class B: A
{
    static void F(B b) {
        b.x = 1;        // 错误,x 不可访问
    }
}

B 类从 A 类继承了私有(private)成员 x。因为这个成员是私有的,所以它只能在 A 的类主体内部可被访问。因此,可以通过 A.F 方法访问 b.x,但不能通过 B.F 访问 b.x

实例成员的受保护访问

当一个以 protected 修饰的实例成员被程序文本之外的类访问时,或者当一个以 protected internal 丢失的实例成员被程序文本之外的类访问时,访问必须发生在此类的派生类内。此外,这个访问必须通过在派生类实例或构造自它的类型的实例来访问。这个限制(restriction)组织了一个派生类访问另一个派生类的 protected 成员(即使它们派生自同一个基类)。

假设 B 为基类(它声明了一个受保护的(protected)实例成员 M),并设 B 为其派生类。在 D 的类主体(class-body)内部,须按照以下形式之一访问 M:

  • M 形式的非限定(unqualified)的 type-nameprimary-expression
  • E.M 形式的 primary-expression,假设类型 E 为 T 或其派生类,T 为 类型 D 或构造自 D 的类型;
  • base.M 形式的 primary-expression

除这些形式的访问外,派生类可以在构造初始化器(constructor-initializer,第十章第 11.1 节)内访问到受保护的(protected)基类实例构造函数

public class A
{
    protected int x;
    static void F(A a, B b) {
        a.x = 1;        // 正确
        b.x = 1;        // 正确
    }
}
public class B: A
{
    static void F(A a, B b) {
        a.x = 1;        // 错误,必须通过 B 的实例访问
        b.x = 1;        // 正确
    }
}

上例 A 中,可以通过 A 和 B 访问到 x,因为这两次访问都发生在 A 的实例或其派生类中。然而,在 B 中,不能通过 A 的实例去访问 x,因为 A 不是 B 的派生类。

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 的赋值动作都是合法的,因为它们都通过构造自泛型类型的类类型的实例进行的。

可访问性约束

在 C# 语言的一些构造中要求类型至少与其成员或其它类型具有相同的可访问性(be at least as accessible as)。如果 T 的可访问域(accessibility domain)是 M 的可访问域的超集(superset),那么我们可以说类型 T 至少拥有与成员或类型 M 相同的可访问性。换句话说,如果在任何 M 可被访问的上下文中,T 都可被访问,那么 T 至少拥有 M 的可访问性。

有以下这些可访问性约束:

  • 类类型的直接基类必须至少与该类类型自身具有相同的可访问性;
  • 接口显式继承的基接口必须至少与该接口类型自身具有相同的可访问性;
  • 委托类型的返回类型与形参类型必须至少与该委托自身具有相同的可访问性;
  • 常量类型必须至少与该常量自身具有相同的可访问性;
  • 字段类型必须至少与该字段自身拥有相同的可访问性;
  • 方法的返回类型与形参类型必须至少与该方法自身具有相同的可访问性;
  • 属性类型必须至少与该属性自身具有相同的可访问性;
  • 事件类型必须至少与该事件自身具有相同的可访问性;
  • 索引器的类型和形参类型必须至少与其自身具有相同的可访问性;
  • 操作符的返回类型和形参类型必须至少与其自身具有相同的可访问性;
  • 实例构造函数的参数类型必须至少与其自身具有相同的可访问性。

在下例中,

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

B 类将出现「编译时错误」,因为 A 类不具备至少与 B 类相同的可访问性。同样,在下例中,

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

H 方法将出现「编译时错误」,因为 H 方法所返回的类型 A 不具备至少与该方法相同的可访问性。

签名与重载

方法、实例构造器、索引器和操作符由其签名来鉴定:

  • 方法签名由方法名、类型参数成员以及形参的类型与种类(值 value、引用 reference 或输出 output),按照从左到右的顺序构成。为此,出现在方法形参内的类型参数的识别并不根据它们的名字,而是依次根据它们在方法类型实参类表中的位置。
  • 实例构造函数的签名由每个形参的类型和种类(值 value、引用 reference 或输出 output)从左到右构成。具体来说,实例构造函数签名不包含最右侧 params 修饰符(params modifier)指定的参数。
  • 索引器签名由其每个形参的类型自左而右地构成。具体来说,索引器签名不包含元素类型,也不包含最右侧 params 修饰符(params modifier)指定的参数。
  • 操作符签名由签名符名和形参类型自左而右构成。具体来说,操作符签名不包含结果类型。

签名能使用类、结构或接口的成员的重载机制:

  • 允许类、结构或接口通过声明多个同名方法对其重载,只要它们的签名在类、结构或接口中是唯一的即可;
  • 允许类或结构的实例构造函数通过声明多个实例构造函数对其重载,只要它们的签名在类或结构中是唯一的即可;
  • 允许类、结构或接口中的索引器通过声明多个索引器对其重载,只要它们的签名在类、结构或接口中是唯一的即可;
  • 允许类或结构的操作符通过声明多个操作符对其重载,只要它们的签名在类或结构中是唯一的即可。

虽然 outref 参数修饰符是构成签名的一部分,但不能仅通过 refout 修饰符来区分成员签名。如果两个成名声明为相同签名的类型(即使它们内的方法的形参修饰符从 out 变为 ref),那么将出现一个「编译时错误」。为了签名匹配的其他用途(比如隐藏 hidden 或重写 overriding),refout 是构成签名的一部分,以便相互不匹配。(这个限制允许 C# 程序能轻易转换为能运行在公共语言基础结构(Common Language Infrastructure,CLI)上,CLI 并未提供方式去定义只通过 refout 进行区别的方法)。

由于签名的缘故,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)   错误
    void F(int x, int y);      // F(int, int)
    int F(string s);           // F(string)
    int F(int x);              // F(int)       错误
    void F(string[] a);        // F(string[])
    void F(params string[] a); // F(string[])  错误
}

注意,任何 refout 参数修饰符(第十章第 6.1 节)都是签名的一部分。因此,F(int)F(ref int) 都是唯一的签名。但是 F(ref int)F(out int) 不能定义在同一个接口内,因为签名不能仅从 refout 加以区分。同样的,返回类型和 params 修饰符都不是签名的一部分,所以不可能仅基于返回类型或是否包含 params 修饰符来区别重载,故而上面的方法声明 F(int)F(params string[]) 会引发一个「编译时错误」。

作用域

名称的作用域(scope)是一个程序文本区域(region),在其中可以用过名称引用实体声明而不对该名称加以限定条件(qualification)。作用域可嵌套,内部作用域可重声明外部作用域名称的含义(但这并不会移除在第三章第三节中对其的限制——在嵌套块内不能声明与闭包块(enclosing block)内局部变量同名的局部变量)。因此可以说此外部作用域的名称在该内部作用域覆盖的程序文本区域内是隐藏(hidden)的,只能通过限定名来访问外部名称。

  • namespace-member-declaration(第九章第五节)声明的命名空间成员的作用域——如果没有其它 namespace-declaration 对其闭包的话——是整个(entire)程序文本。
  • namespace-declaration 内的 namespace-member-declaration 声明的命名空间成员的作用域是——如果假设该命名空间成员声明的完全限定名为 N——完全限定名为 N 或以 N 为始、后跟一个句号(period)的每个 namespace-declarationnamespace-body
  • extern-alias-directive 定义的名称的作用域扩展到直接包含其编译单元或命名空间主体的 using-directivesglobal-attributesnamespace-member-declarationsextern-alias-directive 并不会为底层声明空间(underlying declaration space)增加任何成员。换句话说 extern-alias-directive 并不具传递性,相反其只会影响到在其内出现的 compilation-unitnamespace-body
  • using-directive(第九章第四节)定义或导入的名称的作用域扩展到出现 using-directivecompilation-unitnamespace-body 的整个 namespace-member-declarationsusing-directive 能使零或多个命名空间或类型名在特定的 compilation-unitnamespace-body 变得可用,但并不会为底层声明空间(underlying declaration space)增加任何成员。换句话说 using-directive 并不具传递性,相反其只会影响到在其出现的 compilation-unitnamespace-body
  • 由一个在 class-declaration(第十章第一节)中的 type-parameter-list 所声明的类型形参的作用域是该 class-declarationclass-basetype-parameter-constraints-clauses 以及 class-body
  • 由一个在 struct-declaration(第十一章第一节)中的 type-parameter-list 所声明的类型形参的作用域是该 struct-declarationstruct-interfacestype-parameter-constraints-clauses 以及 struct-body
  • 由一个在 interface-declaration(第十三章第一节)中的 type-parameter-list 所声明的类型形参的作用域是该 interface-declarationinterface-basetype-parameter-constraints-clauses 以及 interface-body
  • 由一个在 delegate-declaration(第十五章第一节)中的 type-parameter-list 所声明的类型形参的作用域是该 delegate-declarationreturn-typeformal-parameter-list 以及 type-parameter-constraints-clauses
  • class-member-declaration(第十章第 1.6 节)所声明的成员的作用域位于该声明所出现的 class-body 之内。此外,类成员的作用域扩展到包含该成员且为可访问的(accessibility,第三章第 5.2 节)派生类的 class-body
  • struct-member-declaration(第十一章第二节)声明的成员的作用域位于该声明出现的 struct-body 之内。
  • enum-member-declaration(第十四章第三节)所声明的成员的作用域位于该声明出现的 enum-body 内。
  • 位于 method-declaration(第十章第六节)内所声明之参数的作用域是该 method-declarationmethod-body
  • 位于 indexer-declaration(第十章第九节)内所声明之参数的作用域是该 indexer-declarationaccessor-declarations
  • 位于 operator-declaration(第十章第十节)内所声明之参数的作用域是该 operator-declarationblock
  • 位于 constructor-declaration(第十章第十一节)内所声明之参数的作用域是该 constructor-declarationconstructor-initializerblock
  • 位于 lambda-declaration(第七章第十五节)内所声明之参数的作用域是该 lambda-expressionlambda-expression-body
  • 位于 anonymous-method-expression(第七章第十五节)内所声明之参数的作用域是该 anonymous-method-expressionblock
  • 位于 labeled-statement(第八章第四节)内所声明之标签的作用域是该声明所在的 block
  • 位于 local-variable-declaration(第八章第 5.1 节)内所声明之局部变量的作用域是该声明所在的 block
  • 位于 switch 语句(第八章第 8.3 节)的 switch-block 内所声明的局部变量的作用域是该 switch-block
  • 位于 for 语句(第八章第 8.3 节)的 for-initializer 内所声明的局部变量的作用域是该语句的 for-initializerfor-conditionfor-iterator 以及所含之 statement
  • 位于 local-constant-declaration(第八章第 5.2 节)内声明的局部变量的作用域是该声明出现的 block。在该局部变量的 constant-declarator 之前的文本位置上引用该局部变量将出现一个「编译时错误」。
  • 作为 foreach-statementusing-statementlock-statementquery-expression 一部分所声明的变量的作用域由给定构造(construct)的扩展(expansion)所决定。

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

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

在此,F 在 i 声明之前引用它是合法的。

在局部变量的作用域内,当引用局部变量的文本位置(textual position)在该局部变量声明之前(local-variable-declarator)会出现一个「编译时错误」。比方说

class A
{
    int i = 0;
    void F() {
        i = 1;   // 错误,在使用前先声明
        int i;
        i = 2;
    }
    void G() {
        int j = (j = 1);     // 有效
    }
    void H() {
        int a = 1, b = ++a;  // 有效
    }
}

在上面方法 F 中,第一个对 i 的赋值实际上并不会引用外部作用域的字段。相反,它引用了本地局部变量,而其结果是引发一个「编译时错误」,因为在文本上要求先声明变量。在方法 G 中,为 j 进行声明的同时在其初始化器内使用 j 是有效的,这是因为并没有在 local-variable-declarator 之前使用。在方法 H 中,后面的 local-variable-declarator 正确引用了局部变量(该局部变量在同一个 local-variable-declarator 内前面那个 local-variable-declarator 中声明了)。

本地布局变量的作用域规则被设计为保证(guarantee)表达式上下文(expression context)中使用的名称的含义与在块中使用的含义是相同的。如果局部变量的作用域只从其声明之处其,到块尾部截止,那么在上例中,第一个赋值将分配给实例变量(instance variable),第二次赋值将分配给局部变量,如果块中的语句之后重新排列,会引发「编译时错误」。

块(block)中名称的含义可能因名称的使用上下文的不同而有所不同。在下例中,

using System;
class A {}
class Test
{
    static void Main() {
        string A = "hello, world";
        string s = A;         // 表达式上下文
        Type t = typeof(A);   // 类型上下文
        Console.WriteLine(s); // 输出 "hello, world"
        Console.WriteLine(t); // 输出 "A"
    }
}

在表达式上下文中的名称 A 引用本地变量 A,在类型上下文中引用类型 A

名称隐藏

实体(entity)的作用域(scope)往往比实体声明空间包含(encompasses)更多的程序文本。具体来说,实体的作用域可能会引入新的声明空间,而其中或许会包含与该实体同名的实体。这类声明导致原始实体变为隐藏的(hidden)。相反,如果实体没有被隐藏,那么我们说它是可见的(visible)

当作用域通过嵌套交叉(overlap)或当作用域通过继承交叉,那么会导致名称隐藏。两个被隐藏的类型的特征在下面两节中进行介绍。

通过嵌套隐藏

在命名空间内嵌套命名空间或类型,或者在类或结构内嵌套类型,以及性参与局部变量的声明,都将导致嵌套隐藏(hiding through nesting)名称。举个例子。

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);        // 调用 Outer.Inner.F
            F("Hello");  // 错误
        }
        static void F(long l) {}
    }
}

调用(call)F(1) 将调用(invokes)内部声明的 F,这是因为所有外部出现的 F 都被内部声明所隐藏。同样的,调用 F("Hello") 的结果是出现「编译时错误」。

通过继承隐藏

当类或结构重新声明(redeclare)从基类继承的名称时,会发生通过继承隐藏(hiding through inheritance)其名称。这种类型的名称隐藏采取以下形式:

  • 常量、字段、属性、事件或类型引入类或结构后,会隐藏所有基类中的同名成员。
  • 方法引入类或结构将隐藏所有同名的非方法基类成员(non-method base class members)和所有同签名(方法名、参数数量、修饰符与类型)的基类方法。
  • 索引器引入类或结构将隐藏所有同签名(参数数量与类型)的基类索引。

管理运算符声明(governing operator declarations,第十章第十节)的规则使其不可能在派生类中声明一个与其基类内同签名的运算符。因此,操作符不能相互隐藏。

与从作用域外部对名称进行隐藏相反,通过继承的作用域隐藏名称可访问性会报出警告。下例中,

class Base
{
    public void F() {}
}
class Derived: Base
{
    public void F() {}  // 警告,隐藏了继承到的名称
}

派生类 DerivedF 的声明会导致一个警告。隐藏所继承的名称实际上不是一个错误,因为这会妨碍基类自身的单独改进。比方说,上述情况可能会发生因为 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() {} // 只会隐藏 Derived 内的 Base.F
}
class MoreDerived: Derived
{
    static void G() { F(); }       // 调用 Base.F
}

上例中,派生类中的 F 声明隐藏了(hide)其从基类继承到的 F。但由于派生类中新 F 的可访问性是 private,因此它的作用域达不到 MoreDerived。因此 MoreDerived.G 调用(call) F() 方法依旧合法,它将调用 Base.F

命名空间与类型名

C# 程序的上下文要求指定命名空间名标记 namespace-name 或类型名标记 type-name

C# Language Specification 5.0 第三章 基本概念_第2张图片

namespace-name 标记是一个引用命名空间的 namespace-or-type-name 标记,它根据如下描述执行解析工作(resolution),namespace-namenamespace-or-type-name 标记必须引用一个命名空间,不然将会出现「编译时错误」。没有类型实参(type arguments,第四章第 4.1 节)可以在 namespace-name 中出现(只有类型才能具有类型实参)。

type-name 标记是一个引用类型的 namespace-or-type-name 标记,它根据如下描述执行解析工作,type-namenamespace-or-type-name 标记必须引用一个类型,不然会出现「编译时错误」。

如果 namespace-or-type-name 标记是 qualified-alias-member 标记,则其含义如第九章第七节所述。否则 namespace-or-type-name 标记有以下四种形式:

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

其中 I 是单一修饰符(single identifier),Nnamespace-or-type-name<A1, ..., AK> 是可选的 type-argument-list。当没有 type-argument-list 被指定时,K 将为零(zero)。

namespace-or-type-name 的含义取决于如下:

  • 如果 namespace-or-type-name 标记是 II<A1, ..., AK> 的形式的话:
    • 如果 K 是零(zero), namespace-or-type-name 标记出现在一个泛型方法声明(generic method declaration,第十章第六节)内且其声明还包含名为 I 的类型形参(type parameter,第十章第 1.3 节),那么 namespace-or-type-name 引用该类型形参;
    • 不然的话,如果 namespace-or-type-name 标记出现在类型声明内,那么类型 T 的每个实例(第十章第 3.1 节),从该类型声明的实例类型开始,每个闭包类(enclosing class)或结构声明(如果有的话)的实例类型继续如下过程:
      • 如果 K 是零,T 的声明包含名为 I 的类型形参,那么 namespace-or-type-name 标记引用该类型形参;
      • 不然的话,如果 namespace-or-type-name 出现在类型声明主体(body)内部,同时 T 或或其任意基类包含具有名称 IK 个类型形参的嵌套可访问类型(nested accessible type),那么 namespace-or-type-name 标记引用该给定类型实参(type arguments)构造的类型。如果存在多个该类型,则选择类型声明于较大程序派生类型内的那个类型。要注意,当确定了 namespace-or-type-name 标记的含义之后,非类型成员(non-type members,包括常量、字段、方法、属性、索引器、操作符、实例构造函数、析构函数以及静态构造函数)以及带有不同数量类型形参的类型成员都将被忽略。
    • 如果前述步骤都不成功,则对每个命名空间 N,起始于出现 namespace-or-type-name 标记的命名空间,持续到每一个闭包命名空间(如果有的话),直至止于全局命名空间(global namespace),对以下步骤进行运算直至定位到(located)一个实体:
      • 如果 K 是零,同时 IN 内命名空间的名称,则:
        • 如果 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-directiveusing-alias-directive 标记,那么 namespace-or-type-name 标记将模棱两可并会出现一个「编译时错误」。
        • 否则的话,namespace-or-type-name 标记引用由给定类型实参构建的类型。
      • 再不然的话,如果 namespace-or-type-name 标记出现的位置闭包于名为 N 的命名空间声明内的话,则:
        • 如果 K 是零并且命名空间包含了名为 I 的与某个导入的命名空间或类型相关的 extern-alias-directiveusing-alias-directive 标记,那么 namespace-or-type-name 将引用该命名空间或类型。
        • 不然的话,如果由 using-namespace-directives 标记导入命名空间声明的命名空间确切包含一个名为 I 的有 K 个类型形参,那么 namespace-or-type-name 将引用由该给定类型实参构造的类型。
        • 否则,如果由 using-namespace-directives 标记导入命名空间声明的命名空间包含超过一个名为 I 的有 K 个类型形参的类型,那么 namespace-or-type-name 标记将模棱两可并会出现一个「编译时错误」。
      • 否则的话,说明 namespace-or-type-name 标记未被定义,同时报出一个「编译时错误」。
    • 否则,形式为 N.IN.I<A1, ..., AK>.Nnamespace-or-type-name 首先解析(resolved)为 namespace-or-type-name。如果 N 的解析(resolution)不成功,那么会出现一个「编译时错误」。否则,N.IN.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 的直接基类将被视为对象(object,第十章第 1.4.1 节)。
      • 否则的话,N.I 是个非法的 namespace-or-type-name 标记,并会出现一个「编译时错误」。

只有在以下情况下才允许 namespace-or-type-name 引用一个静态类(第十章第 1.1.3 节)

  • namespace-or-type-nameT.I 形式的 namespace-or-type-name 中的 T,或者
  • namespace-or-type-nametypeof(T) 形式的 typeof-expression(第七章第 5.11 节)中的 T。

完全限定名

每个命名空间和类型都拥有一个完全限定名(fully qualified name),它是一个唯一标识(uniquely identifies),用于相互间进行区分。命名空间或类型 N 的完全限定名遵照以下规则决定:

  • 如果 N 是一个全局命名空间成员,则其完全限定名为 N
  • 否则的话,其完全限定名为 S.N(S 为 N 所声明的命名空间或类型之完全限定名)。

换句话说,N 的完全限定名是指向(lead to)N 的标识符(identifiers)的完整分层路径(complete hierarchical path)。因为每个命名空间或类型的成员都必须有一个唯一的名称,因此如果把它们放在命名空间或类型的完全限定名之后,这样所形成的名称也总是唯一的。

在下例中演示了多个命名空间和类型的声明以及与其相关的完全限定名。

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
}

自动内存管理

C# 使用(employs)自动内存管理(automatic memory management),这解放了开发人员必须手动分配与释放对象内存战勇的麻烦。自动内存管理策略由垃圾回收器(garbage collector)实现。对象的内存管理生命周期如下:

  1. 当对象被创建时,为它分配内存,运行构造函数,视该对象为存活(live)对象。
  2. 如果一个对象(或其任何一个部分)不能在后续执行(continuation of execution)中被访问——除了析构函数——那么这个对象将被视为不再使用并符合销毁条件(eligible for destruction)。C# 编译器和垃圾回收器会选择分析代码,并确定哪个对象的引用在未来会被使用。比方说,如果某范围内某个局部变量是某对象的唯一存在的引用,但在当前执行点(current execution point)之后的所有过程中(procedure)都不会再引用这个变量,那么垃圾回收器可能(但不是绝对)会理解为这个对象不再被使用。
  3. 一旦对象符合销毁条件(eligible for destruction),那么在稍后某个时间(at some unspecified later time)将运行这个对象的析构函数(第十章第十三节)(如果有析构函数的话)。在正常情况下,对象的析构函数只会运行一次(once only),但特定实现(implementation-specific)的 APIs 可能会允许忽视(overridden)这个行为。
  4. 一旦运行该对象的析构函数,那么这个对象(或其任何一部分)都不能在后续执行中被访问到——包括运行中的析构函数——这个对象将被视为不可访问(inaccessible)并符合回收条件(eligible for collection)。
  5. 最后,在该对象符合回收条件后的某个时刻,垃圾回收器释放分配给该对象的内存。

垃圾回收器维护着对象使用(object usage)的信息,透过这些信息为内存管理作出决策,诸如何处内存存放了新建对象,何时迁移(relocate)一个对象以及一个对象何时开始不被使用或不可访问。

和其它有垃圾回收器的语言类似,C# 的设计也在努力使垃圾回收器能实现更广泛的(wide range)内存管理策略(memory management policies)比方说,C# 并不强求析构函数一定要运行,并不强求对象一满足条件就要立即被回收,并不强求析构函数一定要按照某个特定顺序执行或是在某个特定线程上执行。

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

由于垃圾回收器在决定「何时回收(collect)对象并执行析构函数」这一点上充分自由(allowed wide latitude),因此一个合格的实现也许会产生与下面所示代码有所不同的输出。在程序

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 的实例。当 null 值被赋予变量 b 之后,这些资源成为符合回收条件,这是由于自此之后用户所写的任何代码都不可能访问到它们。输出的结果可能是

Destruct instance of A
Destruct instance of B

或者

Destruct instance of B
Destruct instance of A

,这是由于语言并未对对象被垃圾回收的顺序强加限制。

尽管很相近,但区别「符合销毁条件(eligible for destruction)」与「符合回收条件(eligible for collection)」还是比较重要的,比方说:

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

在上面程序中,如果立即回收器(garbage collector)选择在运行 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 接口。这将允许对象的客户端(client of the object)决定何时释放自身资源,通常会在 using 语句(第八章第十三节)通过资源的方式访问该对象。

执行顺序

C# 程序执行处理是这样进行的,每一个执行线程的副作用都保持在临界执行点(critical execution points)上。副作用被定义为:无定性字段(volatile field)的读写、非无定性变量(non-volatile variable)的写入、外部资源(external resource)的写入以及抛出异常。按照这个副作用定义的顺序,临界执行点分别是指:引用一个无定性字段(volatile fields,第十章第 5.3 节)、引用 lock 语句(第八章第十二节)以及引用线程的创建与终止。执行环境在遵照下列限制的前提下自由改变执行顺序:

  • 在执行线程中保持数据依赖性。也就是说,计算每一个变量的值时,就好似在线程里所有语句都按照原本程序的顺序执行的。
  • 保留初始化的排序规则(第十章第 5.4 节和第十章第 5.5 节)。
  • 对于无定性的(volatile)读和写(第十章第 5.3 节),副作用(side effects)的顺序保持不变。另外,如果执行环境可以推断(deduce)一个表达式的值不会被使用并且不会产生有效的(needed)副作用(包括所有因调用方法或访问无定性字段所导致的副作用)的话,那么就不需要去计算表达式的每一个部分。当程序执行被一个异步(asynchronous)事件(诸如由另一个现成抛出异常)中断(interrupted),就不能保证(guaranteed)可观察(observable)到副作用是否会以原有的程序顺序出现。

__EOF__

C# Language Specification 5.0 翻译计划

你可能感兴趣的:(C# Language Specification 5.0 第三章 基本概念)