Skip to content

Latest commit

 

History

History
379 lines (281 loc) · 16.5 KB

4.2.属性.md

File metadata and controls

379 lines (281 loc) · 16.5 KB

4.2.属性

在变量字段之后,属性字段是第二个用来处理类中的数据的选项。不像变量字段,属性字段能够对字段的访问规则和生成规则提供更细微的控制,通常有以下使用场景:

Next to variables (4.1), properties are the second option for dealing with data on a class. Unlike variables however, they offer more control of which kind of field access should be allowed and how it should be generated. Common use cases include:

  • 一个可以从任何地方读取,但是只能从定义所在的类内部进行写入的字段
  • 一个通过 getter 方法进行读取访问的字段
  • 一个通过 setter 方法进行写入访问的字段
  • Have a field which can be read from anywhere,but only be written from within the defining class.
  • Have a field which invokes a getter-method upon read-access.
  • Have a field which invokes a setter-method upon write-access.

面对属性字段时,重要的是理解以下两种类型的访问:

When dealing with properties, it is important to understand the two kinds of access:

定义 读取访问: 一个字段的读取访问发生于 字段访问表达式(第5.7节)作为右值(right-hand side) 表达式使用时。其中也包括 obj.field() 形式的调用,字段 field 此时发生了读取访问。

定义 写入访问: 一个字段的写入访问发生于字段访问表达式被赋值时,比如 obj.field = value 。当使用 += 等特殊的赋值操作符时,读取访问也会同时发生,比如表达式 obj.field += value

[warning] Definition: Read Access A read access to a field occurs when a right-hand side field access expression (5.7) is used. This includes calls in the form of obj.field(), where field is accessed to be read.

Definition: Write Access A write access to a field occurs when a field access expression (5.7) is assigned a value in the form of obj.field = value. It may also occur in combination with read access (4.2) for special assignment operators such as += in expressions like obj.field += value.

读取访问和写入访问行为直接反映在其声明的语法形式上,如下面的例子:

Read access and write access are directly reflected in the syntax, as the following example shows:

class Main {
    public var x(default, null):Int; 
    static public function main() { } 
} 

声明的语法大体上和变量字段的语法相似,事实上他们的语法规则也是一样的。但是属性字段的声明有以下不同:

For the most part, the syntax is similar to variable syntax, and the same rules indeed apply. Properties are identified by

  • 字段名称后跟开口括号 (
  • 后跟一个特定的访问标识符(此例为:default),
  • 一个逗号 , 进行分隔
  • 后跟另一个特殊的访问标识符(此例为:null),
  • 最后闭口括号 ) 进行闭合
  • the opening parenthesis ( after the field name,
  • followed by a special access identifier (here: default),
  • with a comma , separating
  • another special access identifier (here: null)
  • before a closing parenthesis ).

访问标识符定义了字段的读取(第一个标识符)行为,和写入(第二个标识符)行为。有以下标识符可用:

The access identifiers define the behavior when the field is read (first identifier) and written (second identifier). The accepted values are:

  • default:如果字段修饰为 public 则具备通常的访问权限,否则等于 null 标识符。
  • null:只允许从定义的类内部进行访问。
  • get/set:访问行为被生成为一个存取器方法进行调用。编译器会确保存取器可用。
  • dynamic:作用与 get/set 一样,但是编译器不会验证存取器字段是否存在。
  • never:不允许访问
  • default: Allows normal field access if the field has public visibility, otherwise equal to null access.
  • null: Allows access only from within the defining class.
  • get/set: Access is generated as a call to an accessor method. The compiler ensures that the accessor is available.
  • dynamic: Like get/set access, but does not verify the existence of the accessor field.
  • never: Allows no access at all.

定义 存取器方法: 一个类型为 T 的字段 field 的存取器方法(或简称存取器)即,一个名为 get_field 且类型为 Void -> Tgetter(用于取),或是一个名为 set_field 且类型为 T -> Tsetter(用于存)。

[warning] Definition: Accessor method An accessor method (or short accessor) for a field named field of type T is a getter named get_field of type Void->T or a setter named set_field of type T->T.

花絮:存取器名称 在 Haxe 2中,允许任意的标识符作为访问标识符,这样就需要允许自定义的访问方法名称。这使得部分实现处理起来十分棘手。尤其是 Reflect.getProperty()Reflect.setProterty() 不得不假设名称可能是任意的,因此需要目标生成器生成元信息并执行查找。

最后我们决定不再允许任意标识符,并使用 get_set_ 命名约定,这样做极大地简化了实现。而这也是 Haxe 2 和 Haxe 3 之间的一个断层式地改变。

[warning] Trivia: Accessor names:

In Haxe 2, arbitrary identifiers were allowed as access identifiers and would lead to custom accessor method names to be admitted. This made parts of the implementation quite tricky to deal with. In particular, Reflect.getProperty() and Reflect.setProperty() had to assume that any name could have been used, requiring the target generators to generate meta-information and perform lookups.

