优质
小牛编辑
142浏览
2023-12-01

Dart 是一门面向对象的编程语言,具备类和基于混入的继承。

每一个对象都是一个类的实例,而所有的类都派生自 Object。“基于混入的继承”意味着虽然每个类(除了 Object)都只有一个父类,但类的主体可以在多个类层级中被复用。

使用类成员

对象包含由函数和数据(分别是“方法”和“实例变量“)组成的“成员”。当你调用一个方法时,你在一个对象上”调用“:这个方法可以访问该对象的函数和数据:

使用一个点 (.) 来引用实例变量或方法:

var p = Point(2, 2);

// 设置实例变量 y 的值
p.y = 3;

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

// 调用 p 的 distanceTo() 方法
num distance = p.distanceTo(Point(4, 4));

使用 ?. 代替 . 来避免当左操作数为空时会引发的异常:

// 如果 p 是非空值,设置它的 y 值为 4
p?.y = 4;

使用构造函数

你可以使用”构造函数“创建一个对象。构造函数的名字可以是 ClassNameClassName.identifier。比如,下面的代码使用 Point()Point.fromJson() 构造函数创建了 Point 对象:

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

下面的代码具有相同的效果,但是在构造函数前使用了可选的 new 关键词:

var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

版本说明:关键词 new 在 Dart 2 中变成了可选的。

一些类提供 常量构造函数。要使用构造函数创建一个编译期常量,在构造函数名前面加上 const 关键词:

var p = const ImmutablePoint(2, 2);

构造两个相同的编译期常量结果会是同一个、标准的实例:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // 它们是同一个实例

在一个”常量上下文“中,你可以省略构造函数或字面量前的 const。比如,下面代码中,创建常量映射:

// 这里有很多 const 关键词
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

除了第一个以外,你可以省略其他所有的 const 关键词:

// 只有一个 const,它创建了常量上下文
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

如果一个常量构造函数在常量上下文之外并且没有使用 const 来调用,它会创建一个 非常量对象

var a = const ImmutablePoint(1, 1); // 创建一个常量
var b = ImmutablePoint(1, 1); // 不会创建一个常量

assert(!identical(a, b)); // 不是同一个实例!

版本说明:常量上下文中的 const 关键词在 Dart 2 中变成了可选的。

获取对象类型

要获取一个对象的类型,你可以使用对象的 runtimeType 属性,会返回一个 Type 对象。

print('The type of a is ${a.runtimeType}');

到此为止,你已经看到了如何”使用“类。本章余下的内容会展示如何”实现“类。

实例变量

你可以使用如下方式声明实例变量:

class Point {
  num x; // 声明一个实例变量,初始值为 null
  num y; // 声明 y,初始值为 null
  num z = 0; // 声明 z,初始值为 0
}

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

所有的实例变量都生成隐式的 getter 方法。非 final 的实例变量同时生产一个隐式的 setter 方法。详情请参阅 Getter 和 setter。

class Point {
  num x;
  num y;
}

void main() {
  var point = Point();
  point.x = 4; // 使用 x 的 setter 方法
  assert(point.x == 4); // 使用 x 的 getter 方法
  assert(point.y == null); // 默认值为 null
}

如果你在声明的时候初始化实例变量(而不是在构造函数或者方法里),值会在实例创建的时候被设置,在构造函数和它的初始化列表执行前。

构造函数

