08.Dart Class类

407 阅读12分钟

Dart 是支持基于 mixin 继承机制的面向对象语言,所有对象都是一个类的实例,而除了 Null 以外的所有的类都继承自 Object 类。 基于 mixin 的继承 意味着尽管每个类(top class Object? 除外)都只有一个超类,一个类的代码可以在其它多个类继承中重复使用。 extension扩展方法 是一种在不更改类或创建子类的情况下向类添加功能的方式。

使用类的成员

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

使用(.)来访问对象的实例变量或方法:

import 'dart:math';

var p = Point(2, 2);
// 获取变量 y 的值。
assert(p.y == 2);

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

使用 ?. 来代替 . , 可以避免因为左边对象可能为 null , 导致的异常:

// 如果 p 为 non-null,设置它变量 y 的值为 4。
p?.y = 4;

获取对象的类型

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

var p = Point(2, 2);
// p.runtimeType 是 Point<int> 类型
print(p.runtimeType == Point); // false
print(p is Point); // true 

使用 is 而不是runtimeType来测试对象的类型。Object is Type 比 runtimetype == Type更稳定。

声明

class Point {
  num x; // 成员变量x,初始值null.
  num y = 0; // 成员变量y,初始化为0.
  num _z = 1; // 成员变量_z, 初始化为1,仅在库内可访问
}

实例化

Point p = new Point();
// dart2.0 可以省略 new关键字
Point p1 = Point();

构造函数

几种类型的构造函数:

未定义构造函数的时候,会自动生成一个无参的默认构造函数,并且会调用超类的没有参数的构造函数

普通构造函数

定义一个和类名字一样的方法就定义了一个构造函数

// 常规形式
class Point {
  num x;
  num y;
  Point(num x, num y) {
    // 如果不存在同名冲突,this可以省略
    this.x = x;
    this.y = y;
  }
}
// 语法糖形式
class Point {
  num x;
  num y;
  Point(this.x, this.y);
}

命名构造函数

使用命名构造函数可以为一个类实现多个构造函数, 或者使用命名构造函数来更清晰的表明你的意图。

class Point {
  num x;
  num y;
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}

常量构造函数

如果你的类提供一个状态不变的对象,你可以把这些对象 定义为编译时常量。要实现这个功能,需要定义一个 const 构造函数, 并且声明所有类的变量为 final。

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

工厂方法构造函数

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

  factory Logger.fromJson(Map<String, Object> json) {
    print(json['name'].toString());
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

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

main(){
  var logger = Logger('UI');
  var logger2 = Logger.fromJson({'name': 'mickey'});
  var logger3 = Logger.fromJson({'name': 'jerry'});
  logger.log('Button clicked');
  print(Logger._cache);
}

在工厂构造函数中无法访问 this

重定向构造函数

有时候类中的构造函数仅用于调用类中其它的构造函数,此时该构造函数没有函数体,只需在函数签名后使用(:)指定需要重定向到的其它构造函数 (使用 this 而非类名):

class Point {
  double x, y;

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

  // Delegates to the main constructor.
  Point.alongXAxis(double x) : this(x, 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');
  }
}

void main() {
  var employee = Employee.fromJson({});
  print(employee);
  // Prints:
  // in Person
  // in Employee
  // Instance of 'Employee'
}

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

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

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

方法

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

实例方法

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

import 'dart:math';

class Point {
  double x = 0;
  double y = 0;

  Point(this.x, this.y);

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

操作符

运算符是有着特殊名称的实例方法。 Dart 允许您使用以下名称定义运算符:

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

你可能注意到有一些 操作符 没有出现在列表中,例如 !=。因为它们仅仅是语法糖。表达式 e1 != e2 仅仅是 !(e1 == e2) 的一个语法糖。

为了表示重写操作符,我们使用 operator 标识来进行标记。下面是重写 + 和 - 操作符的例子

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.
  // ···
}

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

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

Getter 和 Setter

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

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

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

  // Define two calculated properties: right and bottom.
  double get right => left + width;
  set right(double value) => left = value - width;
  double get bottom => top + height;
  set bottom(double 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 {
  // 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 标识类可以让该类成为 抽象类,抽象类将无法被实例化。抽象类常用于声明接口方法、有时也会有具体的方法实现。如果想让抽象类同时可被实例化,可以为其定义 工厂构造函数

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

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

  void updateChildren(); // Abstract method.
}

隐式接口

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

一个类可以通过关键字 implements 来实现一个或多个接口并实现每个接口定义的 API:

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final String _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 {
  String 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() {...}
  // ···
}

如果重写 == 操作符,必须同时重写对象 hashCode 的 Getter 方法。你可以查阅 实现映射键 获取更多关于重写的 == 和 hashCode 的例子。

noSuchMethod 方法

如果调用了对象上不存在的方法或实例变量将会触发 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 类型。
  • 接收方具有静态类型,定义了未实现的方法(抽象亦可),并且接收方的动态类型实现了 noSuchMethod 方法且具体的实现与 Object 中的不同。

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

扩展方法

扩展方法是向现有库添加功能的一种方式。你可能已经在不知道它是扩展方法的情况下使用了它。例如,当您在 IDE 中使用代码完成功能时,它建议将扩展方法与常规方法一起使用。

这里是一个在 String 中使用扩展方法的样例,我们取名为 parseInt(),它在 string_apis.dart 中定义:

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

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

枚举类型

枚举类型是一种特殊的类型,也称为 enumerations 或 enums,用于定义一些固定数量的常量值。

使用关键字 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('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,你也不可以实现一个枚举。
  • 不能显式地实例化一个枚举类。

使用 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;
  }
}

想要实现一个 Mixin,请创建一个继承自 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');
    }
  }
}

可以使用关键字 on 来指定哪些类可以使用该 Mixin 类,比如有 Mixin 类 A,但是 A 只能被 B 类使用,则可以这样定义 A:

class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // ...
}

在前面的代码中,只有扩展或实现了Musician类的类才能使用mixin MusicalPerformer。因为SingerDancer扩展了Musician,所以SingerDancer可以混合成MusicalPerformer。

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 {
  double x, y;
  Point(this.x, this.y);

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

对于一些通用或常用的静态方法,应该将其定义为顶级函数而非静态方法。

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