Dart 简明教程 - 06 - Classes

435 阅读7分钟

本系列教程大多翻译自 Dart 官网,因本人英语水平有限,可能有些地方翻译不准确,还请各位大佬斧正。如需转载请标明出处,谢绝 CSDN 爬虫党。

Classes(类)

Dart 是面向对象的语言,支持基于 mixin(混入)的继承方式。所有的对象都是类的实例化,所有的类都来自于 Object

基于混入的继承(mixin-based inheritance)意味着所有的类(除了 Object)都有一个超类,并且可以继承多个父类

类的成员(Using class members)

对象拥有 functionsdata(方法、实例化变量)。当你调用一个方法时,就是在调用一个对象:方法访问了对象的函数和数据。

使用 dot(.)去访问实例的变量或方法:

var p = Point(2, 2);

// Set the value of the instance variable y.
p.y = 3;

// Get the value of y.
assert(p.y == 3);

// Invoke distanceTo() on p.
num distance = p.distanceTo(Point(4, 4));

使用 .? 替代 . 去避免运算符左边的操作数是 null 引发的问题。

// If p is non-null, set its y value to 4.
p?.y = 4;

使用构造函数(Using constructors)

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

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

Version note: 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)); // They are the same instance!

在常量环境(constant context)里,你可以省略 constructorliteral 前面的 const,下面是创建一个 map 常量的代码:

// Lots of const keywords here.
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

如果常量构造函数离开了常量环境,并且没有 const 关键字去调用,则会创造一个非常量的对象(non-constant object):

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!

Version note: 常量环境的 const 关键字在 Dart 2 是可选的。

获取对象的类型(Getting an object’s type)

在运行时获取对象的类型,你可以使用 Object 的 runtimeType 属性,它会返回一个 Type 对象。

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

到这里,你已经基本了解怎么使用 Class 了,剩下的部分,将演示如何实例化 Class。

实例化变量(Instance variables)

这里展示了如何声明一个实例化的变量:

class Point {
  num x; // Declare instance variable x, initially null.
  num y; // Declare y, initially null.
  num z = 0; // Declare z, initially 0.
}

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

所有实例化变量都隐含了 getter 方法。非 final 的实例变量,同样隐含了 setter 方法。 更多详情,请移步 Getters and setters

class Point {
  num x;
  num y;
}

void main() {
  var point = Point();
  point.x = 4; // Use the setter method for x.
  assert(point.x == 4); // Use the getter method for x.
  assert(point.y == null); // Values default to null.
}

如果你在声明变量的地方实例化(除了在构造函数或方法中),其值是在构造函数及其初始化程序列表执行之前设置的。

构造函数(Constructors)

通过创建一个方法,方法名和他所属的 class 相同,来声明一个构造函数(此外,还可以用一个额外的标识符来表明是具名构造函数(Named constructors))

class Point {
  num x, y;

  Point(num x, num y) {
    // There's a better way to do this, stay tuned.
    this.x = x;
    this.y = y;
  }
}

this 关键字指向当前类的实例。

Note: 仅当存在名称冲突时,才使用 this。否则,Dart 的风格会忽略 this

因为实例化构造函数变量的情况大同小异,所以 Dart 准备了一个语法糖:

class Point {
  num x, y;

  // Syntactic sugar for setting x and y
  // before the constructor body runs.
  Point(this.x, this.y);
}

默认构造函数(Default constructors)

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

构造函数不能继承(Constructors aren’t inherited)

子类不能继承父类的构造函数。若子类未声明构造函数,就使用默认构造函数(无参、无名)。

具名构造函数(Named constructors)

使用具名构造函数来给 class 提供多个构造函数或让代码可读性更高:

class Point {
  num x, y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin() {
    x = 0;
    y = 0;
  }
}

记住,构造函数不能继承,这意味着父类的具名构造函数不会继承到子类。如果你想在子类创建一个已经在父类定义的具名构造函数,则必须在子类重新实现它。

从父类引入非默认构造函数

默认情况下,子类调用父类的构造函数是匿名的、无参的构造函数。父类构造函数在构造体创建前被调用。如果同时还有初始化列表,它会在父类调用之前执行。总而言之,执行的顺序如下。

  1. 初始化列表(initializer list)
  2. 父类无参构造函数(superclass’s no-arg constructor)
  3. 子类自己的无参构造函数(main class’s no-arg constructor)

