Dart 类

742 阅读9分钟

Dart 是支持基于 mixin 继承机制的面向对象语言

  • 所有对象都是一个类的实例,而所有的类都继承自 Object 类。
  • 基于 mixin 的继承 意味着每个除 Object 类之外的类都只有一个超类,一个类的代码可以在其它多个类继承中重复使用
  • Extension 方法是一种在不更改类或创建子类的情况下向类添加功能的方式

使用类的成员

对象的 成员 由函数和数据(即 方法 和 实例变量)组成。方法的 调用 要通过对象来完成,这种方式可以访问对象的函数和数据。 使用 . 来访问对象的实例变量或方法:

import 'dart:math';

main(List<String> args) {
  var p = Point(2, 2);
  // 为实例变量 y 赋值。
  p.y = 3;
// 获取 y 的值。
  assert(p.y == 2);
// 调用变量 p 的 distanceTo() 方法。
  num distance = p.distanceTo(Point(4, 4));
  print(distance);
}

使用 ?. 代替 .可以避免因为左边表达式为 null 而导致的问题:

// 如果 p 为非空则将其属性 y 的值设为 4. If p is non-null, set its y value to 4.
p?.y = 4;

使用构造函数

可以使用 构造函数 来创建一个对象。 构造函数的命名方式可以为 类名(ClassName) 或 类名.标识符ClassName.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});

⚠️** 版本提示: **从 Dart 2 开始,new 关键字是可选的

一些类提供了常量构造函数。使用常量构造函数,在构造函数名之前加 const 关键字,来创建编译时常量时:

var p = const ImmutablePoint(2, 2);

两个使用相同构造函数相同参数值构造的编译时常量是同一个对象:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是同一个实例 (They are the same instance!)

根据使用 常量上下文 的场景,你可以省略掉构造函数或字面量前的 const 关键字。例如下面的例子中我们创建了一个常量 Map:

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

根据上下文,你可以只保留第一个 const 关键字,其余的全部省略:

// Only one const, which establishes the constant context.
// 只需要一个 const 关键字,其它的则会隐式地根据上下文进行关联。
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

但是如果无法根据上下文判断是否可以省略 cosnt,则不能省略掉 const 关键字,否则将会创建一个 非常量对象 例如:

var a = const ImmutablePoint(1, 1); // 创建一个常量 (Creates a constant)
var b = ImmutablePoint(1, 1); // 不会创建一个常量 (Does NOT create a constant)
assert(!identical(a, b)); // 这两变量并不相同 (NOT the same instance!)

⚠️**版本提示: **只有从 Dart 2 开始才能根据上下文判断省略 const 关键字

获取对象的类型

可以使用 Object 对象的 runtimeType 属性在运行时获取一个对象的类型,该对象类型是 Type 的实例

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

到目前为止,我们已经解了如何使用类。本节的其余部分将向你介绍如何实现一个类

实例变量

下面是声明实例变量的示例:

class Point {
  num x; // 声明实例变量 x 并初始化为 null。
  num y; // 声明实例变量 y 并初始化为 null。
  num z = 0; // 声明实例变量 z 并初始化为 0。
}

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

所有实例变量均会隐式地声明一个 Getter 方法,非 final 类型的实例变量还会隐式地声明一个 Setter 方法。你可以查阅 Getter 和 Setter 获取更多相关信息

class Point {
  num _x;
  num _y;
  set x(num value) =>_x = value;
  set y(num value){_y = value;print("正在设置y.setter");}

  num get x => _x;
  num get y{print("正在设置y.getter");return _y;}
}


void main() {
  var point = Point();
  point.y = 4; // 使用 x 的 Setter 方法。
  print(point.y);
  point.x = 4; // 使用 x 的 Setter 方法。
  assert(point.x == 3); // 使用 x 的 Getter 方法。
}
class Rectangle {
  num left, top, width, height;

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

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

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

如果你在声明一个实例变量的时候就将其初始化(而不是在构造函数或其它方法中),那么该实例变量的值就会在对象实例创建的时候被设置,该过程会在构造函数以及它的初始化器列表执行前

构造函数

声明一个与类名一样的函数即可声明一个构造函数(对于命名式构造函数还可以添加额外的标识符)。大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:

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

默认构造函数

如果你没有声明构造函数,那么 Dart 会 自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法

构造函数不被继承

子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数

命名式构造函数

可以为一个类声明多个命名式构造函数来表达更明确的意图:

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 does not have a default constructor;
  // you must call super.fromJson(data).
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

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

  // Prints:
  // in Person
  // in Employee
  if (emp is Person) {
    // Type check
    emp.firstName = 'Bob';
  }
  (emp as Person).firstName = 'Bob';
}

因为参数会在子类构造函数被执行前传递给父类的构造函数,因此该参数也可以是一个表达式,比如一个函数:

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

请注意:传递给父类构造函数的参数不能使用 this 关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员都不能被访问,但是类成员可以。

初始化列表

除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。每个实例变量之间使用逗号分隔

// Initializer list sets instance variables before
// the constructor body runs.
// 使用初始化列表在构造函数体执行前设置实例变量。
Point.fromJson(Map<String, num> json)
    : x = json['x'],
      y = json['y'] {
  print('In Point.fromJson(): ($x, $y)');
}

**⚠️注意: **初始化列表表达式= 右边的语句不能使用 this 关键字

初始化中用assert语句,防止乱传参数
class Spacer extends StatelessWidget {
  const Spacer({Key key, this.flex = 1})
    : assert(flex != null),
      assert(flex > 0),
      super(key: key);

  final int flex;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      flex: flex,
      child: const SizedBox.shrink(),
    );
  }
}

在开发模式下,你可以在初始化列表中使用 assert 来验证输入数据:

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

对比常规构造函数传参

import 'dart:math';

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

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

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

重定向构造函数

有时候类中的构造函数会调用类中其它的构造函数,该重定向构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数即可:

class Point {
  num x, y;
  // 该类的主构造函数。
  Point(this.x, this.y);
  // 委托实现给主构造函数。
  Point.alongXAxis(num x) : this(x, 0);
}

常量构造函数

如果类生成的对象都是不会变的,那么可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能。

class ImmutablePoint {
  final num x, y;
  const ImmutablePoint(this.x, this.y);
  static final ImmutablePoint origin =const ImmutablePoint(0, 0);
}

常量构造函数创建的实例并不总是常量,具体可以参考使用构造函数章节

工厂构造函数

使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。 以下示例演示了从缓存中返回对象的工厂构造函数:

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


  //构建私有构造函数
  Logger._internal(this.name);

  // 方式1:工厂构造函数,这种方法每次调用会创建一次实例
  factory Logger(String name) => createInstance(name);
  static Logger createInstance(String name) => new Logger._internal(name);

  // 方式2: 工厂构造函数,保存单例实例的指向
	// 在工厂构造函数中无法访问 this
  //putIfAbsent是Map对象的方法,存在key就获取值,不存在则添加到map 然后返回值
  // _cache 变量是库私有的,因为在其名字前面有下划线。
  // static final Map<String, Logger> _cache = <String, Logger>{};
  // factory Logger(String name) {
  //   return _cache.putIfAbsent(name, () => Logger._internal(name));
  // }

  //方式3: 这种是创建单例,返回的都是第一次创建的实例
  // static var _singleton;
  // factory Logger(String name) {
  //   if (_singleton == null) {
  //     _singleton = Logger._internal(name);
  //   }
  //   return _singleton;
  // }

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

//工厂构造函的调用方式与其他构造函数一样:
void main(List<String> args) {
  var logger1 = Logger('UI');
  var logger2 = Logger('UI');
  var logger3 = Logger('这是不同名字的,会输出不同的hashCode');
  print(logger1.hashCode);
  print(logger2.hashCode);
  print(logger3.hashCode);
  print(logger3.name);
  print(identical(logger1, logger2));
  logger1.log('Button clicked');
}
  • 备忘:在工厂构造函数中无法访问 this

工厂函数和构造函数对比

构造函数
class Symbol {
  static final Map<String, Symbol> cache = {};
  final String name;
  
  Symbol._internal(this.name);
  
  Symbol(this.name) {
    cache[name] = new Symbol._internal(this.name);
  }

 
}

main() {
  var a = new Symbol('something');
  var b = new Symbol('something');
  print(identical(a, b)); // false!
  print(Symbol.cache); //{something: Instance of 'Symbol'}
}
工厂函数
class Symbol {
  final String name;
  static Map<String, Symbol> _cache = new Map<String, Symbol>();

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

  
}


main() {
  var x = new Symbol('X');
  var alsoX = new Symbol('X');

  print(identical(x, alsoX));  // true
}

方法

方法是对象提供行为的函数

实例方法

对象的实例方法可以访问实例变量和 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);
  }
}

Getter 和 Setter

Getter 和 Setter 是一对用来读写对象属性的特殊方法,上面说过实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法,你可以使用 get 和 set 关键字为额外的属性添加 Getter 和 Setter 方法:

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

使用 Getter 和 Setter 的好处是,你可以先使用你的实例变量,过一段时间过再将它们包裹成方法且不需要改动任何代码,即先定义后更改且不影响原有逻辑

备忘:像自增(++)这样的操作符不管是否定义了 Getter 方法都会正确地执行。为了避免一些不必要的异常情况,运算符只会调用 Getter 一次,然后将其值存储在一个临时变量中

抽象方法

实例方法、Getter 方法以及 Setter 方法都可以是抽象的,定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于抽象类中 直接使用分号;替代方法体即可声明一个抽象方法:

abstract class Doer {
  // 定义实例变量和方法等等……
  void doSomething(); // 定义一个抽象方法。
}

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

抽象类

使用关键字 abstract 标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时可被实例化,可以为其定义工厂构造函数。 抽象类常常会包含抽象方法。下面是一个声明具有抽象方法的抽象类示例:

// This class is declared abstract and thus
// can't be instantiated.
// 该类被声明为抽象的,因此它不能被实例化。
abstract class AbstractContainer {
  // 定义构造函数、字段、方法等……
  void updateChildren(); // 抽象方法。
}

隐式接口

每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。 一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:

// A person. The implicit interface contains greet().
// Person 类的隐式接口中包含 greet() 方法。
class Person {
  // _name 变量同样包含在接口中,但它只是库内可见的。
  final _name;

  // 构造函数不在接口中。
  Person(this._name);

  // greet() 方法在接口中。
  String greet(String who) => '你好,$who。我是$_name。';
}

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

  String greet(String who) => '你好$who。你知道我是谁吗?';
}

String greetBob(Person person) => person.greet('陌生人');

void main() {
  print(greetBob(Person('小芸')));
  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() {...}
  // ···
}

限定方法参数以及实例变量的类型可以让代码更加类型安全,你可以使用协变关键字

重写运算符

可以在一个类中重写下表所罗列出的所有运算符。比如如果定一个 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);
  // 运算符 == 和 hashCode 的实现未在这里展示,详情请查看下方说明。
  // ···
}
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 方法。你可以查阅实现映射键获取更多关于重写的 == 和 hashCode 的例子 你也可以查阅扩展一个类获取更多关于重写的信息

noSuchMethod()

如果调用了对象上不存在的方法或实例变量将会触发 noSuchMethod 方法,你可以重写 noSuchMethod 方法来追踪和记录这一行为:

class A {
  // 除非你重写 noSuchMethod,否则调用一个不存在的成员会导致 NoSuchMethodError。
  @override
  void noSuchMethod(Invocation invocation) {
  print('你尝试使用一个不存在的成员:' +
  '${invocation.memberName}');
  }
}

你不能调用一个未实现的方法除非下面其中的一个条件成立:

  • 接收方是静态的 dynamic 类型
  • 接收方具有静态类型,定义了未实现的方法(抽象亦可),并且接收方的动态类型实现了 noSuchMethod 方法且具体的实现与 Object 中的不同

你可以查阅 noSuchMethod 转发规范获取更多相关信息

Extension 方法

Dart 2.7 中引入的 Extension 方法是向现有库添加功能的一种方式。你可能甚至都不知道有 Extension 方法。例如,当您在 IDE 中使用代码完成功能时,它建议将 Extension 方法与常规方法一起使用 这里是一个在 String 中使用 extension 方法的样例,我们取名为 parseInt(),它在 string_apis.dart 中定义:

import 'string_apis.dart';
...
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.

有关使用以及实现 extension 方法的详细信息,请参阅 extension methods 页面

枚举类型

枚举类型是一种特殊的类型,也称为 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 语句中使用枚举,但是需要注意的是必须处理枚举值的每一种情况,即每一个枚举值都必须成为一个 case 子句,不然会出现警告:

var aColor = Color.blue;
switch (aColor) {
  case Color.red:
    print('红如玫瑰!');
    break;
  case Color.green:
    print('绿如草原!');
    break;
  default: // 没有该语句会出现警告。
    print(aColor); // 'Color.blue'
}

枚举类型有如下两个限制:

  • 枚举不能成为子类,也不可以 mixin,你也不可以实现一个枚举。
  • 不能显式地实例化一个枚举类。

你可以查阅 [Dart 编程语言规范][]获取更多相关信息。

使用 Mixin 为类添加功能

Mixin 是一种在多重继承中复用某个类中代码的方法模式

使用 with 关键字并在其后跟上 Mixin 类的名字来使用 Mixin 模式:

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 类:

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 类,比如有 Mixin 类 A,但是 A 只能被 B 类使用,则可以这样定义 A:

mixin MusicalPerformer on Musician {
  // ···
}

版本提示:mixin 关键字在 Dart 2.1 中才被引用支持。早期版本中的代码通常使用 abstract class 代替。你可以查阅 Dart SDK 变更日志2.1 mixin 规范 获取更多有关 Mixin 在 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);
}

备忘:对于一些通用或常用的静态方法,应该将其定义为顶级函数而非静态方法。 可以将静态方法作为编译时常量。例如,你可以将静态方法作为一个参数传递给一个常量构造函数。