Dart 语法游历记【下】「译」

Dart 是一种面向对象语言,包含类和基于 mixin 的继承两部分。每个对象是一个类的实例,并且 Object 是所有类的父类。基于 mixin 的继承指的是每个类(除了 Object )都只有一个父类,类体还可以在多个类继承中被重用。

要创建一个对象,你可以使用 new 关键词并在其后跟上一个构造函数。构造函数可以写成类名,或者类名.标识符形式。例如:

1
2
3
4
5
6
7
var jsonData = JSON.decode('{"x":1, "y":2}');

//用 Point() 创建一个点。
var p1 = new Point(2, 2);

// 用 Point().fromJson() 创建一个点。
var p2 = new Point.fromJson(jsonData);

对象的成员分为函数和数据两类(各自的方法和实例变量)。当你调用一个方法时,你是通过一个对象来调用它的:该方法可访问该对象的方法和数据。用 . 指向对象的方法和数据成员。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var p = new Point(2, 2);

// 给 y赋值。
p.y = 3;

// 获取 y 的值。
assert(p.y == 3);

// 用 p 对象调用 distanceTo() 。
num distance = p.distanceTo(new Point(4, 4));

当你想对一个对象的成员进行一系列操作时,用串联操作( cascade )。

1
2
3
4
querySelector('#button') // 获取一个对象。
    ..text = 'Confirm'   // 调用他的成员。
    ..classes.add('important')
    ..onClick.listen((e) => window.alert('Confirmed!'));

一些类提供常量构造函数,要创建一个编译时用的常量构造函数,使用 const 关键字代替 new

1
2
3
4
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // 他们是相同的实例!

实例变量

这里是你如何声明实例变量:

1
2
3
4
5
class Point {
  num x; // 声明实例变量 x ,默认值为 null 。
  num y; // 声明实例变量 y ,默认值为 null 。
  num z = 0; // 声明实例变量 z ,初始化为 0 。
}

所有未初始化的实例变量的值为 null 。

所有的实例变量会自动生成一个隐式的 getter 方法。 Non-final 实例变量也会自动生成一个隐式的 setter 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Point {
  num x;
  num y;
}

main() {
  var point = new Point();
  point.x = 4;          // 用 setter 方法得到 x 。
  assert(point.x == 4); // 用 getter 方法得到 x 。
  assert(point.y == null); // 值为 null 。
}

如果你在声明实例变量时进行了初始化(而不是在构造函数或方法),在实例创建时该值就确定了,它是在构造函数初始化列表中执行的。

构造函数

要声明一个构造函数,只需创建一个与类同名的方法(或者加上一个额外的标识符命名构造函数的描述)。构造函数最常见的形式,就是自动生成的构造函数,下面创建一个类的新实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Point {
  num x;
  num y;

  Point(num x, num y) {
    // 有个更好的方法来实现。
    this.x = x;
    this.y = y;
  }
}

this 关键字是指当前实例。

为一个实例变量分配一个构造函数参数的模式是很常见的,Dart有语法糖使它使用起来更容易:

1
2
3
4
5
6
7
8
class Point {
  num x;
  num y;

  // 用语法糖来设置 x , y 。
  // 在构造函数运行之前。
  Point(this.x, this.y);
}

如果你不声明一个构造函数,系统会提供默认构造函数。默认构造函数没有参数,它将调用父类的无参数构造函数。

另外需要注意的是子类不继承父类的构造函数。

命名构造函数

使用命名构造函数可以为一个类声明多个构造函数,或者说是提供额外的声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // 命名构造函数
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}

记住,构造函数不能继承,这意味着子类不会继承父类的构造函数。如果你希望子类在创建之后能够拥有在父类中声明的命名构造函数,你就必须在子类中实现该构造函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person 没有默认构造函数;
  // 你必须调用 super.fromJson(data).
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

main() {
  var emp = new Employee.fromJson({});

  // Prints:
  // in Person
  // in Employee
}

在调用父类构造函数前会检测参数,这个参数可以是一个表达式,如函数调用:

1
2
3
4
class Employee extends Person {
  // ...
  Employee() : super.fromJson(findDefaultData());
}

初始化列表