通过创建一个和类名一样(或者类名加上一个可选的、额外的标识符作为class Point { num x, y; Point(num x, num y) { // 有更好的实现方式,请看下文分解 this.x = x; this.y = y; } }

关键词 this 引用当前实例。

说明:仅当有命名冲突时使用 this。否则,Dart 的风格是省略 this

将构造函数的参数赋值给一个实例变量,这种模式是如此常见,因此,Dart 有语法糖来简化操作:

class Point {
  num x, y;

  // 设置 x 和 y 的语法糖
  // 在构造函数体之前执行
  Point(this.x, this.y);
}

默认构造函数

如果你没有声明构造函数,一个默认构造函数会提供给你。默认构造函数没有参数,并且调用父类的无参构造函数。

构造函数不被继承

子类不会继承父类的构造函数。一个没有声明构造函数的子类只拥有默认的(无参、无名字的)构造函数。

命名构造函数

使用命名构造函数来实现多个构造函数或者让代码更清晰:

class Point {
  num x, y;

  Point(this.x, this.y);

  // 命名构造函数
  Point.origin() {
    x = 0;
    y = 0;
  }
}

记住构造函数不被继承,意味着父类的命名构造函数不会被子类继承。如果你希望用父类中的命名构造函数创建子类,你必须在子类中实现该构造函数。

调用父类的非默认构造函数

默认地,子类的构造函数会调用父类的无名、无参构造函数。父类的构造函数会在构造函数体的一开始被调用。如果 初始化列表 也被使用了,它在父类被调用之前调用。总结下来,执行的顺序如下:

  1. 初始化列表
  2. 父类的无参构造函数
  3. 主类的无参构造函数

如果父类没有无名、无参的构造函数,那么你必须手动调用父类的其中一个构造函数。在冒号 (:) 后面,构造函数体之前(如果有的话)指定父类的构造函数。

下面的例子中,Employee 类的构造函数调用了它父类 Person 的命名构造函数。

class Person {
  String firstName;

  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({});

  // 打印:
  // in Person
  // in Employee
  if (emp is Person) {
    // 类型检查
    emp.firstName = 'Bob';
  }
  (emp as Person).firstName = 'Bob';
}

由于父类构造函数的参数在构造函数调用前被计算,参数可以是一个表达式比如一个函数调用:

class Employee extends Person {
  Employee() : super.fromJson(getDefaultData());
  // ···
}

警告:父类的构造函数不能访问 this。因此,参数可以是静态方法但是不能是实例方法。

初始化列表

调用父类构造函数的同时,你也可以在构造函数体执行之前初始化实例变量。使用逗号分隔初始化器。

// 初始化列表在构造函数体执行前设置实例变量的值
Point.fromJson(Map<String, num> json)
    : x = json['x'],
      y = json['y'] {
  print('In Point.fromJson(): ($x, $y)');
}

警告:初始化器右边不能访问 this

在开发阶段,你可以在初始化列表中使用 assets 验证输入。

Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

初始化列表是设置 final 属性的方便方法。下面的例子在初始化列表中初始了三个 final 属性。

import 'dart:math';

class Point {
  final num x;
  final num y;
  final num distanceFromOrigin;

  Point(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

main() {
  var p = new Point(2, 3);
  print(p.distanceFromOrigin);
}

重定向构造函数

有时候一个构造函数的唯一目的是重定向到同一个类的另一个构造函数。一个重定向构造函数的函数体是空的,构造函数的调用在冒号 (:) 后面。

class Point {
  num x, y;

  // 该类的主调用函数
  Point(this.x, this.y);

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

常量构造函数

如果你的类生成的对象从不改变,你可以让这些对象变成编译期常量。要想这样,定义一个常量构造函数并确保所有实例变量都是 final 的。

class ImmutablePoint {
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);

  final num x, y;

  const ImmutablePoint(this.x, this.y);
}

常量构造函数并不总是会创建常量。要了解详情,请看 使用构造函数 章节。

工厂构造函数

当要实现一个不总是创建这个类新实例的构造函数时,使用 factory 关键词。比如,一个工厂构造函数可能从缓存中返回一个实例,或者可能返回子类的一个实例。

下面的代码展示了一个工厂构造函数从缓存中返回对象:

class Logger {
  final String name;
  bool mute = false;

  // _cache 是库内私有的,多亏了它名字前的 _
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  Logger._internal(this.name);

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

说明:工厂构造函数无法访问 this

调用工厂构造函数的方式和其他构造函数一样:

var logger = Logger('UI');
logger.log('Button clicked');

方法

方法指那些为一个对象提供行为的函数。

实例方法

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

import 'dart:math';

class Point {
  num x, 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);
  }
}

Getters 和 setters

Getters 和 setters 是为一个对象的属性提供读写权限的特殊方法。回想每一个实例变量都有一个隐式的 getter,符合条件的还会有一个 setter。你可以通过实现 getters 和 setters 创建额外的属性,使用 getset 关键词:

class Rectangle {
  num left, top, width, height;

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

  // 定义两个计算属性:right 和 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;
}

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

通过 getters 和 setters,你可以从实例变量起步,之后使用方法封装它们,整个过程不需要修改代码。

说明:无论是否明确定义 getter,像自增 (++) 这样的表达式都会以预期的方式执行。为避免任何非预期的副作用,运算符只会调用 getter 一次,然后保存它的值到一个临时变量中。

抽象方法

实例方法、getter 和 setter 都可以是抽象的,这样即定义了一个接口但是把它的实现留给其他类。抽象方法只能存在于 抽象类 中。

要使一个方法变得抽象,使用分号 (;) 代替方法体:

abstract class Doer {
  // 定义实例变量和方法...

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

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

抽象类

使用 abstract 修饰符定义一个“抽象”类——一个不能被实例化的类。抽象类在定义接口时是有用的,通常附带一些实现。如果你想让你的抽象类变成可实例化的,定义一个 工厂构造函数。

抽象类经常包含 抽象方法。下面是一个定义抽象类的例子,它包含一个抽象方法:

// 该类定义为抽象的,因此无法被实例化
abstract class AbstractContainer {
  // 定义构造函数、属性、方法...

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

隐式接口

每一个类都隐式地定义了一个包括它所有实例成员和它实现的任意接口的接口。如果你希望创建一个类 A 来支持类 B 的 API 但是又不继承 B 的实现,类 A 应该实现 B 接口。

一个类要实现一个或多个接口,在 implements 子句中定义它们然后提供这些接口需要的 API 的实现。比如:

// Person 类。隐式接口包含 greet()
class Person {
  // 在接口中,但是只在这个库中可见
  final _name;

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

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

// 一个 Person 接口的实现
class Impostor implements Person {
  get _name => '';

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

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

下面是一个类实现多个接口的例子:

class Point implements Comparable, Location {...}

继承类

使用 extends 来创建一个子类,使用 super 来引用父类:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

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

重写类成员

子类可以重写实例方法、getter 和 setter。你可以使用 @override 注解来表明你想要重写一个成员:

class SmartTelevision extends Television {
  @override
  void turnOn() {...}
  // ···
}

要缩小一个方法参数或者实例变量的类型并写出 类型安全 的代码,你可以使用 covariant 关键词。

重载运算符

你可以重载下表中展示的运算符。比如,如果你定义一个 Vector(矢量)类,你可能会定义一个 + 方法来加两个矢量。

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

说明:你可能注意到 != 不是一个可重载的运算符。表达式 e1 != e2 仅仅是 !(e1 == e2) 的语法糖。

下面是一个重载 +- 运算符的例子:

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  // 运算符 == 和 hasCode 没有展示。详情请看下面的说明
  // ···
}

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

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

如果你重载 ==,你也需要重载对象的 hashCode getter。有关重载 ==hasCode 的例子,请参阅 实现映射的键

要了解更多关于重载的内容,一般来说,可以参阅 继承类。

noSuchMethod()

要检测或响应代码试图使用不存在的方法或实例变量的情况,你可以重写 noSuchMethod()

class A {
  // 除非你重写 noSuchMethod,不然使用
  // 不存在的成员会引发 NoSuchMethodError
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
}

不可以调用一个未实现的方法除非以下情况有一个成立:

  • 接收者有静态类型 dynamic
  • 接收者有一个定义了该未实现方法的静态类型(抽象也可),而且接收者的动态类型有不同于 ObjectnoSuchMethod() 的实现。

要了解更多信息,请参阅 noSuchMethod 跳转规范

枚举类型

枚举类型,通常被称作 enumerationsenums(枚举),是用来表示有固定数量的常量值的一种特殊类。

使用枚举

使用 enum 关键词声明一个枚举类型:

enum Color { red, green, blue }

枚举中的每一个值都有一个 index getter,返回枚举声明中基于0的位置索引。比如,第一个值有索引 0,而第二个值有索引 1.

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

要获取枚举中所有值的列表,使用枚举的 values 常量。

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

你可以在 switch 语句 中使用枚举,而且如果你没有处理所有的枚举值,你会得到一个警告:

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // 没有这个,你会看到一个警告
    print(aColor); // 'Color.blue'
}

枚举类型有以下限制:

  • 你不可以继承、混入或实现一个枚举。
  • 你不可以显式实例化一个枚举。

要了解更多内容,请参阅 Dart 语言规范

为类添加特性:混入

混入 (mixin) 是在类的多继承中复用类代码的一种方式。

要“使用”混入,使用 with 关键词跟着一个或多个混入的名字。下面的例子展示了两个使用混入的类:

class Musician extends Performer with Musical {
  // ···
}

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

要实现一个混入,创建一个类继承 Object,不声明构造函数。除非你希望 mixin 可用作常规类,否则请使用 mixin 关键字代替 class。比如:

mixin 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');
    }
  }
}

要指定只有某些类型可以使用这个混入——比如,这样你的混入就可以调用它没有定义的方法——使用 on 来指定所需的父类:

mixin MusicalPerformer on Musician {
  // ···
}

版本说明:对于 mixin 关键词的支持在 Dart 2.1 中被引入。之前版本的代码通常使用 abstract class 代替。要了解更多关于混入在 2.1 中的改变,请参阅 Dart SDK 变更日志2.1 混入规范

类变量和方法

使用 static 关键词来实现类级别的变量和方法。

静态变量

静态变量(类变量)对类级别的状态和常数是很有用的。

class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}

静态变量直到它们被使用才会初始化。

说明:该页面遵守 代码规范推荐 倾向于使用 ”小驼峰“来作为常量名。

静态方法

静态方法(类方法)不操作实例,因此不能访问 this。比如:

import 'dart:math';

class Point {
  num x, 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);
  }
}

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

说明:对常见或者广泛使用的实用工具和功能,考虑使用顶级函数,而不是静态方法。

你可以使用静态方法作为编译期常量。比如,你可以把静态方法作为一个常量构造函数的参数。