构造函数

构造函数(或构造器,Constructor),是用来创建类实例(对象)的特殊函数。如上一节中

var dog = Dog('Spots', 6);

通过调用Dog类的构造函数,得到Dog类的一个实例 dog

Dart 实现了多种类型的构造函数:

这些函数总体上可以分为 生成式构造函数 与 工厂构造函数 两大类。它们在书写形式上无需指定返回类型,因为它们返回的一定是类的实例。

在本文的最后还将讨论构造函数的函数体

在讨论具体的构造函数之前,先来看一下实例变量的初始化

实例变量的初始化

在声明时初始化

在声明类的实例变量时,可对其进行初始化。例如

// ch0402_1.dart
class Cat {
  var name = "Kitty";
  var age = 1;
}

实例变量 nameage 都有初始值,因此使用 var 关键字来声明,让Dart自动推断其类型。

初始化形参

Dart的初始化形参极大的简化了构造函数的书写。

// ch0402_2.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age);
}

此处的 this 关键字表示当前实例,或则说 this 乃当前实例的引用。

初始化列表

上述示例代码(ch0402_2.dart)等价于:

// ch0402_3.dart
class Cat {
  String name;
  int age;

  Cat(String name, int age) : name = name, age = age; // 1
}
  1. 冒号后面的 name = name, age = age 即初始化列表。

NOTE: 初始化列表中的 name = name,等号(=)左边是实例变量name,右边是构造函数的参数name

当然对于如此简单的初始化操作,使用初始化形参数更为简洁。

实战中经常要对构造函数的参数进行简单校验,比如要求 name 的长度至少为3:

// ch0402_4.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age) : assert(name.length >= 3); // 1
}
  1. 这里在初始化列表里使用 assert对参数name的长度进行了校验。

有时候实例变量的初始化需要一个简单的计算,可以参考下例:

// ch0402_5.dart
class Rectangle { // 1
  final double width;
  final double height;
  final double perimeter;

  Rectangle(this.width, this.height)
    : perimeter = _computePerimeter(width, height); // 3

  // 2
  static double _computePerimeter(double w, double h) => 2 * (w + h);
}
  1. Rectangle类代表了一个矩形;
  2. 定义一个静态的辅助方法_computePerimeter,用以计算矩形的周长;
  3. 在初始化列表中调用_computePerimeter来初始化实例变量 perimeter

生成式构造函数

生成式构造函数(Generative constructors),负责创建实例并初始化实例变量,是创建新实例的常见方式。上文已经演示了一个简单的生成式构造函数:

// ch0402_2.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age);
}

默认构造函数

如何没有显示定义一个生成式构造函数,Dart将自动创建一个不具名的无参构造函数。

// ch0402_6.dart
class Cat {
  var name = "Kitty";
  var age = 1;

  // Auto-created default constructor
  // Cat();

  @override
  String toString() {
    return 'Cat($name,$age)';
  }
}

void main() {
  print(Cat());// Output: Cat(Kitty,1)
}

命名构造函数

命名构造函数用于实现多个构造函数,并且命名构造函数的名称提供了额外的信息,以帮助用户使用(理解代码的意图)。

// ch0402_7.dart
class Point { // 1
  final double x;
  final double y;

  Point(this.x, this.y); // 2

  Point.origin() : x = 0, y = 0; // 3

  @override
  String toString() {
    return '($x, $y)';
  }
}

void main() {
  print(Point(2, 3)); // Output: (2.0, 3.0)
  print(Point.origin()); // Output: (0.0, 0.0)
}
  1. Point 类代表了平面坐标系中的一个点;
  2. 使用初始化形参定义了一个构造函数;
  3. 定义了一个命名构造函数 Point.origin(),它使用初始化列表将实例变量xy的值都设置为0。

常量构造函数

如果要创建编译时( compile-time)常量对象,就需要用到常量构造函数(Constant constructors)。

// ch0402_8.dart
class Point {
  final double x;
  final double y;

  const Point(this.x, this.y); // 1
}
  1. 使用 const 关键字定义常量构造函数。

NOTE: 常量构造函数所在类的实例变量必须为声明为 final

常量构造函数所创建的对象一定是常量吗?