除了调用父类构造函数,你也可以在构造函数体运行之前初始化实例变量。用逗号隔开使其分别初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // 初始化列表在构造函数运行前设置实例变量。

  Point.fromJson(Map jsonMap)
      : x = jsonMap['x'],
        y = jsonMap['y'] {
    print('In Point.fromJson(): ($x, $y)');
  }
}

重定向构造函数

有时一个构造函数的目的只是重定向到同一个类中的另一个构造函数。如果一个重定向的构造函数的主体为空,那么调用这个构造函数的时候,直接在冒号后面调用这个构造函数即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Point {
  num x;
  num y;

  // 这个类的主构造函数。
  Point(this.x, this.y);

  // 主构造函数的代表。
  Point.alongXAxis(num x) : this(x, 0);
}

静态构造函数

如果你的类产生的对象永远不会改变,你可以让这些对象成为编译时常量。为此,需要定义一个 const 构造函数并确保所有的实例变量都是 final 的。

1
2
3
4
5
6
7
class ImmutablePoint {
  final num x;
  final num y;
  const ImmutablePoint(this.x, this.y);
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);
}

工厂构造函数

当实现一个使用 factory 关键词修饰的构造函数时,这个构造函数不必创建类的新实例。例如,工厂构造函数可能从缓存返回实例,或者它可能返回子类型的实例。 下面的示例演示一个工厂构造函数从缓存返回的对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Logger {
  final String name;
  bool mute = false;

  // _cache 是一个私有库,幸好名字前有个 _ 。 
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = new Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) {
      print(msg);
    }
  }
}

实例方法

对象的实例方法可以访问实例变量和 this 。以下示例中的 distanceTo() 方法是实例方法的一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import 'dart:math';

class Point {
  num x;
  num y;
  Point(this.x, this.y);

  num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

setters 和 Getters

是一种提供对方法属性读和写的特殊方法。每个实例变量都有一个隐式的 getter 方法,合适的话可能还会有 setter 方法。你可以通过实现 getters 和 setters 来创建附加属性,也就是直接使用 get 和 set 关键词:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Rectangle {
  num left;
  num top;
  num width;
  num height;

  Rectangle(this.left, this.top, this.width, this.height);

  // 定义两个计算属性: right and bottom.
  num get right             => left + width;
      set right(num value)  => left = value - width;
  num get bottom            => top + height;
      set bottom(num value) => top = value - height;
}

main() {
  var rect = new Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

借助于 getter 和 setter ,你可以直接使用实例变量,并且在不改变客户代码的情况下把他们包装成方法。

抽象方法

Instance , getter 和 setter 方法可以是抽象的,也就是定义一个接口,但是把实现交给其他的类。要创建一个抽象方法,使用分号(;)代替方法体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
abstract class Doer {
  // ...定义实例变量和方法...

  void doSomething(); // 定义一个抽象方法。
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // ...提供一个实现,所以这里的方法不是抽象的...
  }
}

重载操作符

你可以重写在下表中列出的操作符。例如,如果你定义了一个向量类,你可以定义一个 + 方法来加两个向量。

+|[]
>/^[]=
~/&~
>=*==
-%»

以下是一个类中重写 +- 操作符的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Vector {
  final int x;
  final int y;
  const Vector(this.x, this.y);

  /// Overrides + (a + b).
  Vector operator +(Vector v) {
    return new Vector(x + v.x, y + v.y);
  }

  /// Overrides - (a - b).
  Vector operator -(Vector v) {
    return new Vector(x - v.x, y - v.y);
  }
}

main() {
  final v = new Vector(2, 3);
  final w = new Vector(2, 2);

  // v == (2, 3)
  assert(v.x == 2 && v.y == 3);

  // v + w == (4, 5)
  assert((v + w).x == 4 && (v + w).y == 5);

  // v - w == (0, 1)
  assert((v - w).x == 0 && (v - w).y == 1);
}

如果你重写了 == ,你也应该重写对象中 hashCode 的 getter 方法。

抽象类

使用 abstract 修饰符来定义一个抽象类,该类不能被实例化。抽象类在定义接口的时候非常有用,实际上抽象中也包含一些实现。如果你想让你的抽象类被实例化,请定义一个 工厂构造函数。

抽象类通常包含 抽象方法。下面是声明一个含有抽象方法的抽象类的例子:

1
2
3
4
5
6
// 这个类是抽象类,因此不能被实例化。
abstract class AbstractContainer {
  // ...定义构造函数,域,方法...

