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.CustomerMegacorp.Data.Order因为这两个声明对同一个声明空间有贡献,所以如果每个声明包含一个具有相同名称的类的声明,则会导致编译时错误。

如上所述,块的声明空间包括任何嵌套块。因此,在以下示例中,FG方法导致编译时错误,因为名称i在外部块中声明,并且无法在内部块中重新声明。但是,HI方法是有效的,因为这两个方法是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.ValueTypeobject

类成员

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

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

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

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

接口成员

接口的成员是在接口和接口的所有基接口中声明的成员。object严格来说,的成员不是任何接口的成员接口成员)。但是,类object的成员可通过任何接口类型(成员查找)中的成员查找来使用

数组成员

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

委托成员

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

成员访问权限

成员声明允许控制成员访问。成员的可访问性由成员的声明的可访问性(声明的可访问性)与立即包含的类型的可访问性(如果有)相结合来建立。

当允许访问特定成员时,该成员被称为可访问相反,当不允许访问特定成员时,该成员被认为是不可访问的当访问发生的文本位置包含在成员的可访问性域(访问性域)中时,允许访问成员。

声明可访问性

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

  • Public,通过public在成员声明中包含修饰符来选择直观的意思public是“访问不受限制”。
  • 受保护,通过protected在成员声明中包含修饰符来选择直观的含义protected是“访问仅限于包含类或从包含类派生的类型”。
  • 内部,通过internal在成员声明中包含修饰符来选择直观的含义internal是“访问仅限于此程序”。
  • 受保护的内部(表示受保护或内部),通过在成员声明中包含a protectedinternal修饰符来选择直观的含义protected internal是“访问仅限于此程序或从包含类派生的类型”。
  • 私有,通过private在成员声明中包含修饰符来选择直观的含义private是“访问仅限于包含类型”。

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

  • 命名空间隐式public声明了可访问性。命名空间声明中不允许访问修饰符。
  • 在编译单元或命名空间中声明的类型可以具有publicinternal声明可访问性,并且默认为internal声明的可访问性。
  • 类成员可以具有五种声明的可访问性中的任何一种,并且默认为private声明的可访问性。(请注意,声明为类成员的类型可以具有五种声明的可访问性中的任何一种,而声明为命名空间成员的类型只能具有publicinternal声明可访问性。)
  • 结构成员可以具有publicinternalprivate声明可访问性,并且默认为private声明的可访问性,因为结构是隐式密封的。在结构中引入的struct成员(即,不由该结构继承)不能具有protectedprotected internal声明可访问性。(请注意,声明为结构成员的类型可以具有publicinternalprivate声明可访问性,而声明为命名空间成员的类型只能具有publicinternal声明可访问性。)
  • 接口成员隐式public声明了可访问性。接口成员声明中不允许访问修饰符。
  • 枚举成员隐式public声明了可访问性。枚举成员声明中不允许访问修饰符。

可访问性域

成员可访问性域由程序文本的(可能是不相交的)部分组成,其中允许访问该成员。为了定义成员的可访问性域,如果成员未在类型中声明,则称成员为顶级成员,如果成员在另一个类型中声明,则称成员为嵌套成员此外,程序的程序文本被定义为程序的所有源文件中包含的所有程序文本,并且类型的程序文本被定义为该类型type_declaration中包含的所有程序文本(可能包括类型)嵌套在类型中)。

预定义类型的可访问域(如objectintdouble)是无限的。

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

  • 如果声明的可访问性Tpublic,则可访问域T是程序文本P和引用的任何程序P
  • 如果声明的可访问性Tinternal,则可访问性域T是程序文本P

从这些定义可以得出,顶级未绑定类型的可访问性域始终至少是声明该类型的程序的程序文本。

构造类型T<A1, ..., An>的可访问性域是未绑定泛型类型T的可访问域与类型参数的可访问域的交集A1, ..., An

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

  • 如果声明的可访问性Mpublic,则可访问性域M是可访问性域T
  • 如果声明的可访问性Mprotected internal,则允许D程序文本P和从中派生的任何类型的程序文本的联合T,这是在外部声明的P的可访问域M是可访问域的交集TD
  • 如果声明的可访问性MprotectedD则将程序文本T和任何类型的程序文本的联合派生出来T的可访问域M是可访问域的交集TD
  • 如果声明的可访问性Minternal,则可M访问域是T与程序文本的可访问域的交集P
  • 如果声明的可访问性Mprivate,则可访问性域M是程序文本T

