您的位置:首页 > 编程语言 > C#

C# Language Specification 1.2 之三 基本概念

2006-08-03 15:41 330 查看

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.2.3 节)确定。同样,类型的已声明可访问性由其声明中指定的访问修饰符确定。为了使给定类型的给定方法可以被调用,类型和成员都必须是可访问的。然而,应用程序入口点是一种特殊情况。具体而言,执行环境可以访问应用程序的入口点,无论它本身的可访问性和封闭它的类型的可访问性是如何在声明语句中设置的。

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

1.2 应用程序终止

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

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

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

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

1.3 声明

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

一个声明在它自已所属的那个声明空间 (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 引入的。除了重载实例构造函数声明和静态构造函数声明外,类或结构成员声明不能引入与该类或结构同名的成员。类、结构或接口允许声明重载方法和索引器。另外,类或结构允许重载实例构造函数和运算符的声明。例如,类、结构或接口可以包含多个同名的方法声明,前提是这些方法声明的签名(第 3.6 节)不同。注意,基类与类的声明空间无关,基接口与接口的声明空间无关。因此,允许在派生类或接口内声明与所继承的成员同名的成员。我们说这类成员隐藏 (hide) 了那些被它们所继承的成员。

· 每个枚举声明创建一个新的声明空间。名称通过 enum-member-declarations 引入此声明空间。

· 每个 block 或 switch-block,以及 for、foreachusing 语句,都会为局部变量和局部常量创建一个声明空间,名为局部变量声明空间 (local variable declaration space)。名称通过 local-variable-declaration 和 local-constant-declaration 引入此声明空间。如果块是实例构造函数、方法或运算符声明的体,或是索引器声明的 get 或 set 访问器,这些声明中声明的参数是块的局部变量声明空间的成员。如果局部变量声明空间的两个成员具有相同的名称,则会发生错误。如果某个块的局部变量声明空间和嵌套的某个局部变量声明空间包含具有相同名称的元素,则会发生错误。因此,在嵌套声明空间中不可能声明与封闭它的声明空间中的局部变量或常量同名的局部变量或常量。只要两个声明空间彼此互不包含,则两个声明空间可以包含同名的元素。

· 每个 block 或 switch-block 都为标签创建一个单独的声明空间。名称通过 labeled-statement 引入此声明空间,通过 goto-statement 语句被引用。块的标签声明空间 (label declaration space) 包含任何嵌套块。因此,在嵌套块中不可能声明与封闭它的块中的标签同名的标签。

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

· 字段声明和局部变量声明的声明顺序确定其初始值设定项(如果有)的执行顺序。

· 在使用局部变量前必须先定义它们(第 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.3 节)。

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) 由(可能是不连续的)程序文本节组成,从那里可以访问该成员。出于定义成员可访问域的目的,如果成员不是在某个类型内声明的,就称该成员是顶级的;如果成员是在其他类型内声明的,就称该成员是嵌套的。此外,程序的程序文本定义为包含在该程序的所有源文件中的全部程序文本,而类型的程序文本定义为包含在该类型(可能还包括嵌套在该类型内的类型)的 class-body、struct-body、interface-body 或 enum-body 中的开始和结束(“{”和“}”)标记之间的全部程序文本。

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

在程序 P 中声明的顶级类型 T 的可访问域如下定义:

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

· 如果 T 的已声明可访问性为 internal,则 T 的可访问域是 P 的程序文本。

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

在程序 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 的类型是类 D 或是从 D 派生的类)。

· base.M 形式的 primary-expression。

