Dart语言惯用语——Dart中特有的代码味道
对于其它语言,特别是Java和Javascript的开发者,Dart语言设计得看起来很熟悉。如果你足够努力,你可以使 Dart 就像是那些语言一样。如果你非常努力,你甚至可以把Dart变为Fortran,但是这样你将错过Dart中独特的、有趣的部分。
本文将帮助您写出适用于Dart的独特代码。因为语言仍在演进中,所以这里的许多惯用语也在变化。语言中的有些地方我们仍不确定用什么最佳实践好。(也许你能帮助我们。)但这里是一些要点,有助于把你带出Java或Javascript的思维惯式,进入Dart。
构造函数
本文将从每个对象生命周期的起始: 使用构造函数开始。每个对象都将在某一个时刻被构造出来,定义构造函数是创建一个类的重要组成部分。这里Dart有些有趣的想法。
自动初始化字段
首先是摆脱一些单调乏味的部分。许多构造函数仅仅是简单地把参数赋值给字段,如:
class Point { num x, y; Point(num x, num y) { this.x = x; this.y = y; } }
这样我们这里不得不输入4次x,仅仅是初始化一个字段。太烂了。我们可以做的更好:
class Point { num x, y; Point(this.x, this.y); }
在参数列表中,如果参数前使用 this. ,那么这个名字的字段将自动使用该参数值做初始化。这个例子也展示了另外一个小特性:如果一个构造函数体完全是空的,那么可以只使用一个分号(;)替代 { }。
命名构造函数
就像大部分动态类型语言一样,Dart不支持重载。对于方法而言,这没有多少限制,因为你总是可以使用不同的名字,但是构造函数就没有这么幸运了。为了缓和这种情况,Dart允许你定义命名构造函数:
class Point { num x, y; Point(this.x, this.y); Point.zero() : x = 0, y = 0; Point.polar(num theta, num radius) { x = Math.cos(theta) * radius; y = Math.sin(theta) * radius; } }
这里这个 Point 类有三个构造函数,一个标准的和二个命名的。你可以像下面这样使用它们:
var a = new Point(1, 2); var b = new Point.zero(); var c = new Point.polar(Math.PI, 4.0);
注意我们这里在调用命名构造函数的时候仍然使用了 new 。这样它就不是一个静态方法了。
工厂构造函数
有一些和工厂(factory)相关的设计模式。当你需要一些类的实例,但是想以更灵活一点的方式,而不是直接用硬编码的方式调用具体类型的构造函数的话,这时这些工厂模式开始发挥作用了。如果你已经有了一个实例也许会想返回之前已经缓存的实例,或者也许你想返回一个不同类型的对象。
Dart支持这种模式,但不要求你改变创建对象时的样子。而是让你定义一个工厂构造函数。当你调用它的时候看起来像一个普通的构造函数。但是实现可以做任何它想做的事情。例如:
class Symbol { final String name; static Map<String, Symbol> _cache; factory Symbol(String name) { if (_cache == null) { _cache = {}; } if (_cache.containsKey(name)) { return _cache[name]; } else { final symbol = new Symbol._internal(name); _cache[name] = symbol; return symbol; } } Symbol._internal(this.name); }
这里我们有一个表示符号的类。一个符号就像一个字符串,但是我们保证在任意时间点上一个给定的名字只会有一个符号对象。这让你能安全地比较两个对象的相等性仅仅通过测试他们是同一个对象。
这里的默认(未命名)构造函数前加上了 factory 前缀。它告诉Dart这是一个工厂构造函数。当它被调用时,它不会创建一个新对象。(工厂构造函数中没有 this 对象)。相反,期望你创建一个实例并明确地返回它。这里我们用给定的名字查找之前缓存的对象,如果找到了就重用它。
最酷的是调用者根本看不到这点。它们只需要:
var a = new Symbol('something'); var b = new Symbol('something');
第二个 new 将返回之前缓存的对象。这很好,因为这意味着如果我们起初不需要工厂构造函数但之后又认识到需要时,我们将不必把所有之前使用new的地方都改为使用静态方法调用。
函数
像大部分现代语言一样,函数是Dart中的头等公民(first-class),带有完整的闭包和轻量型语法支持。函数就像任何其它对象一样,你应毫不犹豫地自由使用它们。特别是,在Dart团队中我们大量使用函数用作事件处理器(event handler)。
Dart有三种创建函数的表示法:一个是命名函数,一个是带函数体的匿名函数和一个表达式语句函数。命名形式看起来像这样:
void sayGreeting(String salutation, String name) { final greeting = '$salutation $name'; print(greeting); }
这看起来像是一个普通的C语言函数或者Java、Javascript中的方法。和C、C++不同的是,这些可以嵌入到另一个函数的中间。如果你不需要给出函数的名字,也可以使用匿名形式。和上面代码类似,但没有名字或返回类型,像这样:
window.on.click.add((event) { print('You clicked the window.'); })
这里我们传递一个函数到add()方法注册一个事件处理器。最后,如果你需要一个真正的轻量型函数,仅仅对单一表达式求值并返回,使用 => :
var items = [1, 2, 3, 4, 5]; var odd = items.filter((i) => i % 2 == 1); print(odd); // [1, 3, 5]
一个括号的参数列表,跟着一个 => 和一个单一表达式就创建了一个带参数并返回表达式结果的函数。
实际上,只要有可能我们自己更喜欢用这种箭头函数,因为它简洁且容易识别,感谢 => 。我们经常使用匿名函数作为事件处理器和回调函数。命名函数反而使用很少。
Dart 还有一个技巧,这是我最喜欢的语言特性之一:可以使用 => 定义成员。当然,你可以这样做:
class Rectangle { num width, height; bool contains(num x, num y) { return (x < width) && (y < height); } num area() { return width * height; } }
但是当你仅需要下面这样做的时候,为什么还需要上面那样呢:
class Rectangle { num width, height; bool contains(num x, num y) => (x < width) && (y < height); num area() => width * height; }
我们发现箭头函数非常适用于定义简单的getter和其它单行方法来实现计算或访问对象属性。
字段,getters 和 setters
说到属性,Dart使用标准的 object.someProperty 语法使用它们。 当属性是类中一个真实的字段时,大部分语言就是这样做的。但是Dart 还允许你定义一些方法,它们看起来像是访问属性,但实际上可以执行任意你想要的代码。在其它语言中,这些被称为getters 和 setters。看这个例子:
class Rectangle { num left, top, width, height; num get right() => left + width; set right(num value) => left = value - width; num get bottom() => top + height; set bottom(num value) => top = value - height; Rectangle(this.left, this.top, this.width, this.height); }
这里我们定义了一个Rectangle 类,它有四个真实的字段:left, top, width, 和 height。它还有两对getters 和 setters方法定义了两个额外的逻辑属性:right 和 bottom。在你使用这个类时,真实字段与 getters和setters 没有明显的区别:
var rect = new Rectangle(3, 4, 20, 15); print(rect.left); print(rect.bottom); rect.top = 6; rect.right = 12;
字段和getters/setters间的模糊化是语言的基本原则。看待它最清楚的方式就是认为字段仅仅是魔法实现的getters 和 setters。这意味着你可以做些有趣的事情,比如用字段覆盖继承的getter方法,或反之。如果接口定义了一个getter,你可以简单地用一个同名、同类型的字段实现它。如果字段是可变的(非final),那么它也实现了接口要求的setter。
实际上,这意味着你不必防御性地把字段隐藏到getter、setter样板方法里来隔离它们,就像你在Java或C#中所做的。如果你有需要暴露的属性,只需用一个public的字段。如果你不想它们被修改,只需加上final。
稍后,如果你需要做一些验证或什么其它事情,你随时可以用getter和setter代替这个字段。比如我们想确保 Rectangle类总是有非负的大小,我们可以把它改为这样:
class Rectangle { num left, top; num _width, _height; num get width() => _width; set width(num value) { if (value < 0) throw 'Width cannot be negative.'; _width = value; } num get height() => _height; set height(num value) { if (value < 0) throw 'Height cannot be negative.'; _height = value; } num get right() => left + width; set right(num value) => left = value - width; num get bottom() => top + height; set bottom(num value) => top = value - height; Rectangle(this.left, this.top, this._width, this._height); }
现在我们修改这个类增加了一些验证,但根本不影响那些已经使用了它的代码。
顶层定义
Dart是“纯”面向对象语言,变量中的任何东西都是一个真正的对象(没有突变的原始类型),并且每个对象都是某个类的实例。然而它不是教条式的OOP语言。它不要求你把每个东西都定义在类里。相反,如果你愿意,你可以在顶层自由地定义函数、变量甚至是getters和setters。
num abs(num value) => value < 0 ? -value : value; final TWO_PI = Math.PI * 2.0; int get today() { final date = new Date.now(); return date.day; }
即使是那些不要求你把所有东西都放在类或对象中的语言,如Javascript,它们一般仍然是用一种命名空间的形式:相同名字的顶层定义会导致不经意的冲突。为解决这个问题,Dart使用一种 library 系统,允许你用一个前缀导入其它库中的定义来消除歧义。这意味着你不应该需要防御式地把定义放到类中。
我们仍在探索这实际意味着我们应如何定义库。我们的大部分代码是把定义放到类中的,如Math。很难说这仅是我们在其它语言中根深蒂固的习惯还是说对Dart而言这也是一种好的实践方式。我们期待这方面的反馈。
我们确实有一些使用顶层定义的例子。首先你需要运行的main()函数就是要在顶层定义的。如果你使用DOM,熟悉的document 和 window “变量”实际上是Dart中顶层定义的getters 。
字符串和插值
Dart有几种字符串字面值。你可以用单引号或双引号,也可以用三引号的多行字符串:
'I am a "string"' "I'm one too" '''I'm on multiple lines ''' """ As am I """
为了构造更大的字符串,使用+ 连接它们即可:
var name = 'Fred'; var salutation = 'Hi'; var greeting = salutation + ', ' + name;
但是使用字符串插值会更快更清晰:
var name = 'Fred'; var salutation = 'Hi'; var greeting = '$salutation, $name';
在字符串中,一个美元符号($)跟着一个变量将被扩展为该变量的值。(如果变量不是字符串将调用它的toString()方法)。你也可以在大括号里插入表达式:
var r = 2; print('The area of a circle with radius $r is ${Math.PI * r * r}');
操作符
Dart使用你熟悉的C、Java语言里一样的操作符和优先级。它们会按你期望的方式工作。而在背后,它们有点特殊。在Dart中,使用操作符的表达式如1+2,实际上仅是调用方法的语法糖。对于语言,这个例子看起来更像是1.+(2) 。
这意味着你也可以为你自己的类型重载(多数)操作符。例如这是一个Vector 类:
class Vector { num x, y; Vector(this.x, this.y); operator +(Vector other) => new Vector(x + other.x, y + other.y); }
这样,我们可以使用熟悉的语法形式做向量加法:
var position = new Vector(3, 4); var velocity = new Vector(1, 2); var newPosition = position + velocity;
话虽如此,请不要过度滥用。我们给你汽车的钥匙,并且相信你不会掉头把车开到客厅里。
在实践上,如果你定义的类型在“现实世界”中(在黑板上?)经常使用操作符,那么它可能是一个好的操作符重载候选者,如:复数、向量、矩阵等。另外,也不一定。自定义操作符的类型一般也应该是不可变类型。
注意因为操作符调用实际上仅仅是方法调用,它们具有固有的不对称性。方法总是在左边的参数上获取。所以当你做a+b的时候,是根据 a 的类型决定其意义的。
相等性
有一类操作符需要特别注意。Dart有两类相等运算符:== 和 !=, 与 === 和 !== 。Javascript开发者应该很熟悉,但是这里有点区别。
== 和 != 做等价测试。它们应该是你99%的时候使用的。和Javascript不同,它们不做任何隐式转换,所以它们应该像你所期待的那样的行为。别害怕使用它们。和Java不同,它们适用于任何具有等价关系定义的类型。不再需要 someString.equals("something") 这样了。
你可以为你自己的类型重载 == ,只要它们有意义。你不必重载 != ,Dart 自动根据你的 == 定义做推断。