从这些定义可以得出,嵌套成员的可访问域始终至少是声明成员的类型的程序文本。此外,成员的可访问域永远不会比声明成员的类型的可访问域更具包容性。

直观地说,当M访问类型或成员时,将评估以下步骤以确保允许访问:

  • 首先,如果M在类型中声明(与编译单元或命名空间相对),则在无法访问该类型时会发生编译时错误。
  • 然后,如果Mpublic,则允许访问。
  • 否则,如果Mprotected internal,则允许访问,如果它发生在M声明的程序中,或者它发生在从M声明的类派生的类中,并通过派生类类型发生(实例成员的受保护访问) 。
  • 否则,如果Mprotected,则允许访问,如果它发生在M声明的类中,或者它发生在从M声明的类派生的类中,并通过派生类类型发生(实例成员的受保护访问) 。
  • 否则,如果Minternal,则允许访问,如果它发生在M声明的程序中
  • 否则,如果Mprivate,则允许访问,如果它发生在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 }

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

  • 可访问性域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成员都具有公开声明的可访问性,但所有成员都具有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类继承的私有成员xA类。因为该成员是私有的,所以只能在class_body访问A因此,在方法中访问b.x成功A.F,但在B.F方法中失败

实例成员的受保护访问权限

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

B是一个基类,声明了一个受保护的实例成员M,并让D是从派生的类Bclass_bodyD,访问权限M可以采用以下形式之一:

  • 表单的非限定type_nameprimary_expressionM
  • 表单primary_expressionE.M,提供Eis 的类型T或派生自的类T,其中T是类类型D,或者是由类型构造的类类型D
  • 表单primary_expressionbase.M

除了这些访问形式之外,派生类还可以在constructor_initializerConstructor 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通过两个实例AB,因为在这两种情况下所述接入经过的实例发生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则称该类型至少与成员或类型一样可访问换句话说,至少是可访问的,如果是在所有上下文中可以访问访问。MTMTMTM

存在以下可访问性约束:

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

在这个例子中

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可能为最右侧参数指定修饰符。
  • 运算符的签名由运算符的名称和每个形式参数的类型组成,按从左到右的顺序考虑。运算符的签名特别不包括结果类型。

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

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

虽然outref参数修饰符被认为是签名的一部分,但是在单一类型中声明的成员在签名上不能仅仅由ref和表示不同out如果在具有out修饰符的两个方法中的所有参数都更改为ref修饰符的情况下,如果两个成员在同一类型中声明具有相同签名的成员,则会发生编译时错误用于签名匹配的其他目的(例如,隐藏或覆盖),ref并且out被认为是签名的一部分并且彼此不匹配。(此限制是为了允许C#程序轻松转换为在公共语言基础结构(CLI)上运行,这不提供一种方法来定义仅在ref中有区别的方法out。)

出于签名的目的,类型objectdynamic被认为是相同的。因此,在单一类型中声明的成员只能通过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 }

请注意,任何refout参数修饰符(方法参数)都是签名的一部分。因此,F(int)并且F(ref int)是独特的签名。但是,F(ref int)并且F(out int)不能在同一个界面中声明,因为它们的签名完全不同于refout另请注意,返回类型和params修饰符不是签名的一部分,因此不可能仅基于返回类型或包含或排除params修饰符来重载因此,所述方法的声明F(int)F(params string[])识别上述结果在一个编译时间错误。

范围

