Dart中的类(上)

431 阅读4分钟

Dart是一种面向对象的语言,具有类和基于mixin的继承。每个对象都是一个类的实例,所有类都来自Object这个类。基于Mixin的继承意味着虽然每个类(除了Object)只有一个超类,但是类的主体可以在多个类层次结构中重用(通过mixin混入)。

类中成员

Objects具有由函数和数据(分别对应方法和实例变量)组成的成员。调用方法时,可以在object上调用它:该方法可以访问该object的函数和数据。
使用点(.)来引用实例变量或方法:

var p = Point(2, 2);

// 修改实例p中y变量的值.
p.y = 3;

// 获取实例p中y变量的值.
assert(p.y == 3);

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

使用?.代替 .,当最左边的操作数为null时可以避免报错:

// 如果p不是null,那么y的值会设置为4。
// 如果p是null,那么什么也不执行。
p?.y = 4;

使用构造函数

您可以使用构造函数创建object。构造函数名称可以是ClassNameClassName.identifier。例如,以下代码分别使用Point()和Point.fromJson()构造函数创建Point对象:

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
// 一下加上new的写法和上面的写法结果一致,Dart 2之后可以省略new关键字
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

有些类提供常量构造函数。要使用常量构造函数创建编译时常量,只用把const关键字放在构造函数名称之前:

var p = const ImmutablePoint(2, 2);
// 构造两个相同的编译时常量会产生同一个实例
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // 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, 它建立了常量上下文.
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

const关键字在Dart 2中的常量上下文中变为可选。
如果常量构造函数在常量上下文之外(也就是上面代码的这种情况)并且在没有const的情况下调用,则会创建一个非常量对象:

var a = const ImmutablePoint(1, 1); // 创建一个constant
var b = ImmutablePoint(1, 1); // 没有创建constant

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

获取对象的类型

要在运行时获取对象的类型,可以使用ObjectruntimeType属性,该属性返回Type对象

  var a = 123;
  print('The type of a is ${a.runtimeType}'); // The type of a is int

以上部分简单的介绍了如何使用类,下面将介绍如何实现类。

实例变量

声明实例变量:

// 所有未初始化的实例变量的值都为null
class Point {
  num x; // 声明实例变量x,初始为null.
  num y; // 声明实例变量y,初始为null.
  num z = 0; // 声明实例变量z, 初始为0.
}

所有实例变量都生成一个隐式getter方法。Non-final 实例变量也会生成隐式setter方法。有关详细信息,请参阅 Getters and setters

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); // 使用了实例变量y的getter,y的初始值为null
}

如果你在声明实例变量(而不是构造函数或方法)时初始化值,则在创建实例时设置该值,这个操作在构造函数及其初始化列表执行之前执行。

构造函数

通过创建与其类同名的函数来声明构造函数(另外,也可以使用命名构造参数)。最常见的构造函数使用方式就是创建一个类的新实例:

class Point {
  num x, y;
  Point(num x, num y) {
    // 下面会介绍一种更快捷的初始化变量方式。
    this.x = x;
    this.y = y;
  }
}

**this关键字指向当前的实例,只在出现名称重复的情况下使用this,其他时候请省略this关键字。    **
由于将构造函数参数赋值给实例变量的模式非常普遍,所以Dart提供了一个语法糖,使其变得简单:

class Point {
  num x, y;
  // Syntactic sugar for setting x and y
  // 在构造函数体运行之前初始化x和y
  Point(this.x, this.y);
}

默认构造函数

如果你没有声明构造函数,Dart会提供默认构造函数。默认构造函数没有参数,并在超类中调用无参数构造函数。

构造函数不可继承

子类不从其超类继承构造函数。声明没有构造函数的子类只有默认(无参数,无名称)构造函数。

命名构造函数

使用命名构造函数为类实现多个构造函数:

class Point {
  num x, y;

  Point(this.x, this.y);

  // 这个命名构造函数提供了一种x,y等于0的快捷入口。
  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 没有默认构造函数;
  // 你必须手动调用Person.fromJson.
  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());
  // ···
}

超类构造函数的参数无权访问this。比如,参数可以调用静态方法,但不能调用实例方法(因为实例方法是通过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来验证输入:

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

设置final变量时,初始化程序列表很方便。以下示例初始化初始化列表中的三个final字段:

import 'dart:math';
// final关键字定义的变量要么在定义是赋值,要么只能在初始化列表赋值。
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);
}

常量构造函数

如果你定义的类生成永远不会更改的对象,则可以使这些对象成为编译时常量。为此,请定义const构造函数并确保所有实例变量都是final

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

  final num x, y;

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

常量构造函数并不总是创建常量。前面我们在使用构造函数这一节说到了,常量构造函数必须使用const关键字实例化,或者在常量上下文中实例化。

工厂构造函数

当你想实现不总是返回其类的新实例的构造函数时,请使用factory关键字。例如,工厂构造函数可能从缓存中返回实例,或者它可能返回子类型的实例:

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

  // _cache是私有静态变量,因为_打头
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    // 这里缓存了Logger._internal生成的实例,如果有缓存,就直接返回缓存,而不会生成新的实例。
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}
// 工厂构造函数调用方式和普通构造函数一样:
var logger = Logger('UI');
logger.log('Button clicked');

工厂构造者无法访问**this**