We disallowed these identifiers and went for the get_ and set_ naming convention which greatly simplified implementation. This was one of the breaking changes between Haxe 2 and 3.

4.2.1.常见访问标识符组合

以下例子展示了属性字段常用的访问标识符组合:

The next example shows common access identifier combinations for properties:

class Main { 
    // 可从外部可读取,只在 Main 中写入 
    public var ro(default, null):Int; 
    
    // 可从外部写入,只在 Main 中读取 
    public var wo(null, default):Int; 
    
    // 通过 getter 和 setter 访问
    // get_x & set_x 
    public var x(get, set):Int; 
    
    // 通过 getter 读取,不可写入 
    public var y(get, never):Int; 
    
    // 字段 x 的 getter 存取器
    function get_x() return 1; 
    
    // 字段 x 的 setter 存取器
    function set_x(x) return x; 
    
    // 字段 y 的 getter 存取器
    function get_y() return 1; 
    
    function new() { 
        var v = x;
        x = 2; 
        x += 1; 
    }
    
    static public function main() { 
        new Main(); 
    }
} 

JavaScript 目标平台的输出可以帮助我们理解 main 函数中的字段访问会如何被编译:

The JavaScript output helps understand what the field access in the main-method is compiled to:

var Main = function() { 
    var v = this.get_x(); 
    this.set_x(2); 
    var _g = this; 
    _g.set_x(_g.get_x() + 1);
}; 

如上所述,读取访问会生成一个 get_x() 的调用,而写入访问会生成一个 set_x(2) 的调用,其中 2 会被赋值给 x+= 操作生成的代码初看起来有些奇怪,但是可以通过下面的例子来理解:

As specified, the read access generates a call to get_x(), while the write access generates a call to set_x(2) where 2 is the value being assigned to x. The way the += is being generated might look a little odd at first, but can easily be justified by the following example:

class Main { 
    public var x(get, set):Int; 
    function get_x() return 1; 
    function set_x(x) return x; 
    
    public function new() { } 
    
    static public function main() { 
        new Main().x += 1; 
    }
} 

main 函数中对字段 x 的访问的表达式是复杂的:其中包含了潜在的副作用,如本例中 Main 的构造函数。因此,编译器不能将 += 操作符直接生成为 new Main().x = new Main().x + 1, 而是需要把复杂的表达式保存进一个局部变量中使用:

What happens here is that the expression part of the field access to x in the main method is complex: It has potential side-effects, such as the construction of Main in this case. Thus, the compiler cannot generate the += operation as new Main().x = new Main().x + 1 and has to cache the complex expression in a local variable:

Main.main = function() { 
    var _g = new Main(); 
    _g.set_x(_g.get_x() + 1);
}

4.2.2.对类型系统的影响

属性字段的出现会在类型系统上产生一些影响。其中最重要的是属性字段是一个编译时特性,因此字段的类型声明在编译时必须是已知的。如果我们将一个带有属性字段的类型赋值到 Dynamic 对象上,那么字段访问将不再遵循存取器方法。同时,访问限制也将不再适用,此时的访问几乎都是 public 的。

The presence of properties has several consequences on the type system. Most importantly, it is necessary to understand that properties are a compile-time feature and thus require the types to be known. If we were to assign a class with properties to Dynamic, field access would not respect accessor methods. Likewise, access restrictions no longer apply and all access is virtually public.

补充一个例子:

class Main {
	static public function main() {
        
         // 由于 setter 的限制,赋值小于 0 时会被钳位为 0
		trace(new Test().propertie = -100); // 输出 0
        
		// 因为以下访问行为定义为不可从外部访问,因此会出现编译时错误
		// trace(new Test().propertie);
         // trace(new Test().priPropertie = 200);
         // trace(new Test().priPropertie);
        
         // 然而赋值到 Dynamic 对象之后属性字段的访问限制不再生效
		var test:Dynamic = new Test();
		trace(test.propertie = -100); // 输出 -100
		trace(test.propertie); // 输出 -100
		trace(test.priPropertie = 200); // 输出 200
		trace(test.priPropertie); // 输出 200
	}
}

class Test {
	public function new() {}
    
    // 不可从外部读取,可以通过 setter 被写入
	public var propertie(null, set):Int;
    
    // 不可从外部被读取,也不可从外部被写入
	private var priPropertie(default, default):Int;
	function set_propertie(x) {
		if (x >= 0) {
			return x;
		} else
			return 0;
	}
}

编译后生成的 JavaScript:

// Generated by Haxe 3.4.4
(function () { "use strict";
var Main = function() { };
Main.main = function() {
	console.log(new Test().set_propertie(-100));
	var test = new Test();
	console.log(test.propertie = -100);
	console.log(test.propertie);
	console.log(test.priPropertie = 200);
	console.log(test.priPropertie);
};
var Test = function() {
};
Test.prototype = {
	set_propertie: function(x) {
		if(x >= 0) {
			return x;
		} else {
			return 0;
		}
	}
};
Main.main();
})();

如果属性字段使用了 getset 标识符,编译器将会确保 getter 或 setter 存取器存在,否则像下面这样的代码将会产生编译错误:

When using get or set access identifier, the compiler ensures that the getter and setter actually exists. The following code snippet does not compile:

class Main { 
    // 缺少 x 字段所需的 get_x 方法 
    public var x(get, null):Int;
    static public function main() {}
} 

但是字段如果在子类中定义,且父类已经定义了 get_x 方法,那么子类中不需要再定义一次:

The method get_x is missing, but it need not be declared on the class defining the property itself as long as a parent class defines it:

class Base { 
    public function get_x() return 1; 
} 
class Main extends Base { 
    // get_x 已经在父类中声明了, 所以没问题
    public var x(get, null):Int; 
    
    static public function main() {} 
} 

dynamic 访问标识符的作用与 getset 一样,但是编译器不会检查存取器是否存在。

The dynamic access modifier works exactly like get or set, but does not check for the existence

4.2.3.getter和setter的规则

存取器方法的可见性并不会影响属性本身的可见性。也就是说,如果一个属性字段被修饰为 public ,且定义了一个 getter ,getter 的访问标识符是否为 private 是无关紧要的。

Visibility of accessor methods has no effect on the accessibility of its property. That is, if a property is public and defined to have a getter, that getter may be defined as private regardless.

getter 和 setter 都可以访问他们的物理字段进行数据存储。当字段访问表达式出现在字段本身的存取器方法中,编译器会确保此类字段访问不通过存取器方法进行,从而避免出现无限递归:

Both getter and setter may access their physical field for data storage. The compiler ensures that this kind of field access does not go through the accessor method if made from within the accessor method itself, thus avoiding infinite recursion:

 class Main { 
    public var x(default, set):Int;
    
    function set_x(newX) { 
        return x = newX;
    } 
    
    static public function main() {}
} 

然而,只有当字段的其中一个访问标识符为 default 或者 null 时,编译器才假定该物理字段存在。

However, the compiler assumes that a physical field exists only if at least one of the access identifiers is default or null.

定义 物理字段: 一个字段若满足以下条件之一则被认为是物理字段:

  • 一个变量字段
  • 一个读取或写入访问为 defaultnull 的属性字段
  • 一个使用了 :isVar 元数据的属性字段

[warning] Definition: Physical field A field is considered to be physical if it is either [warning] * a variable (4.1) [warning] * a property (4.2) with the read-access or write-access identifier being default or null [warning] * a property (4.2) with :isVar metadata (6.9)

如果一个字段不满足以上条件,那么在其存取器方法中进行字段的访问会导致编译错误:

If this is not the case,access to the field from within an accessor method causes a compilation error:

class Main {
    // 字段不可以被访问,因为它不是一个实际存在的变量 
    public var x(get, set):Int; 
    
    function get_x() { 
        return x; 
    } 
    
    function set_x(x) { 
        return this.x = x;
    } 
    
    static public function main() {} 
} 

如果确实需要一个物理字段,也可以通过把字段归入 :isVar 元数据来强制执行为物理字段:

If a physical field is indeed intended, it can be forced by attributing the field in question with the :isVar metadata:

class Main { 
    // @isVar 强制执行字段为物理字段,从而可以通过编译
    @:isVar public var x(get, set):Int; 
    
    function get_x() { 
        return x; 
    }
    
    function set_x(x) { 
        return this.x = x;
    }
    
    static public function main() {} 
}

花絮 属性的 setter 类型:

第一次使用 Haxe 的人通常会疑惑为什么 setter 的类型是 T -> T 而不是 T -> Void ,也就是为什么 setter 需要返回一个值?

原因是我们想要在给字段赋值时,setter 可以作为 右手表达式(right-side expression) 使用。从而使用诸如 x = y = 1 这样的链式的语法结构,这种链式结构会被解析为 x = (y = 1)。而为了能够把 y = 1 的结果赋值给 x,表达式必须能够返回一个值,而如果说 y 有一个 setter 并且会返回 Void ,那么这种链式的结构就没有意义了。

[warning] Trivia: Property setter type It is not uncommon for new Haxe users to be surprised by the type of a setter being required to be T->T instead of the seemingly more natural T->Void. After all, why would a setter have to return something? The rationale is that we still want to be able to use field assignments using setters as rightside expressions. Given a chain like x = y = 1, it is evaluated as x = (y = 1). In order to assign the result of y = 1 to x, the former must have a value. If y had a setter returning Void, this would not be possible.