void main() {
  var p1 = const Point(1, 2); // 2
  var p2 = Point(1, 2); // 3
  print(p1 == p2); // 4 Output: false

  const p3 = Point(1, 2); // 5
  print(p1 == p3); // 6 Output: true
}
  1. 在常量构造函数 Point(1, 2) 前使用关键字 const 创建Point类的一个实例p1;
  2. p2 是 Point类的另一个实例,但实例化时没有使用const关键字;
  3. 比较 p1p2 是否相等,结果为 false,可见它们引用了不同的实例;
  4. p3p2类似,但用了 const (而非var)关键字来声明;
  5. 比较 p1p3 是否相等,结果为 true,可见它们引用了同一个实例,该实例是一个常量;而p2对应的实例则不是常量。

从语法角度来看:

  1. var p2 = Point(1, 2); 等价于 var p2 = new Point(1, 2);, 这里的关键字 new 通常予以省略;
  2. const p3 = Point(1, 2); 这里的 const关键字开启了一个常量上下文,因此这句代码就相当于 const p3 = const Point(1, 2); ,而第二个const通常予以省略。

重定向构造函数

重定向构造函数没有函数体,它使用 this 关键字重定向到类中的另一个生成式构造函数。

// ch0402_9.dart
class Point {
  double x, y;

  Point(this.x, this.y); // 1

  Point.x(double x) : this(x, 0); // 2
}
  1. 使用初始化形参定一个生成式构造函数;
  2. Point.x(double) 是一个重定向构造函数,它重定向至构造函数 Point(this.x, this.y)(语法上使用this关键字);这行代码的效果为:
  Point.x(this.x) : y = 0; // 2

工厂构造函数

设计模式中有一个创建型模式叫做工厂方法(也称为虚拟构造函数),它专门用来创建类的实例。Dart的工厂构造函数与设计模式里的工厂方法有类似的意图。工厂构造函数是一种返回类实例的特殊函数,它必须借助生成式构造函数,才能创建实例。下面按使用场景举例说明。

缓存实例

工厂构造函数用于缓存实例的一个经典示例是Logger(日志记录器)。

// ch0402_a.dart
class Logger {
  final String name;

  Logger._(this.name); // 1

  static final _cache = <String, Logger>{}; // 2

  factory Logger(String name) { // 3
    return _cache.putIfAbsent(name, () => Logger._(name));
  }

  factory Logger.main() => Logger("main"); // 4

  void log(String message) { // 5
    print('${DateTime.now()} $name: $message');
  }
}

void main() {
  assert(Logger("main") == Logger.main()); // 6
  Logger.main().log('This message is from main function'); // 7
}
  1. Logger._(this.name) 是一个私有的构造函数,对外部文本不可见;
  2. _cache是一个Map,用于缓存 Logger实例;
  3. Logger(String)是一个工厂构造函数,以关键字factory修饰,其内部操作为:如果name参数对应的Logger 实例已经在_cache中,就直接返回该实例,否则创建一个新的Logger实例存放于_cache,然后返回它;
  4. Logger.main() 是命名的工厂构造函数;
  5. log(String) 是用于打印日志的实例方法;
  6. 断言 Logger("main")Logger.main()返回同一个Logger实例;
  7. 调用 Logger.main()log方法打印一行日志。

创建系列对象

有时需要对一系列对象的创建过程进行集中管理,尤其是那些受保护的类。

// ch0402_b.dart
abstract class Toy {
  factory Toy(String name) {
    return switch (name) {
      'car' => _Car(),
      'truck' => _Truck(),
      'airplane' => _Airplane(),
      _ => throw 'unknown toy: $name',
    };
  }
}

class _Car implements Toy {}

class _Truck implements Toy {}

class _Airplane implements Toy {}

void main() {
  print(Toy('car'));
}

此例中的Toy是一个抽象类,它定义了一个工厂构造函数,该函数根据参数name来创建对应的实例。Toy的具体类(_Car_Truck_Airplane)是私有的(受保护的),随着时间的推移,它们的内部实现可能被改变,但不会影响用户代码。抽象类与接口等概念将在下一章介绍。

构造函数的函数体

重定向构造函数没有函数体,其它生成式构造函数的函数体是可选的。示例 ch0402_4.dart 可以改写为:

// ch0402_c.dart
class Cat {
  String name;
  int age;

  Cat(this.name, this.age) {
    assert(name.length >= 3);
  }
}

但一点需要注意,没有late关键字修饰的non-nullable实例变量的初始化,必须在 初始化形参 或 初始化列表中完成,而不可在函数体中完成。

工厂构造函数是一种返回类实例的特殊函数,它一定有函数体。

参考资料