如果父类没有匿名、无参的构造函数,那你必须手动调用父类的一个构造函数。在构造体中,将父类构造函数拼在冒号(:)后面:

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(getDefaultData());
  // ···
}

Warning: 这里的参数(指函数)不能使用 this,例如,参数可以使用静态方法,但不能使用实例化的方法。

初始化列表(initializer list)

除了引入父类构造函数,你还可以在构造体运行之前初始化实例变量。通过逗号(,)分隔:

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

Warning: 实例化时,右边的部分不能使用 this

在开发环境,你可以在初始化列表中使用 assert 验证传参:

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

当设置最终字段(final fields)时,初始化列表就非常有用了:

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 下的其他构造函数。重定向的构造函数体是空的,并将目标构造体放在冒号(:)后面:

class Point {
  num x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(num x) : this(x, 0);
}

void main() {
  var p1 = new Point(1, 2);
  var p2 = new Point.alongXAxis(4);
}

常量构造函数

如果 class 输出的对象永远不会改变,你可以让这些对象成为编译常量。 通过定义 const 构造函数,并确保所有的实例化变量都是 final 型的,来实现这个效果。

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

  final num x, y;

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

常量构造函数也并不总是创建常量,更多详情请移步 using constructors

工厂模式构造函数(Factory constructors)

当实例化构造函数的时候,并不想总是创建一个新的实例,使用 factory 关键字。例如,一个工厂构造函数可能返回的是缓存中的实例,或者返回一个子类型的实例。

下面的例子示范工厂构造函数如何从缓存中返回对象:

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

  // _cache is library-private, thanks to
  // the _ in front of its name.
  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);
  }
}

Note: 工厂构造函数不支持 this。

和其他构造函数一样引入工厂构造函数:

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

方法 (Methods)

方法就是给对象提供了表现行为的函数。

实例化方法 (Instance methods)

在对象中实例化方法,可以传入实参和 this

import 'dart:math';

class Point {
  num x, y;

  Point(this.x, this.y);

  // An example of an instance method
  num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

getters & setters

getter 和 setter 是特殊的方法,它们提供了 读取写入 对象属性的机会。

回顾之前的教程,每一个实例化变量都隐含了 getter 方法,如果情况允许,还会隐含 setter 方法。

你也可以使用 getset 来修改 getter 和 setter 的默认行为:

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

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

  // Define two calculated properties: 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;
}

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

通过 getset,你可以从实例变量开始,稍后使用方法包装它们,全程都不需要修改客户端代码。

Note: 像自增(++)这样的运算符会按照预期那样执行,无论 getter 是否 明确的定义过。为了避免任何未预料的效果,运算符只会被调用一次,并将结果存储为临时变量。

抽象方法(Abstract methods)

实例化、gettersetter 这些方法,都可以被抽象化,只是定义接口,但将其实现留给其他类。抽象方法只存在于抽象类(abstract classes)。

让一个方法抽象化,只需在方法体后面添加分号(;)(好奇怪的设定。。。)

abstract class Doer {
  // Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}

抽象类(Abstract classes)

使用 abstract 关键字来修饰 class,使之变成抽象类 - 一个不能被实例化的类。抽象类通常会带一些实现,在定义接口时会很有用。如果要将抽象类实例化,则要定义一个工厂构造函数。

抽象类通常带有抽象方法:

// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {
  // Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}

没有使用abstract 关键字的类,即使包含抽象方法,也可以被实例化。

class SpecializedContainer extends AbstractContainer {
  // ...Define more constructors, fields, methods...

  void updateChildren() {
    // ...Implement updateChildren()...
  }

  // Abstract method causes a warning but
  // doesn't prevent instantiation.
  void doSomething();
}

隐式接口(Implicit interfaces)

每一个类都隐式定义了一个接口,这个接口包含了该类的所有实例成员和任意接口的实现。如果你想创建一个 A 类,拥有 B 类的 API 然后又不想继承 B 类的实现,那么 A 类需要自己实现 B 类的接口。

一个类可以通过在 implements 语句中声明去实现一或多个接口,并且提供接口所需的 API 。

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
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()));
}