  void updateChildren(); // 抽象方法。
}

下面的类不是抽象类,因此它可以被实例化,即使定义了一个抽象方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class SpecializedContainer extends AbstractContainer {
  // ...定义更多构造函数,域,方法...

  void updateChildren() {
    // ...实现 updateChildren()...
  }

  // 抽象方法造成一个警告,但是不会阻止实例化。
  void doSomething();
}

隐式接口

每个类隐式的定义了一个接口,含有类的所有实例和它实现的所有接口。如果你想创建一个支持类 B 的 API 的类 A,但又不想继承类 B ,那么,类 A 应该实现类 B 的接口。

一个类实现一个或更多接口通过用 implements 子句声明,然后提供 API 接口要求。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 一个 person ,包含 greet() 的隐式接口。
class Person {
  // 在这个接口中,只有库中可见。
  final _name;

  // 不在接口中,因为这是个构造函数。
  Person(this._name);

  // 在这个接口中。
  String greet(who) => 'Hello, $who. I am $_name.';
}

//  Person 接口的一个实现。
class Imposter implements Person {
  // 我们不得不定义它,但不用它。
  final _name = "";

  String greet(who) => 'Hi $who. Do you know who I am?';
}

greetBob(Person person) => person.greet('bob');

main() {
  print(greetBob(new Person('kathy')));
  print(greetBob(new Imposter()));
}

这里是具体说明一个类实现多个接口的例子:

1
2
3
class Point implements Comparable, Location {
  // ...
}

扩展一个类

使用 extends 创建一个子类,同时 supper 将指向父类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ...
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ...
}

子类可以重载实例方法, getters 方法, setters 方法。下面是个关于重写 Object 类的方法 noSuchMethod() 的例子,当代码企图用不存在的方法或实例变量时,这个方法会被调用。

1
2
3
4
5
6
7
class A {
  // 如果你不重写 noSuchMethod 方法, 就用一个不存在的成员,会导致 NoSuchMethodError 错误。
  void noSuchMethod(Invocation mirror) {
    print('You tried to use a non-existent member:' +
          '${mirror.memberName}');
  }
}

你可以使用 @override 注释来表明你重写了一个成员。

1
2
3
4
5
6
class A {
  @override
  void noSuchMethod(Invocation mirror) {
    // ...
  }
}

如果你用 noSuchMethod() 实现每一个可能的 getter 方法,setter 方法和类的方法,那么你可以使用 @proxy 标注来避免警告。

1
2
3
4
5
6
@proxy
class A {
  void noSuchMethod(Invocation mirror) {
    // ...
  }
}

枚举类型

枚举类型,通常被称为 enumerations 或 enums ,是一种用来代表一个固定数量的常量的特殊类。声明一个枚举类型需要使用关键字 enum

1
2
3
4
5
enum Color {
  red,
  green,
  blue
}

在枚举中每个值都有一个 index getter 方法,它返回一个在枚举声明中从 0 开始的位置。例如,第一个值索引值为 0 ,第二个值索引值为 1 。

1
2
3
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

要得到枚举列表的所有值,可使用枚举的 values 常量。

1
2
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);

你可以在 switch 语句 中使用枚举。如果 e 在 switch (e) 是显式类型的枚举,那么如果你不处理所有的枚举值将会弹出警告:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
enum Color {
  red,
  green,
  blue
}
// ...
Color aColor = Color.blue;
switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // Without this, you see a WARNING.
    print(aColor);  // 'Color.blue'
}

为类添加特征:mixins

mixins 是一种多类层次结构的类的代码重用。