名称范围是程序文本的区域,在该区域内可以引用名称声明的实体而无需限定名称。范围可以嵌套,内部范围可以从外部范围重新声明名称的含义(但是,这不会消除由声明强加的限制,在嵌套块中,不可能声明具有相同的局部变量name作为封闭块中的局部变量)。然后,外部作用域中的名称被隐藏在内部作用域所覆盖的程序文本区域中,只有通过限定名称才能访问外部名称。

  • namespace_member_declarationNamespace成员)声明而没有封闭namespace_declaration的名称空间成员的范围是整个程序文本。
  • 通过声明的命名空间成员的范围namespace_member_declaration一个内namespace_declaration,其全名是Nnamespace_body每一个的namespace_declaration,其全名是N或开头N,后跟一个句点。
  • extern_alias_directive定义的名称范围扩展到其直接包含的编译单元或命名空间体using_directiveglobal_attributesnamespace_member_declaration一个extern_alias_directive没有任何新成员的基础声明空间。换句话说,extern_alias_directive不是传递性的,而是仅影响它出现的编译单元或命名空间体。
  • 通过定义的或导入的名称的范围using_directive使用指令)在延伸namespace_member_declaration的第compilation_unitnamespace_body其中using_directive发生。一个using_directive可能使特定的范围内可用的零个或多个命名空间,类型或成员名称compilation_unitnamespace_body,但没有任何新成员,以基础声明空间。换句话说,using_directive不是可传递的,而只影响compilation_unitnamespace_body 它发生的地方。
  • 通过声明的类型参数的范围type_parameter_list上的class_declaration类声明)是class_basetype_parameter_constraints_clause s和class_body那的class_declaration
  • 通过声明的类型参数的范围type_parameter_list上的struct_declarationstruct声明)是struct_interfacestype_parameter_constraints_clause s和struct_body那的struct_declaration
  • 通过声明的类型参数的范围type_parameter_listinterface_declaration接口声明)是interface_basetype_parameter_constraints_clause s和interface_body那的interface_declaration
  • 通过声明的类型参数的范围type_parameter_list上的delegate_declaration委托声明)是return_typeformal_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_declarationMethods)中声明的参数范围是该method_declarationmethod_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_statementLabeled语句)中声明的标签的范围声明发生
  • local_variable_declaration局部变量声明)中声明的局部变量的范围是声明发生的块。
  • 在声明的局部变量的范围switch_block一的switch声明(switch语句)是switch_block
  • 语句for_initializerfor声明的局部变量的范围for语句)是for_initializerfor_conditionfor_iterator语句的包含for语句。
  • local_constant_declaration局部常量声明)中声明的局部常量的范围是声明发生的块。constant_declarator之前的文本位置引用局部常量是编译时错误
  • 声明为foreach_statementusing_statementlock_statementquery_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 }

Fin 的声明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修饰符表明FDerived为“新”,而且它确实是有意隐藏继承成员。

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

 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 }

在上面的例子中,在声明FDerived隐藏了F一个从继承Base,但由于新FDerived具有私有访问,它的范围不会延伸到MoreDerived因此,呼叫F()MoreDerived.G是有效的,将调用Base.F

命名空间和类型名称

C#程序中的几个上下文需要指定namespace_nametype_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_namenamespace_or_type_name是指一个命名空间。按照下面所述的解决方案,namespace_namenamespace_or_type_name必须引用命名空间,否则会发生编译时错误。namespace_name中不能存在任何类型参数(类型参数(只有类型可以具有类型参数)。