下面是通过一条 implements 语句实现多个接口的例子:

class Point implements Comparable, Location {...}

扩展类(Extending a class)

使用 extends 关键字来创建子类,使用 super 关键字来指向父类:

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

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

重写成员(Overriding members)

子类可以重写实例化、getters 和 setters 方法。你可以使用 @override 修饰符来标识你要重写的成员:

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

要在代码中缩小方法参数或实例变量的类型来确保类型安全(type safe),你可以使用 covariant 关键字。

class Animal {
  void chase(Animal x) { ... }
}

class Mouse extends Animal { ... }

class Cat extends Animal {
  void chase(covariant Mouse x) { ... }
}

重载运算符(Overridable operators)

你可以重载下列表格中出现的运算符。例如,你定义了一个 Vector 类,那你可能会宠幸定义 + 来相加两个矢量(add two vectors)。

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

Note; 你可能注意到 != 不是一个可重载的符号。 表达式 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);

  // Operator == and hashCode not shown. For details, see note below.
  // ···
}

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

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

如果你重载了 ==, 你应该也需要重载 Object 的 hashCode 的 getter 方法。

重载 ==hashCode的例子,详情请移步 Implementing map keys

更多关于重载的更多详情,请移步 Extending a class

noSuchMethod()

为了检测或响应代码中尝试使用一个不存在的方法或变量的情况,你可以重载 noSuchMethod()

class A {
  // Unless you override noSuchMethod, using a
  // non-existent member results in a NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
}

你不能调用一个未实现的方法,除非满足下列条件之一:

  • 接收器拥有静态类型 dynamic
  • 接收器拥有一个未实现的静态类型方法(抽象的也可以),并且动态类型的接收器拥有一个实现了的、与 Object 类不同的 noSuchMethod() 方法。

更多详情,请移步 noSuchMethod forwarding specification

枚举类型(Enumerated types)

枚举类型,通常称为 enumerationsenmus,是一种特殊类型的类,用于表示固定数量的常量值。

使用枚举(Using enums)

使用 enum 关键字来声明类型

enum Color { red, green, blue }

每一个枚举值都有索引(index) getter,它可以返回他在枚举声明中的位置,索引从0开始计算。

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

要获取枚举列表里所有的值,使用 values 属性。(类似于 JavaScript 里的 Object.values(obj))

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: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
}

枚举类型有如下限制:

  • 不能创建子类、混入(mix in)或实例
  • 无法显示实例化

更多详情请移步 Dart language specification

使用混入(mixins)给类添加新功能(Adding features to a class: mixins)

混入就是:一个类可以重复被多个类使用

使用 with 关键字来使用混入,后面跟着一个或多个混入名。

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

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

要实现混入,创建一个继承Object并且不声明构造函数的类。除非你想让你的混入像常规类一样有用,使用 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');
    }
  }
}

只有显示的类型才能使用mixin,比如,你的 mixin 可以调用它自身未定义的方法,通过使用on 关键字引入定义方法的父类:

mixin MusicalPerformer on Musician {
  // ···
}

类的变量和方法(Class variables and methods)

使用 static 关键字来实现类作用域(class-wide)里的变量和方法。

静态变量(Static variables)

静态变量(类变量)对于类作用域里的状态和常量非常有用:

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

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

静态变量只有在使用时才会初始化。

静态方法(Static methods)

静态方法(类方法)不能在类的实例上操作,也不能使用 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);
}

Note: 从实用性、功能性和广泛性的角度来看,应该考虑用顶级函数来替代静态方法。

你可以将静态方法当成编译常量。比如,你可以将静态方法当做参数传给常量构造函数。

系列文章:

Dart 简明教程 - 01 - Concepts & Variables
Dart 简明教程 - 02 - Functions
Dart 简明教程 - 03 - Operators
Dart 简明教程 - 04 - Control flow statements
Dart 简明教程 - 05 - Exceptions
Dart 简明教程 - 06 - Classes
Dart 简明教程 - 07 - Generics
Dart 简明教程 - 08 - Libraries and visibility
Dart 简明教程 - 09 - Asynchrony support
Dart 简明教程 - 10 - Generators & Isolates & Typedefs & Metadata...