要使用 mixins ,在 with 关键字后面跟一个或多个 mixin 的名字。下面的例子显示了两个使用mixins的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Musician extends Performer with Musical {
  // ...
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

要实现 mixin ,就创建一个继承 Object 类的子类,不声明任何构造函数,不调用 super 。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
abstract class Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

静态变量

使用 static 关键字来实现类变量和类方法。静态变量(类变量)对于类状态和常数是有用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Color {
  static const red =
      const Color('red'); // 一个恒定的静态变量
  final String name;      // 一个实例变量。 
  const Color(this.name); // 一个恒定的构造函数。
}

main() {
  assert(Color.red.name == 'red');
}

只有当静态变量被使用时才被初始化。

静态方法

静态方法(类方法)不在一个实例上进行操作,因而不必访问 this 。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:math';

class Point {
  num x;
  num y;
  Point(this.x, this.y);

  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

main() {
  var a = new Point(2, 2);
  var b = new Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(distance < 2.9 && distance > 2.8);
}

你可以将静态方法作为编译时常量。例如,你可以把静态方法作为一个参数传递给静态构造函数。

泛型

如果你在 API 文档寻找基本数组类型或者 List 类型,你将会看到该类型实际上为 List<E>,其中 <...> 标记表示此表为一个泛型类型(或为参数化结构)—— 一种含有正规类型参数的类型。按照惯例,类型变量通常为单字符名称,例如 E, T, S, K, 以及 V。

为何要使用泛型

因为在Dart中类型是可选的,你不一定使用泛型。或许你想用,可是,因为一些相同的原因你会想在代码中使用其他的类型:这些类型(泛型或者其他类型)可以注释你的文档以及代码,使你的意图更加清晰。

比如,如果你打算使用一个仅仅包含字符串的 List,你可以声明它为List(可理解为 “字符串类型组成的 List”),通过这种方式,你的程序员同事,以及你的工具(比如 Dart 编辑器和调试模式下的 Dart 虚拟机)能检测到将一个非字符串的变量分配到 List 中很可能是错误的,这里给出一个样例:

1
2
3
4
var names = new List<String>();   
names.addAll([‘Seth’,’Kathy’,’Lars’]);  
//...   
names.add(42); //在调试模式中失败 (在生产模式中成功).  

另外一个使用泛型的原因是为了减少代码的重复。泛型可以让你能共享多个类型的一个接口和实现方式,它在调试模式以及静态分析的错误预警中仍然很有优势。举个例子,当你在创建一个接口来缓存一个对象时:

1
2
3
4
abstract class ObjectCache{   
  object getByKey(String key);   
  setByKey(String key,Object value);   
}   

你发现你想要一个字符串专用的接口,所以你创建了另外一个接口:

1
2
3
4
abstract class StringCache{
  string getByKey(String key);
  setByKey(String key,String value);
}

接下来,你决定你想要一个这种接口的数字专用的接口…你想到了这个方法.泛型类型可以减少你创建这些接口的困难。取而代之的是,你只需要创建一个带有一个类型参数的接口即可:

1
2
3
4
abstract class Cache<T>{
  T getByKey(String key);
  setByKey(String key,T value);
}

在这个代码中,T是一个替代类型,即占位符,你可以将他视为后续被开发者定义的类型。

使用集合常量

Lis 常量以及 map 常量都能被参数化,参数常量就像你已经见过的常量那样,除非你在左 方括号之前添加 <type> (对于 List)或者 <keyType,valuetype> (对于 map)。当你需要避免调试模式下的类型警告,你或许可以使用参数常量。这里有一个使用常量类型的例子:

1
2
3
4
5
6
var names = <String>[‘Seth’,’Kathy’,’Lars’];
var pages = <String,String>{
  ‘index.html’ : ‘homepage’,
  ‘robots.txt’ : ‘Hints for web robots’,
  ‘humans.txt’ : ‘We are people,not machines’
};

使用带构造器的参数化类型

为了在使用构造器时详细说明一个或多个类型,将类型放入类名后的三角括号 <...> 中,举个例子:

1
2
3
var names = new List<String>();
names.addAll([‘Seth’, ‘Kathy’ , ‘Lars’]);
var nameSet = new Set<String>.from(names);

下列代码创建了一个含有整型的键以及值为 View 的 map:

1
var views = new Map<int,view>();

泛型集合及其包含的类型

Dart 泛型类型是被具体化的,意思就是它们在整个运行时间中都携带着类型信息。举个例子,你可以测试一个集合中的类型甚至是在生产模式中:

1
2
3
var names = new List<String>();
names.addAll([‘Seth’, ‘Kathy’, ‘Lars’]);
print(names is List<String>); // true

然而,上述表达式检查的仅仅是集合中的类型–并不是其中的对象。在生产模式下,一个 List<String> 中可能含有一些非字符项,解决方法可以是逐项检查其类型或者在异常处理程序中加入数据项操作代码。