C#6.0语言规范(三) 基本概念
应用程序启动
具有入口点的程序集称为应用程序。运行应用程序时,会创建一个新的应用程序域。应用程序的几个不同实例可以同时存在于同一台机器上,并且每个实例都有自己的应用程序域。
应用程序域通过充当应用程序状态的容器来启用应用程序隔离。应用程序域充当应用程序中定义的类型及其使用的类库的容器和边界。加载到一个应用程序域中的类型与加载到另一个应用程序域中的相同类型不同,并且应用程序域之间不直接共享对象实例。例如,每个应用程序域都有自己的这些类型的静态变量副本,每个应用程序域最多运行一次类型的静态构造函数。实现可以自由地为创建和销毁应用程序域提供特定于实现的策略或机制。
当执行环境调用指定的方法(称为应用程序的入口点)时,将发生应用程序启动。此入口点方法始终以名称命名Main
,并且可以具有以下签名之一:
1 static void Main() {...} 2 3 static void Main(string[] args) {...} 4 5 static int Main() {...} 6 7 static int Main(string[] args) {...}
如图所示,入口点可以可选地返回int
值。此返回值用于应用程序终止(应用程序终止)。
入口点可以可选地具有一个形式参数。参数可以具有任何名称,但参数的类型必须是string[]
。如果存在形式参数,则执行环境将创建并传递string[]
包含应用程序启动时指定的命令行参数的参数。该string[]
参数永远不能为null,但它可能有一个长度为零,如果没有指定命令行参数。
由于C#支持方法重载,因此类或结构可能包含某些方法的多个定义,前提是每个方法具有不同的签名。但是,在单个程序中,任何类或结构都不能包含多个调用Main
的方法,其定义使其有资格用作应用程序入口点。Main
但是,如果它们具有多个参数,或者它们的唯一参数不是类型,则允许其他重载版本string[]
。
应用程序可以由多个类或结构组成。这些类或结构中的多个类可能包含一个名为Main
的方法,该方法的定义使其可用作应用程序入口点。在这种情况下,必须使用外部机制(例如命令行编译器选项)来选择这些Main
方法之一作为入口点。
在C#中,每个方法都必须定义为类或结构的成员。通常,方法的声明的可访问性(声明的可访问性)由其声明中指定的访问修饰符(访问修饰符)确定,类似地,声明的类型的可访问性由其声明中指定的访问修饰符确定。为了使给定类型的给定方法可被调用,类型和成员都必须是可访问的。但是,应用程序入口点是一种特殊情况。具体而言,执行环境可以访问应用程序的入口点,无论其声明的可访问性如何,并且无论其封闭类型声明的声明可访问性如何。
应用程序入口点方法可能不在泛型类声明中。
在所有其他方面,入口点方法的行为类似于非入口点。
应用程序终止
应用程序终止将控制权返回给执行环境。
如果应用程序的入口点方法的返回类型是int
,则返回的值将用作应用程序的终止状态代码。此代码的目的是允许将成功或失败的通信传递给执行环境。
如果入口点方法的返回类型是void
,到达右括号(}
)终止该方法,或执行return
没有表达式的语句,则导致终止状态代码为0
。
在应用程序终止之前,将调用其尚未被垃圾回收的所有对象的析构函数,除非已禁止此类清理(例如,通过调用库方法GC.SuppressFinalize
)。
声明
C#程序中的声明定义了程序的组成元素。C#程序使用名称空间(Namespaces)进行组织,名称空间可以包含类型声明和嵌套的名称空间声明。类型声明(类型声明)用于定义类(类),结构(结构),接口(接口),枚举(枚举)和委托(代理)。类型声明中允许的成员类型取决于类型声明的形式。例如,类声明可以包含常量(常量),字段(字段),方法(方法)的声明),属性(属性),事件(事件),索引器(索引器),运算符(运算符),实例构造函数(实例构造函数),静态构造函数(静态构造函数),析构函数(析构函数)和嵌套类型(嵌套类型)。
声明定义声明所属的声明空间中的名称。除了重载成员(签名和重载)之外,如果有两个或多个声明在声明空间中引入具有相同名称的成员,则编译时错误。声明空间永远不可能包含具有相同名称的不同类型的成员。例如,声明空间永远不能包含同名的字段和方法。
有几种不同类型的声明空间,如下所述。
在程序的所有源文件,namespace_member_declaration s的无封闭namespace_declaration被称为一个组合声明空间的成员全局声明空间。
在程序的所有源文件,namespace_member_declaration秒钟内namespace_declaration具有相同的完全限定的命名空间名称■属于一个组合声明空间。
每个类,结构或接口声明都会创建一个新的声明空间。通过class_member_declaration s,struct_member_declaration s,interface_member_declaration s或type_parameter将名称引入此声明空间秒。除了重载的实例构造函数声明和静态构造函数声明之外,类或结构不能包含与类或结构同名的成员声明。类,结构或接口允许声明重载方法和索引器。此外,类或结构允许声明重载的实例构造函数和运算符。例如,类,结构或接口可能包含多个具有相同名称的方法声明,前提是这些方法声明的签名不同(签名和重载))。请注意,基类不会对类的声明空间有所贡献,并且基接口不会影响接口的声明空间。因此,允许派生类或接口声明与继承成员同名的成员。据说这样的成员隐藏了继承的成员。
每个委托声明都会创建一个新的声明空间。通过形式参数(fixed_parameter s和parameter_array s)和type_parameter s 将名称引入此声明空间。
每个枚举声明都会创建一个新的声明空间。名称通过enum_member_declarations引入此声明空间。
每个方法声明,索引器声明,操作符声明,实例构造函数声明和匿名函数都会创建一个称为局部变量声明空间的新声明空间。通过形式参数(fixed_parameter s和parameter_array s)和type_parameter将名称引入此声明空间秒。函数成员或匿名函数的主体(如果有)被认为嵌套在局部变量声明空间中。局部变量声明空间和嵌套局部变量声明空间包含具有相同名称的元素是错误的。因此,在嵌套声明空间中,不可能在封闭声明空间中声明与局部变量或常量同名的局部变量或常量。只要两个声明空间都不包含另一个声明空间,两个声明空间就可以包含具有相同名称的元素。
每个块或switch_block以及for,foreach和using语句都为局部变量和局部常量创建局部变量声明空间。通过local_variable_declaration和local_constant_declaration将名称引入此声明空间。请注意,在函数成员或匿名函数体内或在函数成员或匿名函数体内出现的块嵌套在这些函数为其参数声明的局部变量声明空间中。因此,例如具有局部变量和具有相同名称的参数的方法是错误的。
每个块或switch_block为标签创建单独的声明空间。名称通过labeled_statement s 引入此声明空间,名称通过goto_statement引用。块的标签声明空间包括任何嵌套块。因此,在嵌套块中,不可能声明与封闭块中的标签具有相同名称的标签。
声明名称的文字顺序通常没有意义。特别是,文本顺序对于名称空间,常量,方法,属性,事件,索引器,运算符,实例构造函数,析构函数,静态构造函数和类型的声明和使用并不重要。声明顺序在以下方面很重要:
字段声明和局部变量声明的声明顺序决定了它们的初始值设定项(如果有)的执行顺序。
必须在使用局部变量之前定义它们(范围)。
枚举成员声明(枚举成员)的声明顺序在省略constant_expression值时很重要。
命名空间的声明空间是“开放式”,并且具有相同完全限定名称的两个命名空间声明对同一声明空间有贡献。例如
1 namespace Megacorp.Data 2 { 3 class Customer 4 { 5 ... 6 } 7 } 8 9 namespace Megacorp.Data 10 { 11 class Order 12 { 13 ... 14 } 15 }
以上两个命名空间声明提供相同的声明空间,在这种情况下,声明两个具有完全限定名Megacorp.Data.Customer
和Megacorp.Data.Order
。因为这两个声明对同一个声明空间有贡献,所以如果每个声明包含一个具有相同名称的类的声明,则会导致编译时错误。
如上所述,块的声明空间包括任何嵌套块。因此,在以下示例中,F
和G
方法导致编译时错误,因为名称i
在外部块中声明,并且无法在内部块中重新声明。但是,H
和I
方法是有效的,因为这两个方法是i
在单独的非嵌套块中声明的。
1 class A 2 { 3 void F() { 4 int i = 0; 5 if (true) { 6 int i = 1; 7 } 8 } 9 10 void G() { 11 if (true) { 12 int i = 0; 13 } 14 int i = 1; 15 } 16 17 void H() { 18 if (true) { 19 int i = 0; 20 } 21 if (true) { 22 int i = 1; 23 } 24 } 25 26 void I() { 27 for (int i = 0; i < 10; i++) 28 H(); 29 for (int i = 0; i < 10; i++) 30 H(); 31 } 32 }
成员
命名空间和类型具有成员。实体的成员通常可以通过使用以对实体的引用开头的限定名称,后跟“ .
”标记,后跟成员的名称来获得。
类型的成员要么在类型声明中声明,要么从类型的基类继承。当类型继承自基类时,基类的所有成员(实例构造函数,析构函数和静态构造函数除外)都将成为派生类型的成员。声明的基类成员可访问性不控制成员是否继承 - 继承扩展到任何不是实例构造函数,静态构造函数或析构函数的成员。但是,继承的成员可能无法在派生类型中访问,因为它声明了可访问性(声明的可访问性),或者因为它被类型本身中的声明隐藏(隐藏继承)。
命名空间成员
没有封闭命名空间的命名空间和类型是全局命名空间的成员。这直接对应于全局声明空间中声明的名称。
在命名空间内声明的命名空间和类型是该命名空间的成员。这直接对应于命名空间的声明空间中声明的名称。
命名空间没有访问限制。无法声明私有,受保护或内部命名空间,并且命名空间名称始终可公开访问。
结构成员
结构的成员是在结构中声明的成员,成员继承自结构的直接基类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
。
枚举成员
枚举的成员是在枚举中声明的常量从枚举的直接基类继承的成员System.Enum
和间接基类System.ValueType
和object
。
类成员
类的成员是在类中声明的成员,而成员是从基类继承的(除了object
没有基类的类)。从基类继承的成员包括基类的常量,字段,方法,属性,事件,索引器,运算符和类型,但不包括基类的实例构造函数,析构函数和静态构造函数。基类成员是继承的,不考虑它们的可访问性。
类声明可以包含常量,字段,方法,属性,事件,索引器,运算符,实例构造函数,析构函数,静态构造函数和类型的声明。
它们的成员object
和string
直接对应于它们别名的类类型的成员:
- 成员
object
是成员System.Object
类。 - 成员
string
是成员System.String
类。
接口成员
接口的成员是在接口和接口的所有基接口中声明的成员。object
严格来说,类中的成员不是任何接口的成员(接口成员)。但是,类object
中的成员可通过任何接口类型(成员查找)中的成员查找来使用。
数组成员
数组的成员是从类继承的成员System.Array
。
委托成员
委托的成员是从类继承的成员System.Delegate
。
成员访问权限
成员声明允许控制成员访问。成员的可访问性由成员的声明的可访问性(声明的可访问性)与立即包含的类型的可访问性(如果有)相结合来建立。
当允许访问特定成员时,该成员被称为可访问。相反,当不允许访问特定成员时,该成员被认为是不可访问的。当访问发生的文本位置包含在成员的可访问性域(可访问性域)中时,允许访问成员。
声明可访问性
该声明可访问成员可以是下列之一:
- Public,通过
public
在成员声明中包含修饰符来选择。直观的意思public
是“访问不受限制”。 - 受保护,通过
protected
在成员声明中包含修饰符来选择。直观的含义protected
是“访问仅限于包含类或从包含类派生的类型”。 - 内部,通过
internal
在成员声明中包含修饰符来选择。直观的含义internal
是“访问仅限于此程序”。 - 受保护的内部(表示受保护或内部),通过在成员声明中包含a
protected
和internal
修饰符来选择。直观的含义protected internal
是“访问仅限于此程序或从包含类派生的类型”。 - 私有,通过
private
在成员声明中包含修饰符来选择。直观的含义private
是“访问仅限于包含类型”。
根据成员声明发生的上下文,仅允许某些类型的声明可访问性。此外,当成员声明不包含任何访问修饰符时,发生声明的上下文确定默认声明的可访问性。
- 命名空间隐式
public
声明了可访问性。命名空间声明中不允许访问修饰符。 - 在编译单元或命名空间中声明的类型可以具有
public
或internal
声明可访问性,并且默认为internal
声明的可访问性。 - 类成员可以具有五种声明的可访问性中的任何一种,并且默认为
private
声明的可访问性。(请注意,声明为类成员的类型可以具有五种声明的可访问性中的任何一种,而声明为命名空间成员的类型只能具有public
或internal
声明可访问性。) - 结构成员可以具有
public
,internal
或private
声明可访问性,并且默认为private
声明的可访问性,因为结构是隐式密封的。在结构中引入的struct成员(即,不由该结构继承)不能具有protected
或protected internal
声明可访问性。(请注意,声明为结构成员的类型可以具有public
,internal
或private
声明可访问性,而声明为命名空间成员的类型只能具有public
或internal
声明可访问性。) - 接口成员隐式
public
声明了可访问性。接口成员声明中不允许访问修饰符。 - 枚举成员隐式
public
声明了可访问性。枚举成员声明中不允许访问修饰符。
可访问性域
成员的可访问性域由程序文本的(可能是不相交的)部分组成,其中允许访问该成员。为了定义成员的可访问性域,如果成员未在类型中声明,则称成员为顶级成员,如果成员在另一个类型中声明,则称成员为嵌套成员。此外,程序的程序文本被定义为程序的所有源文件中包含的所有程序文本,并且类型的程序文本被定义为该类型的type_declaration中包含的所有程序文本(可能包括类型)嵌套在类型中)。
预定义类型的可访问域(如object
,int
或double
)是无限的。
在程序中声明的顶级未绑定类型T
(绑定和未绑定类型)的可访问域P
定义如下:
- 如果声明的可访问性
T
是public
,则可访问域T
是程序文本P
和引用的任何程序P
。 - 如果声明的可访问性
T
是internal
,则可访问性域T
是程序文本P
。
从这些定义可以得出,顶级未绑定类型的可访问性域始终至少是声明该类型的程序的程序文本。
构造类型T<A1, ..., An>
的可访问性域是未绑定泛型类型T
的可访问域与类型参数的可访问域的交集A1, ..., An
。
在程序M
中的类型中声明的嵌套成员的可访问域定义如下(注意它本身可能是一个类型):T
P
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
声明的类派生的类中,并通过派生类类型发生(实例成员的受保护访问) 。 - 否则,如果
M
是protected
,则允许访问,如果它发生在M
声明的类中,或者它发生在从M
声明的类派生的类中,并通过派生类类型发生(实例成员的受保护访问) 。 - 否则,如果
M
是internal
,则允许访问,如果它发生在M
声明的程序中。 - 否则,如果
M
是private
,则允许访问,如果它发生在M
声明的类型中。 - 否则,类型或成员不可访问,并发生编译时错误。
在这个例子中
1 public class A 2 { 3 public static int X; 4 internal static int Y; 5 private static int Z; 6 } 7 8 internal class B 9 { 10 public static int X; 11 internal static int Y; 12 private static int Z; 13 14 public class C 15 { 16 public static int X; 17 internal static int Y; 18 private static int Z; 19 } 20 21 private class D 22 { 23 public static int X; 24 internal static int Y; 25 private static int Z; 26 } 27 }
类和成员具有以下可访问性域:
- 可访问性域
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
受包含类型约束的可访问性域。
如Members中所述,基类的所有成员(实例构造函数,析构函数和静态构造函数除外)都由派生类型继承。这甚至包括基类的私有成员。但是,私有成员的可访问域仅包括声明成员的类型的程序文本。在这个例子中
1 class A 2 { 3 int x; 4 5 static void F(B b) { 6 b.x = 1; // Ok 7 } 8 } 9 10 class B: A 11 { 12 static void F(B b) { 13 b.x = 1; // Error, x not accessible 14 } 15 }
在B
类继承的私有成员x
从A
类。因为该成员是私有的,所以只能在class_body中访问A
。因此,在方法中访问b.x
成功A.F
,但在B.F
方法中失败。
实例成员的受保护访问权限
当protected
在声明它的类protected internal
的程序文本之外访问实例成员时,并且当在声明它的程序的程序文本之外访问实例成员时,访问必须在派生的类声明中进行。来自声明它的类。此外,访问需要通过该派生类类型的实例或从其构造的类类型进行。此限制可防止一个派生类访问其他派生类的受保护成员,即使这些成员是从同一基类继承的。
让B
是一个基类,声明了一个受保护的实例成员M
,并让D
是从派生的类B
。在class_body中D
,访问权限M
可以采用以下形式之一:
- 表单的非限定type_name或primary_expression
M
。 - 表单的primary_expression
E.M
,提供E
is 的类型T
或派生自的类T
,其中T
是类类型D
,或者是由类型构造的类类型D
- 表单的primary_expression
base.M
。
除了这些访问形式之外,派生类还可以在constructor_initializer(Constructor initializers)中访问基类的受保护实例构造函数。
在这个例子中
1 public class A 2 { 3 protected int x; 4 5 static void F(A a, B b) { 6 a.x = 1; // Ok 7 b.x = 1; // Ok 8 } 9 } 10 11 public class B: A 12 { 13 static void F(A a, B b) { 14 a.x = 1; // Error, must access through instance of B 15 b.x = 1; // Ok 16 } 17 }
内A
,能够访问x
通过两个实例A
和B
,因为在这两种情况下所述接入经过的实例发生A
或从派生的类A
。但是,在内部B
,不可能x
通过实例访问A
,因为A
不是从中派生的B
。
在这个例子中
1 class C<T> 2 { 3 protected T x; 4 } 5 6 class D<T>: C<T> 7 { 8 static void F() { 9 D<T> dt = new D<T>(); 10 D<int> di = new D<int>(); 11 D<string> ds = new D<string>(); 12 dt.x = default(T); 13 di.x = 123; 14 ds.x = "test"; 15 } 16 }
x
允许三个赋值,因为它们都是通过从泛型类型构造的类类型的实例发生的。
可访问性限制
C#语言中的几个结构要求类型至少与成员或其他类型一样可访问。如果可访问域是可访问域的超集,T
则称该类型至少与成员或类型一样可访问。换句话说,至少是可访问的,如果是在所有上下文中可以访问访问。M
T
M
T
M
T
M
存在以下可访问性约束:
- 类类型的直接基类必须至少与类类型本身一样可访问。
- 接口类型的显式基接口必须至少与接口类型本身一样可访问。
- 委托类型的返回类型和参数类型必须至少与委托类型本身一样可访问。
- 常量的类型必须至少与常量本身一样可访问。
- 字段的类型必须至少与字段本身一样可访问。
- 方法的返回类型和参数类型必须至少与方法本身一样可访问。
- 属性的类型必须至少与属性本身一样可访问。
- 事件的类型必须至少与事件本身一样可访问。
- 索引器的类型和参数类型必须至少与索引器本身一样可访问。
- 运算符的返回类型和参数类型必须至少与运算符本身一样可访问。
- 实例构造函数的参数类型必须至少与实例构造函数本身一样可访问。
在这个例子中
1 class A {...} 2 3 public class B: A {...}
在B
类导致编译时错误,因为A
没有至少可访问B
。
同样,在示例中
1 class A {...} 2 3 public class B 4 { 5 A F() {...} 6 7 internal A G() {...} 8 9 public A H() {...} 10 }
签名和重载
方法,实例构造函数,索引器和运算符的特征在于它们的签名:
- 方法的签名包括方法的名称,类型参数的数量以及每个形式参数的类型和种类(值,引用或输出),按从左到右的顺序考虑。出于这些目的,在形式参数类型中出现的方法的任何类型参数不是通过其名称来标识,而是通过其在方法的类型参数列表中的序号位置来标识。方法的签名特别不包括返回类型,
params
可以为最右边的参数指定的修饰符,也不包括可选的类型参数约束。 - 实例构造函数的签名由每个形式参数的类型和种类(值,引用或输出)组成,按从左到右的顺序考虑。实例构造函数的签名特别不包括
params
可以为最右边的参数指定的修饰符。 - 索引器的签名由每个形式参数的类型组成,按从左到右的顺序考虑。索引器的签名具体不包括元素类型,也不包括
params
可能为最右侧参数指定的修饰符。 - 运算符的签名由运算符的名称和每个形式参数的类型组成,按从左到右的顺序考虑。运算符的签名特别不包括结果类型。
签名是在类,结构和接口中重载成员的启用机制:
- 方法的重载允许类,结构或接口声明具有相同名称的多个方法,前提是它们的签名在该类,结构或接口中是唯一的。
- 实例构造函数的重载允许类或结构声明多个实例构造函数,前提是它们的签名在该类或结构中是唯一的。
- 索引器的重载允许类,结构或接口声明多个索引器,前提是它们的签名在该类,结构或接口中是唯一的。
- 运算符的重载允许类或结构声明具有相同名称的多个运算符,前提是它们的签名在该类或结构中是唯一的。
虽然out
和ref
参数修饰符被认为是签名的一部分,但是在单一类型中声明的成员在签名上不能仅仅由ref
和表示不同out
。如果在具有out
修饰符的两个方法中的所有参数都更改为ref
修饰符的情况下,如果两个成员在同一类型中声明具有相同签名的成员,则会发生编译时错误。用于签名匹配的其他目的(例如,隐藏或覆盖),ref
并且out
被认为是签名的一部分并且彼此不匹配。(此限制是为了允许C#程序轻松转换为在公共语言基础结构(CLI)上运行,这不提供一种方法来定义仅在ref
和中有区别的方法out
。)
出于签名的目的,类型object
和dynamic
被认为是相同的。因此,在单一类型中声明的成员只能通过object
和签名dynamic
。
以下示例显示了一组重载的方法声明及其签名。
1 interface ITest 2 { 3 void F(); // F() 4 5 void F(int x); // F(int) 6 7 void F(ref int x); // F(ref int) 8 9 void F(out int x); // F(out int) error 10 11 void F(int x, int y); // F(int, int) 12 13 int F(string s); // F(string) 14 15 int F(int x); // F(int) error 16 17 void F(string[] a); // F(string[]) 18 19 void F(params string[] a); // F(string[]) error 20 }
请注意,任何ref
和out
参数修饰符(方法参数)都是签名的一部分。因此,F(int)
并且F(ref int)
是独特的签名。但是,F(ref int)
并且F(out int)
不能在同一个界面中声明,因为它们的签名完全不同于ref
和out
。另请注意,返回类型和params
修饰符不是签名的一部分,因此不可能仅基于返回类型或包含或排除params
修饰符来重载。因此,所述方法的声明F(int)
和F(params string[])
识别上述结果在一个编译时间错误。
范围
名称的范围是程序文本的区域,在该区域内可以引用名称声明的实体而无需限定名称。范围可以嵌套,内部范围可以从外部范围重新声明名称的含义(但是,这不会消除由声明强加的限制,在嵌套块中,不可能声明具有相同的局部变量name作为封闭块中的局部变量)。然后,外部作用域中的名称被隐藏在内部作用域所覆盖的程序文本区域中,只有通过限定名称才能访问外部名称。
- 由namespace_member_declaration(Namespace成员)声明而没有封闭namespace_declaration的名称空间成员的范围是整个程序文本。
- 通过声明的命名空间成员的范围namespace_member_declaration一个内namespace_declaration,其全名是
N
是namespace_body每一个的namespace_declaration,其全名是N
或开头N
,后跟一个句点。 - 由extern_alias_directive定义的名称范围扩展到其直接包含的编译单元或命名空间体的using_directive,global_attributes和namespace_member_declaration。一个extern_alias_directive没有任何新成员的基础声明空间。换句话说,extern_alias_directive不是传递性的,而是仅影响它出现的编译单元或命名空间体。
- 通过定义的或导入的名称的范围using_directive(使用指令)在延伸namespace_member_declaration的第compilation_unit或namespace_body其中using_directive发生。一个using_directive可能使特定的范围内可用的零个或多个命名空间,类型或成员名称compilation_unit或namespace_body,但没有任何新成员,以基础声明空间。换句话说,using_directive不是可传递的,而只影响compilation_unit或namespace_body 它发生的地方。
- 通过声明的类型参数的范围type_parameter_list上的class_declaration(类声明)是class_base,type_parameter_constraints_clause s和class_body那的class_declaration。
- 通过声明的类型参数的范围type_parameter_list上的struct_declaration(struct声明)是struct_interfaces,type_parameter_constraints_clause s和struct_body那的struct_declaration。
- 通过声明的类型参数的范围type_parameter_list上interface_declaration(接口声明)是interface_base,type_parameter_constraints_clause s和interface_body那的interface_declaration。
- 通过声明的类型参数的范围type_parameter_list上的delegate_declaration(委托声明)是return_type,formal_parameter_list,和type_parameter_constraints_clause该第delegate_declaration。
- 通过声明的成员的范围class_member_declaration(类体)是class_body在该声明所在。此外,类成员的范围扩展到成员的可访问性域(可访问性域)中包含的派生类的class_body。
- 通过声明的成员的范围struct_member_declaration(结构成员)是struct_body在该声明所在。
- 通过声明的成员的范围enum_member_declaration (枚举成员)是enum_body在该声明所在。
- method_declaration(Methods)中声明的参数范围是该method_declaration的method_body。
- 在声明的参数的范围indexer_declaration(索引器)是accessor_declarations那的indexer_declaration。
- 在声明的参数的范围operator_declaration(算)是块那个的operator_declaration。
- 在声明的参数的范围constructor_declaration(实例构造)是constructor_initializer和块那的constructor_declaration。
- 在声明的参数的范围lambda_expression(匿名函数表达式)是anonymous_function_body那的lambda_expression
- 在声明的参数的范围anonymous_method_expression(匿名函数表达式)是块那个的anonymous_method_expression。
- 在labeled_statement(Labeled语句)中声明的标签的范围是声明发生的块。
- 在local_variable_declaration(局部变量声明)中声明的局部变量的范围是声明发生的块。
- 在声明的局部变量的范围switch_block一的
switch
声明(switch语句)是switch_block。 - 在语句的for_initializer中
for
声明的局部变量的范围(for语句)是for_initializer,for_condition,for_iterator和语句的包含for
语句。 - 在local_constant_declaration(局部常量声明)中声明的局部常量的范围是声明发生的块。在constant_declarator之前的文本位置引用局部常量是编译时错误。
- 声明为foreach_statement,using_statement,lock_statement或query_expression的一部分的变量的范围由给定构造的扩展决定。
在命名空间,类,结构或枚举成员的范围内,可以在成员声明之前的文本位置引用成员。例如
1 class A 2 { 3 void F() { 4 i = 1; 5 } 6 7 int i = 0; 8 }
这里,在声明之前F
引用它是有效的i
。
在局部变量的范围内,在局部变量的local_variable_declarator之前的文本位置引用局部变量是编译时错误。例如
1 class A 2 { 3 int i = 0; 4 5 void F() { 6 i = 1; // Error, use precedes declaration 7 int i; 8 i = 2; 9 } 10 11 void G() { 12 int j = (j = 1); // Valid 13 } 14 15 void H() { 16 int a = 1, b = ++a; // Valid 17 } 18 }
在F
上面的方法中,第一个赋值i
具体不引用外部作用域中声明的字段。相反,它引用局部变量并导致编译时错误,因为它在文本上先于变量的声明。在该G
方法中,j
在初始化程序中使用声明j
是有效的,因为使用不在local_variable_declarator之前。在该H
方法中,后续的local_variable_declarator正确引用在同一local_variable_declaration中的早期local_variable_declarator中声明的局部变量。
局部变量的作用域规则旨在保证表达式上下文中使用的名称的含义在块内始终相同。如果局部变量的范围仅从其声明扩展到块的末尾,那么在上面的示例中,第一个赋值将分配给实例变量,第二个赋值将分配给局部变量,可能导致如果稍后重新排列块的语句,则编译时错误。
块中名称的含义可能根据使用名称的上下文而有所不同。在这个例子中
1 using System; 2 3 class A {} 4 5 class Test 6 { 7 static void Main() { 8 string A = "hello, world"; 9 string s = A; // expression context 10 11 Type t = typeof(A); // type context 12 13 Console.WriteLine(s); // writes "hello, world" 14 Console.WriteLine(t); // writes "A" 15 } 16 }
该名称A
在表达式上下文中用于引用局部变量A
,在类型上下文中用于引用该类A
。
实体隐藏
实体的范围通常包含比实体的声明空间更多的程序文本。特别是,实体的范围可能包括引入包含同名实体的新声明空间的声明。此类声明会导致原始实体隐藏。相反,当一个实体没有被隐藏时,它被认为是可见的。
当范围通过嵌套重叠并且范围通过继承重叠时,会发生名称隐藏。以下各节描述了两种隐藏的特征。
嵌套隐藏
由于在类或结构中嵌套类型以及作为参数和局部变量声明的结果,在命名空间内嵌套命名空间或类型,可能会发生通过嵌套隐藏的名称。
在这个例子中
1 class A 2 { 3 int i = 0; 4 5 void F() { 6 int i = 1; 7 } 8 9 void G() { 10 i = 1; 11 } 12 }
在F
方法中,实例变量i
由局部变量隐藏i
,但在G
方法内i
仍然引用实例变量。
当内部作用域中的名称隐藏外部作用域中的名称时,它会隐藏该名称的所有重载出现。在这个例子中
1 class Outer 2 { 3 static void F(int i) {} 4 5 static void F(string s) {} 6 7 class Inner 8 { 9 void G() { 10 F(1); // Invokes Outer.Inner.F 11 F("Hello"); // Error 12 } 13 14 static void F(long l) {} 15 } 16 }
该调用F(1)
调用F
声明的in,Inner
因为所有外部事件F
都被内部声明隐藏。出于同样的原因,调用会F("Hello")
导致编译时错误。
继承隐藏
当类或结构重新声明从基类继承的名称时,会发生通过继承隐藏的名称。此类名称隐藏采用以下形式之一:
- 类或结构中引入的常量,字段,属性,事件或类型会隐藏具有相同名称的所有基类成员。
- 类或结构中引入的方法隐藏所有具有相同名称的非方法基类成员,以及具有相同签名的所有基类方法(方法名称和参数计数,修饰符和类型)。
- 在类或结构中引入的索引器隐藏具有相同签名(参数计数和类型)的所有基类索引器。
管理运算符声明(运算符)的规则使派生类无法声明与基类中的运算符具有相同签名的运算符。因此,运营商永远不会互相隐瞒。
与从外部作用域隐藏名称相反,从继承的作用域隐藏可访问的名称会导致报告警告。在这个例子中
1 class Base 2 { 3 public void F() {} 4 } 5 6 class Derived: Base 7 { 8 public void F() {} // Warning, hiding an inherited name 9 }
F
in 的声明Derived
导致报告警告。隐藏继承的名称特别不是错误,因为这将排除基类的单独演变。例如,上面的情况可能是因为Base
引入了F
一个在该类的早期版本中不存在的方法的更高版本。如果上述情况是错误,那么对单独版本化的类库中的基类所做的任何更改都可能导致派生类变为无效。
隐藏继承名称引起的警告可以通过使用new
修饰符来消除:
1 class Base 2 { 3 public void F() {} 4 } 5 6 class Derived: Base 7 { 8 new public void F() {} 9 }
该new
修饰符表明F
在Derived
为“新”,而且它确实是有意隐藏继承成员。
新成员的声明仅在新成员的范围内隐藏继承的成员。
1 class Base 2 { 3 public static void F() {} 4 } 5 6 class Derived: Base 7 { 8 new private static void F() {} // Hides Base.F in Derived only 9 } 10 11 class MoreDerived: Derived 12 { 13 static void G() { F(); } // Invokes Base.F 14 }
在上面的例子中,在声明F
中Derived
隐藏了F
一个从继承Base
,但由于新F
的Derived
具有私有访问,它的范围不会延伸到MoreDerived
。因此,呼叫F()
中MoreDerived.G
是有效的,将调用Base.F
。
命名空间和类型名称
C#程序中的几个上下文需要指定namespace_name或type_name。
1 namespace_name 2 : namespace_or_type_name 3 ; 4 5 type_name 6 : namespace_or_type_name 7 ; 8 9 namespace_or_type_name 10 : identifier type_argument_list? 11 | namespace_or_type_name '.' identifier type_argument_list? 12 | qualified_alias_member 13 ;
一个namespace_name是namespace_or_type_name是指一个命名空间。按照下面所述的解决方案,namespace_name的namespace_or_type_name必须引用命名空间,否则会发生编译时错误。namespace_name中不能存在任何类型参数(类型参数)(只有类型可以具有类型参数)。
甲TYPE_NAME是namespace_or_type_name其是指一类。根据如下所述的分辨率,所述namespace_or_type_name一个的TYPE_NAME必须引用一个类型,或以其它方式编译时会出现误差。
如果namespace_or_type_name是qualified-alias-member,则其含义与Namespace别名限定符中所述相同。否则,namespace_or_type_name具有以下四种形式之一:
I
I<A1, ..., Ak>
N.I
N.I<A1, ..., Ak>
where 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出现在泛型方法声明(方法)中,并且该声明包含带名称的类型参数(类型参数)I
,则namespace_or_type_name引用该类型参数。 - 否则,如果namespace_or_type_name出现在类型声明中,则对于每个实例类型
T
(实例类型),从该类型声明的实例类型开始,并继续每个封闭类或结构声明的实例类型(如果有):- 如果
K
为零且声明T
包含带有name的类型参数I
,则namespace_or_type_name引用该类型参数。 - 否则,如果namespace_or_type_name出现在类型声明的主体内,
T
或者其任何基类型包含具有nameI
和K
type参数的嵌套可访问类型,则namespace_or_type_name指的是使用给定类型参数构造的类型。如果存在多个此类型,则选择在更多派生类型中声明的类型。请注意,在确定namespace_or_type_name的含义时,将忽略具有不同类型参数数的非类型成员(常量,字段,方法,属性,索引器,运算符,实例构造函数,析构函数和静态构造函数)和类型成员。
- 如果
- 如果之前的步骤不成功,则对于每个命名空间
N
,从发生namespace_or_type_name的命名空间开始,继续使用每个封闭的命名空间(如果有),并以全局命名空间结束,将评估以下步骤,直到找到实体:- 如果
K
为零并且I
是名称空间的名称N
,则:- 如果发生namespace_or_type_name的位置由名称空间声明括起来,
N
并且名称空间声明包含将名称与名称空间或类型相关联的extern_alias_directive或using_alias_directiveI
,则namespace_or_type_name不明确并且发生编译时错误。 - 否则,namespace_or_type_name指命名的命名空间
I
在N
。
- 如果发生namespace_or_type_name的位置由名称空间声明括起来,
- 否则,如果
N
包含具有名称I
和K
类型参数的可访问类型,则:- 如果
K
为零且namespace_or_type_name出现的位置由名称空间声明括起,N
并且名称空间声明包含将名称与名称空间或类型相关联的extern_alias_directive或using_alias_directiveI
,则namespace_or_type_name不明确并且发生编译时错误。 - 否则,namespace_or_type_name引用使用给定类型参数构造的类型。
- 如果
- 否则,如果发生namespace_or_type_name的位置由名称空间声明括起
N
:- 如果
K
为零且名称空间声明包含将名称与导入的名称空间或类型相关联的extern_alias_directive或using_alias_directiveI
,则namespace_or_type_name引用该名称空间或类型。 - 否则,如果命名空间声明的using_namespace_directive和using_alias_directive导入的命名空间和类型声明只包含一个具有name
I
和K
type参数的可访问类型,则namespace_or_type_name指的是使用给定类型参数构造的类型。 - 否则,如果由名称空间声明的using_namespace_directive和using_alias_directive s 导入的名称空间和类型声明包含多个具有名称
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
被认为是对象(基类)。 - 否则,namespace_or_type_name
N.I
无效,并发生编译时错误。
- 如果
仅当namespace_or_type_name允许引用静态类(静态类)时才允许
- 该namespace_or_type_name是
T
在一个namespace_or_type_name形式的T.I
,或 - 该namespace_or_type_name是
T
在typeof_expression(参数列表的形式的1)typeof(T)
。
完全限定名称
每个名称空间和类型都有一个完全限定的名称,该名称唯一地标识所有其他名称空间或类型。命名空间或类型的完全限定名称N
确定如下:
- 如果
N
是全局命名空间的成员,则其完全限定名称为N
。 - 否则,其完全限定名称是
S.N
,声明S
的名称空间或类型的完全限定名称N
。
换句话说,完全限定名称N
是N
从全局名称空间开始的标识符的完整分层路径。因为命名空间或类型的每个成员都必须具有唯一的名称,所以命名空间或类型的完全限定名称始终是唯一的。
下面的示例显示了几个名称空间和类型声明及其关联的完全限定名称。
1 class A {} // A 2 3 namespace X // X 4 { 5 class B // X.B 6 { 7 class C {} // X.B.C 8 } 9 10 namespace Y // X.Y 11 { 12 class D {} // X.Y.D 13 } 14 } 15 16 namespace X.Y // X.Y 17 { 18 class E {} // X.Y.E 19 }
自动内存管理
C#采用自动内存管理,使开发人员无需手动分配和释放对象占用的内存。自动内存管理策略由垃圾收集器实现。对象的内存管理生命周期如下:
- 创建对象时,将为其分配内存,运行构造函数,并将对象视为实时对象。
- 如果除了运行析构函数之外,任何可能的继续执行都无法访问该对象或其任何部分,则该对象将被视为不再使用,并且它有资格进行销毁。C#编译器和垃圾收集器可以选择分析代码以确定将来可以使用对对象的哪些引用。例如,如果范围内的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中从不引用该局部变量,则垃圾收集器可能(但是不要求将对象视为不再使用。
- 一旦该对象有资格进行销毁,稍后在某些未指定的时间运行该对象的析构函数(Destructors)(如果有的话)。在正常情况下,对象的析构函数仅运行一次,尽管特定于实现的API可能允许覆盖此行为。
- 一旦运行了对象的析构函数,如果该对象或其任何部分无法通过任何可能的执行继续访问,包括运行析构函数,则该对象被视为不可访问,并且该对象符合收集条件。
- 最后,在对象符合收集条件后的某个时间,垃圾收集器释放与该对象关联的内存。
垃圾收集器维护有关对象使用的信息,并使用此信息来做出内存管理决策,例如在内存中查找新创建的对象的位置,何时重定位对象,以及何时对象不再使用或不可访问。
与假设存在垃圾收集器的其他语言一样,C#的设计使得垃圾收集器可以实现各种内存管理策略。例如,C#不要求运行析构函数,或者只要符合条件就收集对象,或者以任何特定顺序或任何特定线程运行析构函数。
可以通过类上的静态方法在一定程度上控制垃圾收集器的行为System.GC
。此类可用于请求集合发生,析构函数运行(或不运行)等。
由于垃圾收集器在决定何时收集对象和运行析构函数时允许宽范围,因此符合实现可能产生与以下代码所示的输出不同的输出。该程序
1 using System; 2 3 class A 4 { 5 ~A() { 6 Console.WriteLine("Destruct instance of A"); 7 } 8 } 9 10 class B 11 { 12 object Ref; 13 14 public B(object o) { 15 Ref = o; 16 } 17 18 ~B() { 19 Console.WriteLine("Destruct instance of B"); 20 } 21 } 22 23 class Test 24 { 25 static void Main() { 26 B b = new B(new A()); 27 b = null; 28 GC.Collect(); 29 GC.WaitForPendingFinalizers(); 30 } 31 }
创建类A
的实例和类的实例B
。当为变量b
赋值时,这些对象有资格进行垃圾收集null
,因为在此之后,任何用户编写的代码都无法访问它们。输出可以是
1 Destruct instance of A 2 Destruct instance of B
要么
1 Destruct instance of B 2 Destruct instance of A
因为该语言对垃圾收集对象的顺序没有任何限制。
在微妙的情况下,“有资格获得销毁”和“有资格获得收集”之间的区别可能很重要。例如,
1 using System; 2 3 class A 4 { 5 ~A() { 6 Console.WriteLine("Destruct instance of A"); 7 } 8 9 public void F() { 10 Console.WriteLine("A.F"); 11 Test.RefA = this; 12 } 13 } 14 15 class B 16 { 17 public A Ref; 18 19 ~B() { 20 Console.WriteLine("Destruct instance of B"); 21 Ref.F(); 22 } 23 } 24 25 class Test 26 { 27 public static A RefA; 28 public static B RefB; 29 30 static void Main() { 31 RefB = new B(); 32 RefA = new A(); 33 RefB.Ref = RefA; 34 RefB = null; 35 RefA = null; 36 37 // A and B now eligible for destruction 38 GC.Collect(); 39 GC.WaitForPendingFinalizers(); 40 41 // B now eligible for collection, but A is not 42 if (RefA != null) 43 Console.WriteLine("RefA is not null"); 44 } 45 }
在上面的程序中,如果垃圾收集器选择在析构函数A
之前运行析构函数B
,那么该程序的输出可能是:
1 Destruct instance of A 2 Destruct instance of B 3 A.F 4 RefA is not null
请注意,虽然A
没有使用实例并且A
运行了析构函数,但是仍然可以从另一个析构函数中调用A
(在这种情况下F
)的方法。另请注意,运行析构函数可能会导致对象再次从主线程序中变为可用。在这种情况下,B
析构函数的运行导致A
之前未使用的实例可以从实时引用中访问Test.RefA
。调用之后WaitForPendingFinalizers
,实例B
有资格进行收集,但实例A
不是,因为引用Test.RefA
。
为了避免混淆和意外行为,析构函数通常只对存储在其对象自己的字段中的数据执行清理,而不对引用的对象或静态字段执行任何操作。
使用析构函数的另一种方法是让类实现System.IDisposable
接口。这允许对象的客户端通常通过将对象作为using
语句(using语句)中的资源来访问来确定何时释放对象的资源。
执行顺序
执行C#程序,使得每个执行线程的副作用在关键执行点处得以保留。甲副作用被定义为挥发性字段的读或写,以非易失性可变的写入,到外部资源的写入,和一个异常的投掷。必须保留这些副作用的顺序的关键执行点是对易失性字段(易失性字段),lock
语句(锁定语句)以及线程创建和终止的引用。执行环境可以自由更改C#程序的执行顺序,但受以下限制:
- 数据依赖性保留在执行的线程中。也就是说,计算每个变量的值,就好像线程中的所有语句都以原始程序顺序执行一样。
- 保留初始化排序规则(字段初始化和变量初始化程序)。
- 关于易失性读取和写入(易失性字段),保留了副作用的顺序。此外,执行环境不需要评估表达式的一部分,如果它可以推断出该表达式的值未被使用并且不产生所需的副作用(包括由调用方法或访问volatile字段引起的任何副作用)。当程序执行被异步事件(例如另一个线程抛出的异常)中断时,不能保证可观察的副作用在原始程序顺序中可见。