TYPE_NAMEnamespace_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是单个标识符,Nnamespace_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或者其任何基类型包含具有name IKtype参数的嵌套可访问类型,则namespace_or_type_name指的是使用给定类型参数构造的类型。如果存在多个此类型,则选择在更多派生类型中声明的类型。请注意,在确定namespace_or_type_name的含义时,将忽略具有不同类型参数数的非类型成员(常量,字段,方法,属性,索引器,运算符,实例构造函数,析构函数和静态构造函数)和类型成员
    • 如果之前的步骤不成功,则对于每个命名空间N,从发生namespace_or_type_name的命名空间开始,继续使用每个封闭的命名空间(如果有),并以全局命名空间结束,将评估以下步骤,直到找到实体:
      • 如果K为零并且I是名称空间的名称N,则:
        • 如果发生namespace_or_type_name的位置由名称空间声明括起来,N并且名称空间声明包含将名称与名称空间或类型相关联的extern_alias_directiveusing_alias_directiveI,则namespace_or_type_name不明确并且发生编译时错误。
        • 否则,namespace_or_type_name指命名的命名空间IN
      • 否则,如果N包含具有名称IK类型参数的可访问类型,则:
        • 如果K为零且namespace_or_type_name出现的位置由名称空间声明括起,N并且名称空间声明包含将名称与名称空间或类型相关联的extern_alias_directiveusing_alias_directiveI,则namespace_or_type_name不明确并且发生编译时错误。
        • 否则,namespace_or_type_name引用使用给定类型参数构造的类型。
      • 否则,如果发生namespace_or_type_name的位置由名称空间声明括起N
        • 如果K为零且名称空间声明包含将名称与导入的名称空间或类型相关联的extern_alias_directiveusing_alias_directiveI,则namespace_or_type_name引用该名称空间或类型。
        • 否则,如果命名空间声明的using_namespace_directiveusing_alias_directive导入的命名空间和类型声明只包含一个具有name IKtype参数的可访问类型,则namespace_or_type_name指的是使用给定类型参数构造的类型。
        • 否则,如果由名称空间声明的using_namespace_directiveusing_alias_directive s 导入的名称空间和类型声明包含多个具有名称IK类型参数的可访问类型,则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.IN.I<A1, ..., Ak>解决如下:
    • 如果K为零并且N引用命名空间并N包含带名称的嵌套命名空间I,则namespace_or_type_name引用该嵌套命名空间。
    • 否则,如果N引用名称空间并N包含具有名称IK类型参数的可访问类型,则namespace_or_type_name引用使用给定类型参数构造的类型。
    • 否则,如果N引用(可能构造的)类或结构类型和/ N或其任何基类包含具有名称IK类型参数的嵌套可访问类型,则namespace_or_type_name引用使用给定类型参数构造的类型。如果存在多个此类型,则选择在更多派生类型中声明的类型。注意,如果N.I确定的含义是解析基类规范的一部分,N那么直接基类N被认为是对象(基类)。
    • 否则,namespace_or_type_nameN.I无效,并发生编译时错误。

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

  • namespace_or_type_nameT在一个namespace_or_type_name形式的T.I,或
  • namespace_or_type_nameTtypeof_expression参数列表的形式的1) typeof(T)

完全限定名称

每个名称空间和类型都有一个完全限定的名称,该名称唯一地标识所有其他名称空间或类型。命名空间或类型的完全限定名称N确定如下:

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

换句话说,完全限定名称NN从全局名称空间开始的标识符的完整分层路径因为命名空间或类型的每个成员都必须具有唯一的名称,所以命名空间或类型的完全限定名称始终是唯一的。

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

 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#采用自动内存管理,使开发人员无需手动分配和释放对象占用的内存。自动内存管理策略由垃圾收集器实现对象的内存管理生命周期如下:

  1. 创建对象时,将为其分配内存,运行构造函数,并将对象视为实时对象。
  2. 如果除了运行析构函数之外,任何可能的继续执行都无法访问该对象或其任何部分,则该对象将被视为不再使用,并且它有资格进行销毁。C#编译器和垃圾收集器可以选择分析代码以确定将来可以使用对对象的哪些引用。例如,如果范围内的局部变量是对象的唯一现有引用,但该过程中当前执行点的任何可能的继续执行中从不引用该局部变量,则垃圾收集器可能(但是不要求将对象视为不再使用。
  3. 一旦该对象有资格进行销毁,稍后在某些未指定的时间运行该对象的析构函数(Destructors)(如果有的话)。在正常情况下,对象的析构函数仅运行一次,尽管特定于实现的API可能允许覆盖此行为。
  4. 一旦运行了对象的析构函数,如果该对象或其任何部分无法通过任何可能的执行继续访问,包括运行析构函数,则该对象被视为不可访问,并且该对象符合收集条件。
  5. 最后,在对象符合收集条件后的某个时间,垃圾收集器释放与该对象关联的内存。

垃圾收集器维护有关对象使用的信息,并使用此信息来做出内存管理决策,例如在内存中查找新创建的对象的位置,何时重定位对象,以及何时对象不再使用或不可访问。

与假设存在垃圾收集器的其他语言一样,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字段引起的任何副作用)。当程序执行被异步事件(例如另一个线程抛出的异常)中断时,不能保证可观察的副作用在原始程序顺序中可见。
原文地址:https://www.cnblogs.com/strengthen/p/9732615.html