构造函数
构造函数(或构造器,Constructor),是用来创建类实例(对象)的特殊函数。如上一节中
var dog = Dog('Spots', 6);
通过调用Dog
类的构造函数,得到Dog
类的一个实例 dog
。
Dart 实现了多种类型的构造函数:
这些函数总体上可以分为 生成式构造函数 与 工厂构造函数 两大类。它们在书写形式上无需指定返回类型,因为它们返回的一定是类的实例。
在本文的最后还将讨论构造函数的函数体。
在讨论具体的构造函数之前,先来看一下实例变量的初始化。
实例变量的初始化
在声明时初始化
在声明类的实例变量时,可对其进行初始化。例如
// ch0402_1.dart
class Cat {
var name = "Kitty";
var age = 1;
}
实例变量 name
和 age
都有初始值,因此使用 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
}
- 冒号后面的
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
}
- 这里在初始化列表里使用
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);
}
Rectangle
类代表了一个矩形;- 定义一个静态的辅助方法
_computePerimeter
,用以计算矩形的周长; - 在初始化列表中调用
_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)
}
Point
类代表了平面坐标系中的一个点;- 使用初始化形参定义了一个构造函数;
- 定义了一个命名构造函数
Point.origin()
,它使用初始化列表将实例变量x
和y
的值都设置为0。
常量构造函数
如果要创建编译时( compile-time)常量对象,就需要用到常量构造函数(Constant constructors)。
// ch0402_8.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
关键字重定向到类中的另一个生成式构造函数。
// ch0402_9.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(日志记录器)。
// 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
}
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
方法打印一行日志。
创建系列对象
有时需要对一系列对象的创建过程进行集中管理,尤其是那些受保护的类。
// 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实例变量的初始化,必须在 初始化形参 或 初始化列表中完成,而不可在函数体中完成。
工厂构造函数是一种返回类实例的特殊函数,它一定有函数体。