这节详细介绍如何定义类的变量声明和构造方法。
实例变量
下面是声明实例变量的示例:
class Point {
num x; // 声明实例变量 x 并初始化为 null。
num y; // 声明实例变量 y 并初始化为 null。
num z = 0; // 声明实例变量 z 并初始化为 0。
}
备忘:"所有未初始化的实例变量其值均为 null。"
所有实例变量均会隐式地声明一个 Getter 方法,非 final 类型的实例变量还会隐式地声明一个 Setter 方法。你可以查阅 Getter 和 Setter 。
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); // 默认值为 null。
}
备忘:
如果你在声明一个实例变量的时候就将其初始化(而不是在构造函数或其它方法中),那么该实例变量的值就会在对象实例创建的时候被设置,该过程会在构造函数以及它的初始化器列表执行前。
构造函数
声明一个与类名一样的函数即可声明一个构造函数(对于命名式构造函数还可以添加额外的标识符)。大部分的构造函数形式是生成式构造函数,其用于创建一个类的实例:
class Point {
num x, y;
Point(num x, num y) {
// 还会有更好的方式来实现此逻辑,敬请期待。
this.x = x;
this.y = y;
}
}
当且仅当命名冲突时使用 this 关键字才有意义,否则 Dart 会忽略 this 关键字。
对于大多数编程语言来说在构造函数中为实例变量赋值的过程都是类似的,而 Dart 则提供了一种特殊的语法糖来简化该步骤:
class Point {
num x, y;
// 在构造函数体执行前用于设置 x 和 y 的语法糖。
Point(this.x, this.y);
}
这种用类名加变量的声明构造方法只能有一个,那么如何定义多个构造方法呢?先带着疑问,继续往下看。
默认构造函数
如果你没有声明构造函数,那么 Dart 会自动生成一个无参数的构造函数并且该构造函数会调用其父类的无参数构造方法。
构造函数不会被继承
子类不会继承父类的构造函数,如果子类没有声明构造函数,那么只会有一个默认无参数的构造函数。
命名式构造函数
可以为一个类声明多个命名式构造函数来表达更明确的意图:
class Point {
num x, y;
Point(this.x, this.y);
// 命名式构造函数
Point.origin() {
x = 0;
y = 0;
}
}
跟构造函数的语法糖一样,也可以写成:
class Point {
num x, y;
Point(this.x, this.y);
// 命名式构造函数
Point.origin(this.x, this.y) ;
}
备忘:
- 记住构造函数是不能被继承的,这将意味着子类不能继承父类的命名式构造函数,如果你想在子类中提供一个与父类命名构造函数名字一样的命名构造函数,则需要在子类中显式地声明。
- 如果声明有参构造函数,这时就没有无参构造函数,也无法显示声明,因为类型构造函数只有一个;如果你声明了命名式的构造函数,这时就没有无参构造函数了,但你可以显示地声明出来。
- 第1点不能函数名重名的原因是:C++ 与 Java 的做法是,提供函数的重载,即提供同名但参数不同的函数。但 Dart 认为重载会导致混乱,因此从设计之初就
不支持重载
,而是提供了可选命名参数和可选参数。
- 第1点不能函数名重名的原因是:C++ 与 Java 的做法是,提供函数的重载,即提供同名但参数不同的函数。但 Dart 认为重载会导致混乱,因此从设计之初就
调用父类非默认构造函数
默认情况下,子类的构造函数会调用父类的匿名无参数构造方法,并且该调用会在子类构造函数的函数体代码执行前,如果子类构造函数还有一个初始化列表,那么该初始化列表会在调用父类的该构造函数之前被执行,总的来说,这三者的调用顺序如下:
- 初始化列表
- 父类的无参数构造函数
- 当前类的构造函数
如果父类没有匿名无参数构造函数,那么子类必须调用父类的其中一个构造函数,为子类的构造函数指定一个父类的构造函数只需在构造函数体前使用(:)指定。
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');
}
}
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(defaultData);
// ···
}
class Employee extends Person {
Employee() : super();
// ···
}
备忘:
- 构造函数声明主体后面接
:
可以调用父类的构造方法,用super代替父类的类型,如super()
、super.fromJson()
,用this可以接本类的其他构造方法,很常用很强大,要记住。
- 构造函数声明主体后面接
- 传递给父类构造函数的参数不能使用
this
关键字,因为在参数传递的这一步骤,子类构造函数尚未执行,子类的实例对象也就还未初始化,因此所有的实例成员
都不能被访问,但是类成员
可以。
- 传递给父类构造函数的参数不能使用
- 构造函数声明主体后面接
:
不仅仅是接父类构造函数,还可以接表达式、初始化列表(下面会介绍)。
- 构造函数声明主体后面接
初始化列表
刚才我们提到初始化列表会在父类构造函数前执行,到底什么是初始化列表?
//初始化列表必须在父类表达式声明之前
Employee.fromJson(Map data) : employeeName = 'hahha',super(){
print('in Employee');
}
除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量,每个实例变量之间使用逗号分隔,这就是初始化列表:
// 使用初始化列表在构造函数体执行前设置实例变量。
Point.fromJson(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
初始化列表表达式 = 右边的语句不能使用 this 关键字。
初始化列表用来设置final
字段是非常好用的,下面的示例中就使用初始化列表来设置了三个final
变量的值。
import 'dart:math';
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);
}
这个语法糖和命名构造函数共同实现多个构造函数的实现,比起只用命名构造函数实现,委托实现可以帮忙实现重复的初始化代码。
委托后面不能再加大括号:
Employee.fromJson(Map data) : this();
常量构造函数
如果类生成的对象都是不会变的,那么可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上const
关键字并确保所有实例变量均为final
来实现该功能。
class ImmutablePoint {
static final ImmutablePoint origin =
const ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
工厂构造函数
使用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));
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
在工厂构造函数中无法访问 this。
工厂构造函的调用方式与其他构造函数一样:
var logger = Logger('UI');
logger.log('Button clicked');
网友评论