构造函数
构造函数(或构造器,Constructor),是用来创建类实例(对象)的特殊函数。如上一节中
var dog = Dog('Spots', 6);
通过调用Dog类的构造函数,得到Dog类的一个实例 dog。
Dart 实现了多种类型的构造函数:
这些函数总体上可以分为 生成式构造函数 与 工厂构造函数 两大类。它们在书写形式上无需指定返回类型,因为它们返回的一定是类的实例。
在本文的最后还将讨论构造函数的函数体。
在讨论具体的构造函数之前,先来看一下实例变量的初始化。
实例变量的初始化
在声明时初始化
在声明类的实例变量时,可对其进行初始化。例如
// ex421.dart
class Cat {
var name = "Kitty";
var age = 1;
}
实例变量 name 和 age 都有初始值,因此使用 var 关键字来声明,让Dart自动推断其类型。
初始化形参
Dart的初始化形参极大的简化了构造函数的书写。
// ex422.dart
class Cat {
String name;
int age;
Cat(this.name, this.age);
}
此处的 this 关键字表示当前实例,或则说 this 乃当前实例的引用。
初始化列表
上述示例代码(ex422)等价于:
// ex423.dart
class Cat {
String name;
int age;
Cat(String name, int age) : name = name, age = age; // 1
}
- 冒号后面的
name = name, age = age即初始化列表。
NOTE: 初始化列表中的
name = name,等号(=)左边是实例变量name,右边是构造函数的参数name。
当然对于如此简单的初始化操作,使用初始化形参数更为简洁。
实战中经常要对构造函数的参数进行简单校验,比如要求 name 的长度至少为3:
// ex424.dart
class Cat {
String name;
int age;
Cat(this.name, this.age) : assert(name.length >= 3); // 1
}
- 这里在初始化列表里使用
assert对参数name的长度进行了校验。
有时候实例变量的初始化需要一个简单的计算,可以参考下例:
// ex425.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);
}
Rectangle类代表了一个矩形;- 定义一个静态的辅助方法
_computePerimeter,用以计算矩形的周长; - 在初始化列表中调用
_computePerimeter来初始化实例变量perimeter。
生成式构造函数
生成式构造函数(Generative constructors),负责创建实例并初始化实例变量,是创建新实例的常见方式。上文已经演示了一个简单的生成式构造函数:
// ex422.dart
class Cat {
String name;
int age;
Cat(this.name, this.age);
}
默认构造函数
如何没有显示定义一个生成式构造函数,Dart将自动创建一个不具名的无参构造函数。
// ex426.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)
}
命名构造函数
命名构造函数用于实现多个构造函数,并且命名构造函数的名称提供了额外的信息,以帮助用户使用(理解代码的意图)。
// ex427.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)
}
Point类代表了平面坐标系中的一个点;- 使用初始化形参定义了一个构造函数;
- 定义了一个命名构造函数
Point.origin(),它使用初始化列表将实例变量x和y的值都设置为0。
常量构造函数
如果要创建编译时( compile-time)常量对象,就需要用到常量构造函数(Constant constructors)。
// ex428.dart
class Point {
final double x;
final double y;
const Point(this.x, this.y); // 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
}
- 在常量构造函数
Point(1, 2)前使用关键字const创建Point类的一个实例p1; - p2 是
Point类的另一个实例,但实例化时没有使用const关键字; - 比较
p1和p2是否相等,结果为false,可见它们引用了不同的实例; p3与p2类似,但用了const(而非var)关键字来声明;- 比较
p1和p3是否相等,结果为true,可见它们引用了同一个实例,该实例是一个常量;而p2对应的实例则不是常量。
从语法角度来看:
var p2 = Point(1, 2);等价于var p2 = new Point(1, 2);, 这里的关键字new通常予以省略;const p3 = Point(1, 2);这里的const关键字开启了一个常量上下文,因此这句代码就相当于const p3 = const Point(1, 2);,而第二个const通常予以省略。
重定向构造函数
重定向构造函数没有函数体,它使用 this 关键字重定向到类中的另一个生成式构造函数。
// ex429.dart
class Point {
double x, y;
Point(this.x, this.y); // 1
Point.x(double x) : this(x, 0); // 2
}
- 使用初始化形参定一个生成式构造函数;
Point.x(double)是一个重定向构造函数,它重定向至构造函数Point(this.x, this.y)(语法上使用this关键字);这行代码的效果为:
Point.x(this.x) : y = 0; // 2
工厂构造函数
设计模式中有一个创建型模式叫做工厂方法(也称为虚拟构造函数),它专门用来创建类的实例。Dart的工厂构造函数与设计模式里的工厂方法有点类似,但在结构上有所不同。工厂构造函数是一种返回类实例的特殊函数,它必须借助生成式构造函数,才能创建实例。下面按使用场景举例说明。
缓存实例
工厂构造函数用于缓存实例的一个经典示例是Logger(日志记录器)。
// ex42a.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
}
Logger._(this.name)是一个私有的构造函数,对外部文本不可见;_cache是一个Map,用于缓存Logger实例;Logger(String)是一个工厂构造函数,以关键字factory修饰,其内部操作为:如果name参数对应的Logger实例已经在_cache中,就直接返回该实例,否则创建一个新的Logger实例存放于_cache,然后返回它;Logger.main()是命名的工厂构造函数;log(String)是用于打印日志的实例方法;- 断言
Logger("main")与Logger.main()返回同一个Logger实例; - 调用
Logger.main()的log方法打印一行日志。
创建系列对象
有时需要对一系列对象的创建过程进行集中管理,尤其是那些受保护的类。
// ex42b.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)是私有的(受保护的),随着时间的推移,它们的内部实现可能被改变,但不会影响用户代码。抽象类与接口等概念将在下一章介绍。
命名工厂构造函数与静态方法
实际上,命名工厂构造函数可以用返回类实例的静态方法进行改写,可以说二者是等价的。示例 ex42a 的 Logger.main 方法可以改写为:
static Logger main() => Logger("main"); // 4
构造函数的函数体
重定向构造函数没有函数体,其它生成式构造函数的函数体是可选的。示例 ex424 可以改写为:
// ex42c.dart
class Cat {
String name;
int age;
Cat(this.name, this.age) {
assert(name.length >= 3);
}
}
但一点需要注意,没有late关键字修饰的non-nullable实例变量的初始化,必须在 初始化形参 或 初始化列表中完成,而不可在函数体中完成。
工厂构造函数是一种返回类实例的特殊函数,它一定有函数体。