【C#语言规范版本5.0学习】3.3其他基本概念
➤ 签名和重载
方法、实例构造函数、索引器和运算符是由它们的签名 (signature) 来刻画的:
✹ 方法签名由方法的名称、类型形参的个数和它的每一个形参(按从左到右的顺序)的类型和种类 (值、引用或输出)组成。为了实现这些目的,形参的类型中出现的方法的任何类型形参都不是由其名称标识的,而是由它在方法的类型实参列表中的序号位置标识的。需注意的是,方法签名既不包含返回类型和 params 修饰符(它可用于指定最右边的形参),也不包含可选类型形参约束。
✹ 实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。 具体而言,实例构造函数的签名不包含可为最右边的参数指定的 params 修饰符。
✹ 索引器签名由它的每一个形参(按从左到右的顺序)的类型组成。具体而言,索引器签名既不包含元素类型,也不包含可为最右边的形参指定的 params 形参数组签名和修饰符。
✹ 运算符签名由运算符的名称和它的每一个形参(按从左到右的顺序)的类型组成。具体而言,运算符的签名不包含结果类型。 签名是对类、结构和接口的成员实施重载 (overloading) 的机制:
✹ 方法重载允许类、结构或接口用同一个名称声明多个方法,条件是它们的签名在该类、结构或接口 中是唯一的。
✹ 实例构造函数重载允许类或结构声明多个实例构造函数,条件是它们的签名在该类或结构中是唯一 的。
✹ 索引器重载允许类、结构或接口声明多个索引器,条件是它们的签名在该类、结构或接口中是唯一 的。
✹ 运算符重载允许类或结构用同一名称声明多个运算符,条件是它们的签名在该类或结构中是唯一的。
虽然 out 和 ref 参数修饰符被视为签名的一部分,但是在同一个类型中声明的成员不能仅通过 ref 和 out 在签名上加以区分。在同一类型中声明了两个成员时,如果将这两个方法中带有 out 修饰符的所有形参更改为 ref 修饰符会使这两个成员的签名相同,则会发生编译时错误。出于签名匹配的其他目的 (如隐藏或重写),ref 和 out 被视为签名的组成部分,并且互不匹配。(此限制使 C# 程序能够方便地进行转换,以便能在公共语言基础结构 (CLI) 上运行,CLI 并未提供任何方式来定义仅通过 ref 和 out 就能加以区分的方法。)
由于签名的原因,将类型 object 和 dynamic 视为是相同的。因此,在同一个类型中声明的成员不能仅通过 object 和 dynamic 在签名上加以区分。 下面的示例演示一组重载方法声明及其签名。
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 参数修饰符都是签名的一部分。因此,F(int) 和 F(ref int) 是唯一的签名。但是,F(ref int) 和 F(out int) 不能在同一个接口中声明,因为它们的签名仅 ref 和 out 不同。此外,返回类型和 params 修饰符不是签名的组成部分,所以不可能仅基于返回类型或是否存在 params 修饰符来实施重载。因此,上面列出的关于方法 F(int) 和 F(params string[]) 的声明会导致编译时错误。
➤ 范围
名称的范围 (scope) 是一个程序文本区域,在其中可以引用由该名称声明的实体,而不对该名称加以限定。范围可以嵌套 (nested),并且内部范围可以重新声明外部范围中的名称的含义(但这并不会取消声明中强加的限制,即在嵌套块中不可能声明与它的封闭块中的局部变量同名的局部变量)。因此,我们就说,外部范围中的这个同名的名称在由内部范围覆盖的程序文本区域中是隐藏的 (hidden),对外部名称只能通过它的限定名才能从内部范围来访问。
✹ 由 namespace-member-declaration所声明的命名空间成员的范围,如果没有其他封闭它的 namespace-declaration,则它的范围是整个程序文本。
✹ namespace-declaration 中 namespace-member-declaration 所声明的命名空间成员的范围是这样定义的, 如果该命名空间成员声明的完全限定名为 N,则其声明的命名空间成员的范围是,完全限定名为 N 或以 N 开头后跟句点的每个 namespace-declaration 的 namespace-body。
✹ extern-alias-directive 范围定义的名称范围扩展到直接包含其的编译单元或命名空间体的 usingdirectives、global-attributes 和 namespace-member-declarations。extern-alias-directive 不会把任何新成 员提供给基础声明空间。换言之,extern-alias-directive 不具传递性,它仅影响它在其中出现的编译 单元或命名空间体。
✹ 由 using-directive定义或导入的名称的范围扩展到出现 using-directive 的 compilation-unit 或 namespace-body 内的整个 namespace-member-declarations 中。using-directive 可以使零个或更多个 命名空间或者类型名称在特定的 compilation-unit 或 namespace-body 中可用,但不会把任何新成员提 供给基础声明空间。换言之,using-directive 不具传递性,它仅影响它在其中出现的 compilation-unit 或 namespace-body。
✹ 由 class-declaration的 type-parameter-list 声明的类型形参的范围是该 class-declaration 的 class-base、type-parameter-constraints-clauses 和 class-body。
✹ 由 struct-declaration的 type-parameter-list 声明的类型形参的范围是该 structdeclaration 的 struct-interfaces、 type-parameter-constraints-clauses 和 struct-body。
✹ 由 interface-declaration的 type-parameter-list 声明的类型形参的范围是该 interfacedeclaration 的 interface-base、type-parameter-constraints-clauses 和 interface-body。
✹ 由 delegate-declaration的 type-parameter-list 声明的类型形参的范围是该 delegatedeclaration 的 return-type、formal-parameter-list 和 type-parameter-constraints-clauses。
✹ 由 class-member-declaration所声明的成员范围是该声明所在的那个 class-body。此外, 类成员的范围扩展到该成员的可访问域中包含的那些派生类的 class-body。
✹ 由 struct-member-declaration声明的成员范围是该声明所在的 struct-body。
✹ 由 enum-member-declaration声明的成员范围是该声明所在的 enum-body。
✹ 在 method-declaration中声明的形参范围是该 method-declaration 的 method-body。
✹ 在 indexer-declaration中声明的形参范围是该 indexer-declaration 的 accessordeclarations。
✹ 在 operator-declaration中声明的形参范围是该 operator-declaration 的 block。
✹ 在 constructor-declaration中声明的形参范围是该 constructor-declaration 的 constructor-initializer 和 block。
✹ 在 lambda-expression中声明的形参范围是该 lambda-expression 的 lambda-expressionbody。
✹ 在 anonymous-method-expression中声明的形参范围为该 anonymous-method-expression 的 block。
✹ 在 labeled-statement中声明的标签范围是该声明所在的 block。
✹ 在 local-variable-declaration中声明的局部变量范围是该声明所在的块。
✹ 在 switch 语句的 switch-block 中声明的局部变量范围是该 switch-block。
✹ 在 for 语句的 for-initializer 中声明的局部变量范围是该 for 语句的 for-initializer、 for-condition、for-iterator 以及所包含的 statement。
✹ 在 local-constant-declaration中声明的局部常量范围是该声明所在的块。在某局部常量 constant-declarator 之前的文本位置中引用该局部常量是编译时错误。
✹ 作为 foreach-statement、using-statement、lock-statement 或 query-expression 一部分声明的变量的范围 由给定构造的扩展确定。
在命名空间、类、结构或枚举成员的范围内,可以在位于该成员的声明之前的文本位置引用该成员。例如
class A { void F() { i = 1; } int i = 0; }
这里,F 在声明 i 之前引用它是有效的。 在局部变量的范围内,在位于该局部变量的 local-variable-declarator 之前的文本位置引用该局部变量是编译时错误。例如
class A { int i = 0; void F() { i = 1; // Error, use precedes declaration int i; i = 2; } void G() { int j = (j = 1); // Valid } void H() { int a = 1, b = ++a; // Valid } }
在上面的 F 方法中,第一次明确给 i 赋值时,并未引用在外部范围声明的字段。相反,它所引用的是局部变量 i,这会导致编译时错误,因为它在文本上位于该变量的声明之前。在方法 G 中,在 j 的声明初 始值设定项中使用 j 是有效的,因为并未在 local-variable-declarator 之前使用。在方法 H 中,后面的 local-variable-declarator 正确引用在同一 local-variable-declaration 内的前面的 local-variable-declarator 中声明的局部变量。
局部变量的范围规则旨在保证表达式上下文中使用的名称的含义在块中总是相同。如果局部变量的范围仅从它的声明扩展到块的结尾,则在上面的示例中,第一次赋值将会分配给实例变量,第二次赋值将会 分配给局部变量,如果后来重新排列块的语句,则可能会导致编译时错误。 块中名称的含义可能因该名称的使用上下文而异。在下面的示例中
using System; class A {} class Test { static void Main() { string A = "hello, world"; string s = A; // expression context Type t = typeof(A); // type context Console.WriteLine(s); // writes "hello, world" Console.WriteLine(t); // writes "A" } }
名称 A 在表达式上下文中用来引用局部变量 A,在类型上下文中用来引用类 A。
⟰ 名称隐藏
实体的范围通常比该实体的声明空间包含更多的程序文本。具体而言,实体的范围可能包含一些声明, 它们会引入一些新的声明空间,其中可能含有与该实体同名的新实体。这类声明导致原始的实体变为隐 藏的 (hidden)。相反,当实体不是隐藏的时,就说它是可见的 (visible)。 当范围之间相重叠(或通过嵌套重叠,或通过继承重叠)时会发生名称隐藏。以下各节介绍这两种隐藏类型的特性。
通过嵌套隐藏
以下各项活动会导致发生通过嵌套的名称隐藏:在命名空间内嵌套其他命名空间或类型;在类或结构中的嵌套类型;声明形参和局部变量。 在下面的示例中
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") 将导致编译时错误。
通过继承隐藏
当类或结构重新声明从基类继承的名称时,会发生通过继承的名称隐藏。这种类型的名称隐藏采取下列形式之一:
✹ 类或结构中引入的常量、字段、属性、事件或类型会把所有同名的基类成员隐藏起来。
✹ 类或结构中引入的方法隐藏所有同名的非方法基类成员,以及所有具有相同签名(方法名称和参数个数、修饰符和类型)的基类方法。
✹ 类或结构中引入的索引器隐藏所有具有相同签名(参数个数和类型)的基类索引器。
管理运算符声明的规则使派生类不可能声明与基类中的运算符具有相同签名的运算符。 因此,运算符从不相互隐藏。 与隐藏外部范围中的名称相反,隐藏继承范围中的可访问名称会导致发出警告。在下面的示例中
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。
➤ 命名空间和类型名称
C# 程序中的若干上下文要求指定 namespace-name 或 type-name。
namespace-name: namespace-or-type-name type-name: namespace-or-type-name namespace-or-type-name: identifier type-argument-listopt namespace-or-type-name . identifier type-argument-listopt qualified-alias-member
namespace-name 是引用一个命名空间的 namespace-or-type-name。根据如下所述的解析过程, namespace-name 的 namespace-or-type-name 必须引用一个命名空间,否则将发生编译时错误。 namespace-name 中不能存在任何类型实参,只有类型才能具有类型实参。 type-name 是引用一个类型的 namespace-or-type-name。根据如下所述的解析过程,type-name 的 namespace-or-type-name 必须引用一个类型,否则将发生编译时错误。 如果 namespace-or-type-name 是 qualified-alias-member,则其含义如命名空间别名限定符节中所述。否则,namespaceor-type-name 具有下列四种形式之一:
✹ I
✹ I<a1, ...,ak="">
✹ N.I
✹ N.I<a1, ...,ak="">
其中 I 是单个标识符,N 是 namespace-or-type-name,<a1, ...,="" ak=""> 是可选的 type-argument-list。如果未指定 type-argument-list 时,则可将 K 视为零。
namespace-or-type-name 的含义按下述步骤确定:
如果 namespace-or-type-name 的形式为 I 或 I<a1, ...,ak="">:
▧ 如果 K 为零,namespace-or-type-name 出现在泛型方法声明中,且该声明包含名为 I 的类型形参,则 namespace-or-type-name 引用该类型形参。
▧ 否则,如果 namespace-or-type-name 出现在类型声明中,则对于每个实例类型 T, 从该类型声明的实例类型开始,对每个封闭类或结构声明(如果有)的实例类型继续如下过程:
▧ 如果 K 为零,并且 T 的声明包含名为 I 的类型形参,则 namespace-or-type-name 引用该类型形参。
✹ 否则,如果 namespace-or-type-name 出现在该类型声明的体中,且 T 或其任一基类型包含具 有名称 I 和 K 个类型形参的嵌套可访问类型,则 namespace-or-type-name 引用利用给定类型 实参构造的该类型。如果存在多个这样的类型,则选择在派生程度较大的类型中声明的类型。 请注意,在确定 namespace-or-type-name 的含义时,将忽略非类型成员(常量、字段、方法、 属性、索引器、运算符、实例构造函数、析构函数和静态构造函数)和具有不同数目的类型形参的类型成员。
▧ 如果当时前面的步骤不成功,则对于每个命名空间 N,从出现 namespace-or-type-name 的命名空间开始,继续到每个封闭命名空间(如果有)且到全局命名空间结束,对下列步骤进行计算直到找到实体:
✹ 如果 K 为零,并且 I 为 N 中的命名空间的名称,则:
▧ 如果出现 namespace-or-type-name 的位置包含在 N 的命名空间声明中,并且该命名空间声明包含将名称 I 与某个命名空间或类型关联的 extern-alias-directive 或 using-aliasdirective,则 namespace-or-type-name 是不明确的,并将发生编译时错误。
▧ 否则,namespace-or-type-name 引用 N 中名为 I 的命名空间。
✹ 否则,如果 N 包含一个具有名称 I 且有 K 个类型形参的可访问类型,则:
▧如果 K 为零,并且出现 namespace-or-type-name 的位置包含在 N 的命名空间声明中,并且该命名空间声明包含将名称 I 与某个命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 namespace-or-type-name 是不明确的,并将发生编译时错误。
▧ 否则,namespace-or-type-name 引用利用给定类型实参构造的该类型。
✹ 否则,如果出现 namespace-or-type-name 的位置包含在 N 的命名空间声明中:
▧ 如果 K 为零,并且该命名空间声明包含一个将名称 I 与一个导入的命名空间或类型关联 的 extern-alias-directive 或 using-alias-directive,则 namespace-or-type-name 引用该命名空间或类型。
▧否则,如果该命名空间声明的 using-namespace-directive 导入的命名空间恰好包含一个具有名称 I 且有 K 个类型形参的类型,则 namespace-or-type-name 引用利用给定类型实参构造的该类型。
▧ 否则,如果该命名空间声明的 using-namespace-directives 导入的命名空间包含多个具有名称 I 且有 K 个类型形参的类型,则 namespace-or-type-name 是不明确的,并将导致发生错误。
▧ 否则,namespace-or-type-name 未定义,并将导致发生编译时错误。
✹ 否则,namespace-or-type-name 的形式为 N.I 或 N.I<a1, ...,ak="">.N 首先解析为 namespace-or-typename。如果对 N 的解析不成功,则发生编译时错误。否则,N.I 或 N.I<a1, ...,="" ak=""> 按如下方式进行解析:
▧ 如果 K 为零,N 引用一个命名空间,并且 N 包含名为 I 的嵌套命名空间,则 namespace-or-typename 引用该嵌套命名空间。
▧ 否则,如果 N 引用一个命名空间,并且 N 包含一个具有名称 I 且有 K 个类型形参的可访问类型, 则 namespace-or-type-name 引用利用给定类型实参构造的该类型。
▧ 否则,如果 N 引用一个(可能是构造的)类或结构类型,并且 N 或其任一基类包含一个具有名 称 I 且有 K 个类型形参的嵌套可访问类型,则 namespace-or-type-name 引用利用给定类型实参构造的该类型。如果存在多个这样的类型,则选择在派生程度较大的类型中声明的类型。请注意, 如果要将 N.I 的含义确定为解析 N 的基类指定的一部分,则将 N 的直接基类视为对象。
▧ 否则,N.I 是无效的 namespace-or-type-name 并将发生编译时错误。 仅当下列条件成立时才允许 namespace-or-type-name 引用静态类
✹ namespace-or-type-name 是 T.I 形式的 namespace-or-type-name 中的 T,或者
✹ namespace-or-type-name 是 typeof(T) 形式的 typeof-expression中的 T。
⟰ 完全限定名
每个命名空间和类型都具有一个完全限定名 (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 }
➤ 自动内存管理
C# 使用自动内存管理,它使开发人员不再需要以手动方式分配和释放对象占用的内存。自动内存管理策略由垃圾回收器 (garbage collector) 实现。
一个对象的内存管理生存周期如下所示:
1. 当创建对象时,为其分配内存,运行构造函数,将该对象被视为活对象。
2. 在后续执行过程中,如果不会再访问该对象或它的任何部分(除了运行它的析构函数),则将该对象视为不再使用,可以销毁。C# 编译器和垃圾回收器可以通过分析代码,确定哪些对象引用可能在将来被使用。例如,如果范围内的某个局部变量是现有的关于此对象的唯一引用,但在当前执行点之后的任何后续执行过程中,该局部变量都不会再被引用,那么垃圾回收器可以(但不是必须)认为该对象不再被使用。
3. 一旦对象符合销毁条件,在稍后某个时间将运行该对象的析构函数(如果有)。在普通情况下,对象的析构函数仅运行一次,但特定于实现的 API 可允许忽略此行为。
4. 一旦运行对象的析构函数,如果该对象或它的任何部分无法由任何可能的执行继续(包括运行析构函数)访问,则该对象被视为不可访问,可以回收。
5. 最后,在对象变得符合回收条件后,垃圾回收器将释放与该对象关联的内存。
垃圾回收器维护对象的使用信息,并利用此信息做出内存管理决定,如在内存中的何处安排一个新创建的对象、何时重定位对象以及对象何时不再被使用或不可访问。
与其他假定存在垃圾回收器的语言一样,C# 也旨在使垃圾回收器可以实现广泛的内存管理策略。例如, C# 并不要求一定要运行析构函数,不要求对象一符合条件就被回收,也不要求析构函数以任何特定的 顺序或在任何特定的线程上运行。
垃圾回收器的行为在某种程度上可通过类 System.GC 的静态方法来控制。该类可用于请求执行一次回收操作、运行(或不运行)析构函数,等等。
由于垃圾回收器在决定何时回收对象和运行析构函数方面可以有很大的选择范围,它的一个符合条件的实现所产生的输出可能与下面的代码所显示的不同。程序
using System; class A { ~A() { Console.WriteLine("Destruct instance of A"); } } class B { object Ref; public B(object o) { Ref = o; } ~B() { Console.WriteLine("Destruct instance of B"); } } class Test { static void Main() { B b = new B(new A()); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); } }
创建类 A 的一个实例和类 B 的一个实例。当给变量 b 赋值 null 后,这些对象变得符合垃圾回收条件, 这是因为从此往后,任何用户编写的代码不可能再访问这些对象。输出可以为
Destruct instance of A
Destruct instance of B
或
Destruct instance of B
Destruct instance of A
这是因为该语言对于对象的垃圾回收顺序没有强加约束。 “符合销毁条件”和“符合回收条件”之间的区别虽然微小,但也许非常重要。例如,
using System; class A { ~A() { Console.WriteLine("Destruct instance of A"); } public void F() { Console.WriteLine("A.F"); Test.RefA = this; } } class B { public A Ref; ~B() { Console.WriteLine("Destruct instance of B"); Ref.F(); } } class Test { public static A RefA; public static B RefB; static void Main() { RefB = new B(); RefA = new A(); RefB.Ref = RefA; RefB = null; RefA = null; // A and B now eligible for destruction GC.Collect(); GC.WaitForPendingFinalizers(); // B now eligible for collection, but A is not if (RefA != null) Console.WriteLine("RefA is not null"); } }
在上面的程序中,如果垃圾回收器选择在 B 的析构函数之前运行 A 的析构函数,则该程序的输出可能是:
Destruct instance of A
Destruct instance of B
A.F
RefA is not null
请注意,虽然 A 的实例没有使用,并且 A 的析构函数已被运行过了,但仍可能从其他析构函数调用 A 的 方法(此例中是指 F)。还请注意,运行析构函数可能导致对象再次从主干程序中变得可用。在此例中, 运行 B 的析构函数导致了先前没有被使用的 A 的实例变得可从当前有效的引用 Test.RefA 访问。调用 WaitForPendingFinalizers 后,B 的实例符合回收条件,但由于引用 Test.RefA 的缘故,A 的实例 不符合回收条件。
为了避免混淆和意外的行为,好的做法通常是让析构函数只对存储在它们对象本身字段中的数据执行清理,而不对它所引用的其他对象或静态字段执行任何操作。 另一种使用析构函数的方法是允许类实现 System.IDisposable 接口。这样的话,对象的客户端就可以确定何时释放该对象的资源,通常是通过在 using 语句中以资源形式访问该对象。
➤执行顺序
C# 程序执行时,在临界执行点保留每个执行线程的副作用。副作用 (side effect) 副作用定义为对可变字段的读取或写入、对非可变变量的写入、对外部资源的写入以及异常的引发。临界执行点(这些副作用 的顺序必须保存在其中)是指下列各活动:
引用一些可变字段;
引用 lock 语句;
引用线程的创建与终止。
执行环境可以随便更改 C# 程序的执行顺序,但受下列约束限制:
✹ 在执行线程中需保持数据依赖性。就是说,在计算每个变量的值时,就好像线程中的所有语句都是按原始程序顺序执行的。
✹ 保留初始化的排序规则。
✹ 对于不稳定读写,副作用的顺序需保持不变。此外,执行环境甚至可以不需要计算一个表达式的各个部分,如果它能推断出表达式的值是“不会被使用的”而且不会产生有效的副作用(包括由调用方法或访问不稳定字段导致的任何副作用)。当程序执行被异步事件(例如其他线程引发的异常)中断时,它不保证可观察到的副作用以原有的程序顺序出现。