除了上述访问形式外,派生类还可以在它的 constructor-initializer 中(第 10.10.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。

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 修饰符(它可用于最右边的参数)。

· 运算符签名由运算符的名称和它的每一个形参(按从左到右的顺序)的类型组成。具体而言,运算符的签名不包含结果类型。

签名是对类、结构和接口的成员实施重载 (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.5.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.4 节)所声明的命名空间成员的范围,如果没有其他封闭它的 namespace-declaration,则它的范围是整个程序文本。

· namespace-declaration 中 namespace-member-declaration 所声明的命名空间成员的范围是这样定义的,如果该命名空间成员声明的完全限定名为 N,则其声明的命名空间成员的范围是,完全限定名为 N 或以 N 开头后跟句点的每个 namespace-declaration 的 namespace-body。

· 由 using-directive(第 9.3 节)定义或导入的名称的范围扩展到出现 using-directive 的 compilation-unit 或 namespace-body 内的整个 namespace-member-declarations 中。using-directive 可以使零个或更多个命名空间或者类型名称在特定的 compilation-unit 或 namespace-body 中可用,但不会把任何新成员提供给下层声明空间。换言之,using-directive 仅在使用它的 compilation-unit 或 namespace-body 范围内有效,它的功效是不可传递的。

· 由 class-member-declaration(第 10.2 节)所声明的成员范围是该声明所在的那个 class-body。此外,类成员的范围扩展到该成员的可访问域(第 3.5.2 节)中包含的那些派生类的 class-body。

· 由 struct-member-declaration(第 11.2 节)声明的成员范围是该声明所在的 struct-body。

· 由 enum-member-declaration(第 14.3 节)声明的成员范围是该声明所在的 enum-body。

· 在 method-declaration(第 10.5 节)中声明的参数范围是该 method-declaration 的 method-body。

· 在 indexer-declaration(第 10.8 节)中声明的参数范围是该 indexer-declaration 的 accessor-declarations。

· 在 operator-declaration(第 10.9 节)中声明的参数范围是该 operator-declaration 的 block。

· 在 constructor-declaration(第 10.10 节)中声明的参数范围是该 constructor-declaration 的 constructor-initializer 和 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 之前的文本位置中引用该局部常量是编译时错误。

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

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

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

· 类或结构中引入的常量、字段、属性、事件或类型会把其基类中所有的同名的成员隐藏起来。

· 类或结构中引入的方法隐藏所有同名的非方法基类成员,以及所有具有相同签名(方法名称和参数个数、修饰符和类型)的基类方法。

· 类或结构中引入的索引器隐藏所有具有相同签名(参数个数和类型)的基类索引器。

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

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

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
namespace-or-type-name . identifier

type-name 是引用一个类型的 namespace-or-type-name。需遵循下述决策:type-name 的 namespace-or-type-name 必须引用一个类型,否则将发生编译时错误。

namespace-name 是引用一个命名空间的 namespace-or-type-name。需遵循下述决策:namespace-name 的 namespace-or-type-name 必须引用一个命名空间,否则将发生编译时错误。

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

· 如果 namespace-or-type-name 由单个标识符组成:

o 如果 namespace-or-type-name 出现在类或结构声明体内,则从该类或结构声明开始查找,遍及每个封闭它的类或结构声明(若它们存在),如果具有给定名称的成员存在、可访问且表示类型,则 namespace-or-type-name 引用该成员。请注意,当确定 namespace-or-type-name 的含义时,会忽略非类型成员(常量、字段、方法、属性、索引器、运算符、实例构造函数、析构函数和静态构造函数)。

o 否则,从发生 namespace-or-type-name 的命名空间开始,遍及每个封闭它的命名空间(若它们存在),直至全局命名空间结束,对下列步骤进行评估,直到找到实体:

· 如果命名空间包含具有给定名称的命名空间成员,则 namespace-or-type-name 引用该成员,并根据该成员归为命名空间或类型类别。

· 否则,如果命名空间有一个对应的命名空间声明,且 namespace-or-type-name 出现的位置包含在该命名空间声明中,则:

o 如果该命名空间声明包含一个将给定名称与一个导入的命名空间或类型关联的 using-alias-directive,则 namespace-or-type-name 引用该命名空间或类型。

o 否则,如果该命名空间声明中有一个 using-namespace-directive,它导入的那个命名空间内含有一个与给定名称完全匹配的类型,则 namespace-or-type-name 引用该类型。

o 否则,如果该 using-namespace-directive 导入的命名空间包含多个具有给定名称的类型,则 namespace-or-type-name 就被认为是含义不清的,将导致发生错误。

o 否则,namespace-or-type-name 就被认为是未定义的,导致发生编译时错误。

· 否则,namespace-or-type-name 的形式为 N.I,其中 N 是由除最右边的标识符以外的所有标识符组成的 namespace-or-type-name,I 是最右边的标识符。N 最先按 namespace-or-type-name 解析。如果对 N 的解析不成功,则发生编译时错误。否则,N.I 按下面这样解析:

o 如果 N 是一个命名空间而 I 是该命名空间中可访问成员的名称,则 N.I引用该成员,并根据该成员归为命名空间或类型类别。

o 如果 N 是类或结构类型而 I 是 N 中可访问类型的名称,则 N.I引用该类型。

o 否则,N.I是 invalid namespace-or-type-name 并将发生编译时错误。

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.12 节)(如果有)。除非被显式调用所重写,否则对象的析构函数只运行一次。

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 的实例不符合回收条件。

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

1.10 执行顺序

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

· 在执行线程中需保持数据依赖性。就是说,在计算每个变量的值时,就好像线程中的所有语句都是按原始程序顺序执行的。

· 保留初始化的排序规则(第 10.4.4 节和第 10.4.5 节)。

对于易失性读写(第 10.4.3 节)的副作用的顺序需保持不变。此外,执行环境甚至可以不需要计算一个表达式的各个部分,如果它能推断出表达式的值是“不会被使用的”而且不会产生有效的副作用(包括由调用方法或访问易失性字段导致的任何副作用)。当程序执行被异步事件(例如其他线程引发的异常)中断时,它不保证可观察到的副作用以原有的程序顺序出现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: