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;
使用构造函数
你可以使用”构造函数“创建一个对象。构造函数的名字可以是 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});
版本说明:关键词 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 方法。
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; } }
记住构造函数不被继承,意味着父类的命名构造函数不会被子类继承。如果你希望用父类中的命名构造函数创建子类,你必须在子类中实现该构造函数。
调用父类的非默认构造函数
默认地,子类的构造函数会调用父类的无名、无参构造函数。父类的构造函数会在构造函数体的一开始被调用。如果 初始化列表 也被使用了,它在父类被调用之前调用。总结下来,执行的顺序如下:
- 初始化列表
- 父类的无参构造函数
- 主类的无参构造函数
如果父类没有无名、无参的构造函数,那么你必须手动调用父类的其中一个构造函数。在冒号 (:) 后面,构造函数体之前(如果有的话)指定父类的构造函数。
下面的例子中,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');