深色模式
Dart 类与对象
类
Dart 是一种面向对象的语言,支持类和基于混入(mixin)的继承。每个对象都是类的实例,除 Null
之外的所有类都继承自 Object
。基于混入的继承意味着,尽管每个类(顶级类 Object
除外)都只有一个直接父类,但类的主体可以在多个类层次结构中复用。扩展方法是一种在不改变类或创建子类的情况下为类添加功能的方式。类修饰符可以控制库对类进行子类化的方式。
使用类成员
对象的成员包括函数和数据(分别对应方法和实例变量)。调用方法时,是在对象上调用它,该方法可以访问该对象的函数和数据。
使用点号(.
)来引用实例变量或方法:
dart
var p = Point(2, 2);
// 获取 y 的值
assert(p.y == 2);
// 在 p 上调用 distanceTo() 方法
double distance = p.distanceTo(Point(4, 4));
使用 ?.
而非 .
,可以避免最左边的操作数为 null
时抛出异常:
dart
// 如果 p 不为 null,将其 y 值赋给变量
var a = p?.y;
使用构造函数
可以使用构造函数来创建对象。构造函数名可以是 ClassName
或者 ClassName.identifier
。例如,以下代码使用 Point()
和 Point.fromJson()
构造函数创建 Point
对象:
dart
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
以下代码有相同的效果,但在构造函数名前使用了可选的 new
关键字:
dart
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
有些类提供常量构造函数。要使用常量构造函数创建编译时常量,在构造函数名前加上 const
关键字:
dart
var p = const ImmutablePoint(2, 2);
创建两个相同的编译时常量会得到同一个规范实例:
dart
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // 它们是同一个实例!
在常量上下文中,可以省略构造函数或字面量前的 const
。例如,下面这段创建常量映射的代码:
dart
// 这里有很多 const 关键字
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
可以省略除第一个 const
之外的所有 const
关键字:
dart
// 只保留一个 const 来建立常量上下文
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
如果常量构造函数在常量上下文之外被调用且没有使用 const
,则会创建一个非常量对象:
dart
var a = const ImmutablePoint(1, 1); // 创建一个常量
var b = ImmutablePoint(1, 1); // 不创建常量
assert(!identical(a, b)); // 不是同一个实例!
获取对象的类型
要在运行时获取对象的类型,可以使用 Object
的 runtimeType
属性,它会返回一个 Type
对象。
dart
print('The type of a is ${a.runtimeType}');
警告: 使用类型测试运算符而不是 runtimeType
来测试对象的类型。在生产环境中,对象 is 类型
测试比 对象.runtimeType == 类型
测试更稳定。
到目前为止,你已经了解了如何使用类。本节的其余部分将展示如何实现类。
实例变量
以下是声明实例变量的方式:
dart
class Point {
double? x; // 声明实例变量 x,初始值为 null
double? y; // 声明 y,初始值为 null
double z = 0; // 声明 z,初始值为 0
}
使用可空类型声明但未初始化的实例变量的值为 null
。非可空实例变量必须在声明时进行初始化。
所有实例变量都会生成一个隐式的 getter 方法。非 final
实例变量和没有初始值设定项的 late final
实例变量还会生成一个隐式的 setter 方法。详情请查阅“Getters 和 Setters”。
dart
class Point {
double? x; // 声明实例变量 x,初始值为 null
double? y; // 声明 y,初始值为 null
}
void main() {
var point = Point();
point.x = 4; // 使用 x 的 setter 方法
assert(point.x == 4); // 使用 x 的 getter 方法
assert(point.y == null); // 值默认是 null
}
在声明非 late
实例变量时对其进行初始化,会在实例创建时(在构造函数及其初始化列表执行之前)设置该值。因此,非 late
实例变量的初始化表达式(=
后面的部分)不能访问 this
。
dart
double initialX = 1.5;
class Point {
// 可以,能访问不依赖于 `this` 的声明
double? x = initialX;
// 错误,不能在非 `late` 初始化器中访问 `this`
double? y = this.x;
// 可以,能在 `late` 初始化器中访问 `this`
late double? z = this.x;
// 可以,`this.x` 和 `this.y` 是参数声明,不是表达式
Point(this.x, this.y);
}
实例变量可以是 final
的,这种情况下它们必须且只能被设置一次。可以在声明时、使用构造函数参数或使用构造函数的初始化列表来初始化 final
的非 late
实例变量:
dart
class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}
如果需要在构造函数体开始执行后为 final
实例变量赋值,可以使用以下方法之一:
- 使用工厂构造函数。
- 使用
late final
,但要注意:没有初始值设定项的late final
会在 API 中添加一个 setter。
隐式接口
每个类都会隐式定义一个接口,该接口包含该类及其实现的所有接口的实例成员。如果你想创建一个类 A
,使其支持类 B
的 API 但不继承 B
的实现,那么类 A
应该实现 B
接口。
类通过在 implements
子句中声明接口,然后提供接口所需的 API 来实现一个或多个接口。例如:
dart
// 一个人,隐式接口包含 greet() 方法
class Person {
// 在接口中,但仅在本库中可见
final String _name;
// 不在接口中,因为这是一个构造函数
Person(this._name);
// 在接口中
String greet(String who) => 'Hello, $who. I am $_name.';
}
// Person 接口的实现类
class Impostor implements Person {
String get _name => '';
String greet(String who) => 'Hi $who. Do you know who I am?';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}
以下是指定一个类实现多个接口的示例:
dart
class Point implements Comparable, Location {
...
}
类变量和方法
使用 static
关键字来实现类级别的变量和方法。
静态变量
静态变量(类变量)对于类级别的状态和常量很有用:
dart
class Queue {
static const initialCapacity = 16;
// ···
}
void main() {
assert(Queue.initialCapacity == 16);
}
静态变量在使用之前不会被初始化。 注意: 本页面遵循了风格指南的建议,常量名使用小驼峰命名法(lowerCamelCase
)。
静态方法
静态方法(类方法)不操作实例,因此无法访问 this
。不过,它们可以访问静态变量。如下例所示,直接在类上调用静态方法:
dart
import 'dart:math';
class Point {
double x, y;
Point(this.x, this.y);
static double distanceBetween(Point a, Point b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
}
void main() {
var a = Point(2, 2);
var b = Point(4, 4);
var distance = Point.distanceBetween(a, b);
assert(2.8 < distance && distance < 2.9);
print(distance);
}
注意: 对于常见或广泛使用的工具函数和功能,考虑使用顶级函数而非静态方法。
可以将静态方法用作编译时常量。例如,可以将静态方法作为参数传递给常量构造函数。
构造方法
构造函数是用于创建类实例的特殊函数。
Dart 实现了多种类型的构造函数。除默认构造函数外,这些函数的名称与它们所属的类相同。
- 生成式构造函数:创建新实例并初始化实例变量。
- 默认构造函数:在未指定构造函数时用于创建新实例。它不接受参数且没有名称。
- 命名构造函数:明确构造函数的用途,或者允许为同一个类创建多个构造函数。
- 常量构造函数:将实例创建为编译时常量。
- 工厂构造函数:可以创建子类的新实例,或者从缓存中返回现有实例。
- 重定向构造函数:将调用转发到同一个类中的另一个构造函数。
构造函数的类型
生成式构造函数
要实例化一个类,可使用生成式构造函数。
dart
class Point {
// 用于存储点坐标的实例变量
double x;
double y;
// 带有初始化形式参数的生成式构造函数
Point(this.x, this.y);
}
默认构造函数
如果你没有声明构造函数,Dart 会使用默认构造函数。默认构造函数是一个没有参数和名称的生成式构造函数。
命名构造函数
使用命名构造函数可以为一个类实现多个构造函数,或者提供更清晰的语义。
dart
const double xOrigin = 0;
const double yOrigin = 0;
class Point {
final double x;
final double y;
// 在构造函数体运行之前设置 x 和 y 实例变量
Point(this.x, this.y);
// 命名构造函数
Point.origin() : x = xOrigin, y = yOrigin;
}
子类不会继承父类的命名构造函数。若要创建一个具有父类中定义的命名构造函数的子类,需要在子类中实现该构造函数。
常量构造函数
如果你的类生成的对象是不可变的,可以将这些对象设为编译时常量。要使对象成为编译时常量,需定义一个常量构造函数,并将所有实例变量设为 final
。
dart
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final double x, y;
const ImmutablePoint(this.x, this.y);
}
常量构造函数并不总是创建常量,它们可能在非常量上下文中被调用。要了解更多信息,请查阅“使用构造函数”部分。
重定向构造函数
一个构造函数可以重定向到同一个类中的另一个构造函数。重定向构造函数的函数体为空,在冒号(:
)后面使用 this
而不是类名。
dart
class Point {
double x, y;
// 该类的主构造函数
Point(this.x, this.y);
// 委托给主构造函数
Point.alongXAxis(double x) : this(x, 0);
}
工厂构造函数
在实现构造函数时遇到以下两种情况之一,可使用 factory
关键字:
- 构造函数并不总是创建其所属类的新实例。虽然工厂构造函数不能返回
null
,但它可能返回:- 从缓存中获取的现有实例,而非创建新实例。
- 子类的新实例。
- 在构造实例之前需要执行一些复杂的操作。这可能包括检查参数,或进行任何初始化列表无法处理的其他处理。
提示: 你也可以使用 late final
(需谨慎使用)来处理 final
变量的延迟初始化。
以下示例包含两个工厂构造函数:
Logger
工厂构造函数从缓存中返回对象。Logger.fromJson
工厂构造函数从 JSON 对象初始化final
变量。
dart
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));
}
factory Logger.fromJson(Map<String, Object> json) {
return Logger(json['name'].toString());
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
警告: 工厂构造函数不能访问 this
。
像使用其他构造函数一样使用工厂构造函数:
dart
var logger = Logger('UI');
logger.log('Button clicked');
var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);
重定向工厂构造函数
重定向工厂构造函数指定对另一个类的构造函数的调用,每当有人调用重定向构造函数时就会使用该调用。
dart
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;
看起来普通的工厂构造函数也可以创建并返回其他类的实例,这样重定向工厂构造函数就显得多余了。但重定向工厂构造函数有几个优点:
- 抽象类可以提供一个常量构造函数,该构造函数使用另一个类的常量构造函数。
- 重定向工厂构造函数避免了转发器重复形式参数及其默认值。
构造函数剥离
Dart 允许你不调用构造函数,而是将其作为参数传递。这被称为“剥离”(就像你去掉了括号),它相当于一个闭包,会使用相同的参数调用构造函数。
如果剥离的构造函数与方法接受的参数具有相同的签名和返回类型,你可以将该剥离构造函数用作参数或变量。
剥离与 lambda 表达式或匿名函数不同。lambda 表达式是构造函数的包装器,而剥离就是构造函数本身。
使用构造函数剥离
dart
// 对命名构造函数使用剥离
var strings = charCodes.map(String.fromCharCode);
// 对未命名构造函数使用剥离
var buffers = charCodes.map(StringBuffer.new);
不使用 lambda 表达式
dart
// 不使用 lambda 表达式来调用命名构造函数
var strings = charCodes.map((code) => String.fromCharCode(code));
// 不使用 lambda 表达式来调用未命名构造函数
var buffers = charCodes.map((code) => StringBuffer(code));
要了解更多讨论内容,请观看《Dart 构造函数剥离 | 解析 Flutter》视频。
实例变量初始化
Dart 可以通过三种方式初始化变量。
在声明时初始化实例变量
在声明变量时对实例变量进行初始化。
dart
class PointA {
double x = 1.0;
double y = 2.0;
// 隐式默认构造函数将这些变量设置为 (1.0, 2.0)
// PointA();
@override
String toString() {
return 'PointA($x,$y)';
}
}
使用初始化形式参数
为了简化将构造函数参数赋值给实例变量的常见模式,Dart 引入了初始化形式参数。
在构造函数声明中,包含 this.<属性名>
并省略函数体。this
关键字指向当前实例。
当存在名称冲突时,使用 this
;否则,按照 Dart 风格可以省略 this
。不过,对于生成式构造函数,必须在初始化形式参数名称前加上 this
。
如本指南前面所述,某些构造函数和构造函数的某些部分不能访问 this
,这些包括:
- 工厂构造函数
- 初始化列表的右侧
- 父类构造函数的参数
初始化形式参数还允许你初始化非可空或 final
实例变量。这两种类型的变量都需要进行初始化或设置默认值。
dart
class PointB {
final double x;
final double y;
// 在构造函数体运行之前设置 x 和 y 实例变量
PointB(this.x, this.y);
// 初始化形式参数也可以是可选的
PointB.optional([this.x = 0.0, this.y = 0.0]);
}
私有字段不能用作命名初始化形式参数。
dart
class PointB {
// ...
PointB.namedPrivate({required double x, required double y})
: _x = x,
_y = y;
// ...
}
这对命名变量同样适用。
dart
class PointC {
double x; // 必须在构造函数中设置
double y; // 必须在构造函数中设置
// 带有默认值的初始化形式参数的生成式构造函数
PointC.named({this.x = 1.0, this.y = 1.0});
@override
String toString() {
return 'PointC.named($x,$y)';
}
}
// 使用命名变量的构造函数
final pointC = PointC.named(x: 2.0, y: 2.0);
从初始化形式参数引入的所有变量都是 final
的,并且仅在被初始化变量的作用域内有效。
若要执行无法在初始化列表中表达的逻辑,可以创建一个包含该逻辑的工厂构造函数或静态方法,然后将计算出的值传递给普通构造函数。
构造函数参数可以设置为可空且不进行初始化。
dart
class PointD {
double? x; // 如果在构造函数中未设置,则为 null
double? y; // 如果在构造函数中未设置,则为 null
// 带有初始化形式参数的生成式构造函数
PointD(this.x, this.y);
@override
String toString() {
return 'PointD($x,$y)';
}
}
使用初始化列表
在构造函数体运行之前,你可以初始化实例变量,用逗号分隔各个初始化语句。
dart
// 初始化列表在构造函数体运行之前设置实例变量
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}
警告: 初始化列表的右侧不能访问 this
。
在开发过程中,若要验证输入,可以在初始化列表中使用 assert
。
dart
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}
初始化列表有助于设置 final
字段。
以下示例在初始化列表中初始化了三个 final
字段。点击“运行”来执行代码。
构造函数继承
子类不会从其父类继承构造函数。如果一个类没有声明构造函数,它只能使用默认构造函数。
一个类可以继承父类的参数,这些参数被称为父类参数。
构造函数的工作方式与调用一系列静态方法有些相似。每个子类都可以调用其父类的构造函数来初始化实例,就像子类可以调用父类的静态方法一样。这个过程不会“继承”构造函数的函数体或签名。
非默认父类构造函数
Dart 按以下顺序执行构造函数:
- 初始化列表
- 父类的未命名、无参数构造函数
- 主类的无参数构造函数
如果父类没有未命名、无参数的构造函数,则需要调用父类中的一个构造函数。在构造函数体(如果有的话)之前,在冒号(:
)后面指定父类构造函数。
在以下示例中,Employee
类的构造函数调用了其父类 Person
的命名构造函数。点击“运行”来执行以下代码。
由于 Dart 在调用父类构造函数之前会计算其参数,因此参数可以是像函数调用这样的表达式。
dart
class Employee extends Person {
Employee() : super.fromJson(fetchDefaultData());
// ···
}
警告: 父类构造函数的参数不能访问 this
。例如,参数可以调用静态方法,但不能调用实例方法。
父类参数
为避免在构造函数的 super
调用中逐个传递参数,可以使用父类初始化参数将参数转发到指定的或默认的父类构造函数。此功能不能用于重定向构造函数。父类初始化参数的语法和语义与初始化形式参数类似。
版本说明
使用父类初始化参数需要至少 2.17 版本的语言支持。如果你使用的是早期版本的语言,必须手动传递所有父类构造函数的参数。
如果父类构造函数的调用包含位置参数,则父类初始化参数不能是位置参数。
dart
class Vector2d {
final double x;
final double y;
Vector2d(this.x, this.y);
}
class Vector3d extends Vector2d {
final double z;
// 将 x 和 y 参数转发到默认的父类构造函数,就像这样:
// Vector3d(final double x, final double y, this.z) : super(x, y);
Vector3d(super.x, super.y, this.z);
}
为进一步说明,考虑以下示例:
dart
// 如果你使用任何位置参数调用父类构造函数 (`super(0)`),
// 使用父类参数 (`super.x`) 会导致错误。
Vector3d.xAxisError(super.x): z = 0, super(0); // 错误示例
这个命名构造函数试图设置两次 x
值:一次在父类构造函数中,一次作为位置父类参数。由于两者都针对 x
位置参数,因此会导致错误。
当父类构造函数有命名参数时,你可以将它们拆分为命名父类参数(下例中的 super.y
)和父类构造函数调用的命名参数(super.named(x: 0)
)。
dart
class Vector2d {
// ...
Vector2d.named({required this.x, required this.y});
}
class Vector3d extends Vector2d {
final double z;
// 将 y 参数转发到命名的父类构造函数,就像这样:
// Vector3d.yzPlane({required double y, required this.z})
// : super.named(x: 0, y: y);
Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}
方法
方法是为对象提供行为的函数。
实例方法
对象上的实例方法可以访问实例变量和 this
。以下示例中的 distanceTo()
方法就是一个实例方法:
dart
import 'dart:math';
class Point {
final double x;
final double y;
// 在构造函数体运行之前设置 x 和 y 实例变量
Point(this.x, this.y);
double distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
}
运算符
大多数运算符都是具有特殊名称的实例方法。Dart 允许你定义以下名称的运算符: <
>
<=
>=
==
~
-
+
/
~/
*
%
|
^
&
<<
>>>
>>
[]=
[]
注意: 你可能已经注意到,像 !=
这样的一些运算符不在名称列表中。这些运算符不是实例方法,它们的行为是 Dart 内置的。
要声明一个运算符,使用内置标识符 operator
然后跟上你要定义的运算符。以下示例定义了向量加法(+
)、减法(-
)和相等性(==
):
dart
class Vector {
final int x, y;
Vector(this.x, this.y);
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
@override
bool operator ==(Object other) =>
other is Vector && x == other.x && y == other.y;
@override
int get hashCode => Object.hash(x, y);
}
void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);
assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}
Getters 和 Setters
Getters 和 Setters 是特殊的方法,用于提供对对象属性的读写访问。请记住,每个实例变量都有一个隐式的 getter,如果合适的话还有一个 setter。你可以使用 get
和 set
关键字实现 getters 和 setters 来创建额外的属性:
dart
class Rectangle {
double left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算属性:right 和 bottom
double get right => left + width;
set right(double value) => left = value - width;
double get bottom => top + height;
set bottom(double value) => top = value - height;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}
使用 getters 和 setters,你可以从实例变量开始,之后用方法包装它们,而无需更改客户端代码。
注意: 诸如自增(++
)之类的运算符按预期方式工作,无论是否显式定义了 getter。为避免任何意外的副作用,运算符仅调用 getter 一次,将其值保存在临时变量中。
抽象方法
实例方法、getter 方法和 setter 方法可以是抽象的,它们定义了一个接口,但将其实现留给其他类。抽象方法只能存在于抽象类或混入(mixin)中。
要使一个方法成为抽象方法,使用分号(;
)代替方法体:
dart
abstract class Doer {
// 定义实例变量和方法...
void doSomething(); // 定义一个抽象方法
}
class EffectiveDoer extends Doer {
void doSomething() {
// 提供实现,因此这里的方法不是抽象的...
}
}
扩展一个类
使用 extends
来创建子类,使用 super
来引用超类:
dart
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}
如需了解 extends
的另一种用法,请参阅泛型页面中关于参数化类型的讨论。
重写成员
子类可以重写实例方法(包括运算符)、getter 和 setter。你可以使用 @override
注解来表明你是有意重写某个成员:
dart
class Television {
// ···
set contrast(int value) {
...
}
}
class SmartTelevision extends Television {
@override
set contrast(num value) {
...
}
// ···
}
重写方法的声明必须在以下几个方面与被重写的方法相匹配:
- 返回类型必须与被重写方法的返回类型相同(或为其子类型)。
- 参数类型必须与被重写方法的参数类型相同(或为其超类型)。在上述示例中,
SmartTelevision
的contrast
setter 将参数类型从int
更改为超类型num
。 - 如果被重写的方法接受
n
个位置参数,那么重写方法也必须接受n
个位置参数。 - 泛型方法不能重写非泛型方法,非泛型方法也不能重写泛型方法。
有时你可能想缩小方法参数或实例变量的类型。这违反了正常规则,类似于向下转型,可能会在运行时导致类型错误。不过,如果代码能够保证不会发生类型错误,那么缩小类型是可行的。在这种情况下,你可以在参数声明中使用 covariant
关键字。有关详细信息,请参阅 Dart 语言规范。
警告: 如果你重写了 ==
,那么你也应该重写 Object
的 hashCode
getter。
noSuchMethod()
为了在代码尝试使用不存在的方法或实例变量时进行检测或做出响应,你可以重写 noSuchMethod()
:
dart
class A {
// 除非你重写 noSuchMethod,否则使用不存在的成员会导致 NoSuchMethodError。
@override
void noSuchMethod(Invocation invocation) {
print(
'You tried to use a non-existent member: '
'${invocation.memberName}',
);
}
}
除非满足以下条件之一,否则你不能调用未实现的方法:
- 接收者的静态类型为
dynamic
。 - 接收者的静态类型定义了未实现的方法(抽象方法也可以),并且接收者的动态类型有一个与
Object
类中不同的noSuchMethod()
实现。
有关更多信息,请参阅非正式的 noSuchMethod
转发规范。
混入
混入(Mixins)是一种定义可在多个类层次结构中复用代码的方式。它们旨在批量提供成员实现。
要使用混入,需使用 with
关键字,后面跟一个或多个混入名称。以下示例展示了两个使用(或作为其子类)混入的类:
dart
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
要定义一个混入,使用 mixin
声明。在极少数情况下,如果你需要同时定义一个混入和一个类,可以使用 mixin class
声明。
混入和混入类不能有 extends
子句,并且不得声明任何生成式构造函数。
例如:
dart
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
指定混入可以调用自身的成员
有时,一个混入依赖于能够调用某个方法或访问字段,但它自身无法定义这些成员(因为混入不能使用构造函数参数来初始化自己的字段)。
以下部分介绍了不同的策略,以确保混入的任何子类都定义了混入行为所依赖的成员。
在混入中定义抽象成员
在混入中声明一个抽象方法会强制任何使用该混入的类型定义其行为所依赖的抽象方法。
dart
mixin Musician {
void playInstrument(String instrumentName); // 抽象方法。
void playPiano() {
playInstrument('Piano');
}
void playFlute() {
playInstrument('Flute');
}
}
class Virtuoso with Musician {
@override
void playInstrument(String instrumentName) { // 子类必须定义。
print('Plays the $instrumentName beautifully');
}
}
访问混入子类中的状态
声明抽象成员还允许你通过调用在混入中定义为抽象的 getter 来访问混入子类的状态:
dart
/// 可应用于任何具有 [name] 属性的类型,并根据该属性提供 [hashCode] 和 `==` 运算符的实现。
mixin NameIdentity {
String get name;
@override
int get hashCode => name.hashCode;
@override
bool operator ==(other) => other is NameIdentity && name == other.name;
}
class Person with NameIdentity {
final String name;
Person(this.name);
}
实现一个接口
与将混入声明为抽象类似,在混入上添加 implements
子句但实际上不实现该接口,也能确保为混入定义所有成员依赖项。
dart
abstract interface class Tuner {
void tuneInstrument();
}
mixin Guitarist implements Tuner {
void playSong() {
tuneInstrument();
print('Strums guitar majestically.');
}
}
class PunkRocker with Guitarist {
@override
void tuneInstrument() {
print("Don't bother, being out of tune is punk rock.");
}
}
使用 on
子句声明超类
on
子句用于定义 super
调用所解析的类型。因此,只有当你需要在混入内部进行 super
调用时才应该使用它。
on
子句强制任何使用混入的类也必须是 on
子句中指定类型的子类。如果混入依赖于超类中的成员,这可以确保在使用混入的地方这些成员是可用的:
dart
class Musician {
musicianMethod() {
print('Playing music!');
}
}
mixin MusicalPerformer on Musician {
performerMethod() {
print('Performing music!');
super.musicianMethod();
}
}
class SingerDancer extends Musician with MusicalPerformer { }
main() {
SingerDancer().performerMethod();
}
在这个例子中,只有继承或实现 Musician
类的类才能使用 MusicalPerformer
混入。因为 SingerDancer
继承自 Musician
,所以 SingerDancer
可以混入 MusicalPerformer
。
class
、mixin
还是 mixin class
?
版本说明:mixin class
声明需要至少 3.0 版本的语言支持。
mixin
声明定义一个混入。class
声明定义一个类。mixin class
声明定义一个既可以作为普通类又可以作为混入使用的类,具有相同的名称和类型。
dart
mixin class Musician {
// ...
}
class Novice with Musician { // 将 Musician 用作混入
// ...
}
class Novice extends Musician { // 将 Musician 用作类
// ...
}
任何适用于类或混入的限制也适用于混入类:
- 混入不能有
extends
或with
子句,混入类也不能有。 - 类不能有
on
子句,混入类也不能有。
枚举
枚举类型,通常称为枚举(enumerations 或 enums),是一种特殊的类,用于表示固定数量的常量值。
注意: 所有枚举都会自动继承 Enum
类。它们还是密封的,这意味着不能对其进行子类化、实现、混入,或以其他方式显式实例化。
抽象类和混入可以显式实现或继承 Enum
,但除非它们随后被枚举声明实现或混入,否则实际上没有对象可以实现该类或混入的类型。
声明简单枚举
要声明一个简单的枚举类型,使用 enum
关键字并列出你想要枚举的值:
dart
enum Color { red, green, blue }
提示: 在声明枚举类型时,你也可以使用尾随逗号,以避免复制粘贴时出错。
声明增强枚举
Dart 还允许枚举声明定义带有字段、方法和常量构造函数的类,这些类仅限于固定数量的已知常量实例。
要声明增强枚举,语法与普通类类似,但有一些额外要求:
- 实例变量必须是
final
的,包括混入添加的变量。 - 所有生成式构造函数必须是常量构造函数。
- 工厂构造函数只能返回固定的、已知的枚举实例之一。
- 不能继承其他类,因为会自动继承
Enum
。 - 不能重写
index
、hashCode
和相等运算符==
。 - 枚举中不能声明名为
values
的成员,因为它会与自动生成的静态values
getter 冲突。 - 枚举的所有实例必须在声明开头进行声明,并且至少要声明一个实例。
- 增强枚举中的实例方法可以使用
this
来引用当前的枚举值。
以下是一个声明增强枚举的示例,该枚举包含多个实例、实例变量、getter 和实现的接口:
dart
enum Vehicle implements Comparable<Vehicle> {
car(tires: 4, passengers: 5, carbonPerKilometer: 400),
bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);
const Vehicle({
required this.tires,
required this.passengers,
required this.carbonPerKilometer,
});
final int tires;
final int passengers;
final int carbonPerKilometer;
int get carbonFootprint => (carbonPerKilometer / passengers).round();
bool get isTwoWheeled => this == Vehicle.bicycle;
@override
int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}
版本说明: 增强枚举需要至少 2.17 版本的语言支持。
使用枚举
像访问其他静态变量一样访问枚举值:
dart
final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
print('Your favorite color is blue!');
}
枚举中的每个值都有一个 index
getter,它返回该值在枚举声明中的从零开始的位置。例如,第一个值的索引是 0,第二个值的索引是 1。
dart
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
要获取所有枚举值的列表,使用枚举的 values
常量。
dart
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
你可以在 switch
语句中使用枚举,如果你没有处理枚举的所有值,会收到警告:
dart
var aColor = Color.blue;
switch (aColor) {
case Color.red:
print('Red as roses!');
case Color.green:
print('Green as grass!');
default: // 没有这一行,你会看到一个警告。
print(aColor); // 'Color.blue'
}
如果你需要访问枚举值的名称,例如从 Color.blue
中获取 'blue'
,使用 .name
属性:
dart
print(Color.blue.name); // 'blue'
你可以像访问普通对象一样访问枚举值的成员:
dart
print(Vehicle.car.carbonFootprint);
扩展方法
扩展方法可以为现有的库添加功能。你可能在不知不觉中就使用了扩展方法。例如,当你在集成开发环境(IDE)中使用代码补全功能时,它会像显示常规方法一样显示扩展方法。
概述
当你使用别人的 API 或者实现一个被广泛使用的库时,通常很难甚至不可能去更改该 API。但你可能仍然希望添加一些功能。
例如,考虑下面将字符串解析为整数的代码:
dart
int.parse('42')
如果能让这个功能成为 String
类自身的方法,代码可能会更简洁,并且在使用工具时也更方便:
dart
'42'.parseInt()
要实现这样的代码,你可以导入一个包含 String
类扩展的库:
dart
import 'string_apis.dart';
void main() {
print('42'.parseInt()); // 使用扩展方法
}
扩展不仅可以定义方法,还可以定义其他成员,如 getter、setter 和运算符。此外,扩展可以有名称,当出现 API 冲突时,这会很有帮助。下面是如何使用一个作用于字符串的扩展(名为 NumberParsing
)来实现 parseInt()
扩展方法:
lib/string_apis.dart
dart
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
下一节将介绍如何使用扩展方法,之后的章节会介绍如何实现扩展方法。
使用扩展方法
和所有 Dart 代码一样,扩展方法存在于库中。你已经看到了如何使用扩展方法 —— 只需导入包含该扩展方法的库,然后像使用普通方法一样使用它:
dart
// 导入包含 String 扩展的库
import 'string_apis.dart';
void main() {
print('42'.padLeft(5)); // 使用 String 类的方法
print('42'.parseInt()); // 使用扩展方法
}
通常,了解这些就足以使用扩展方法了。在编写代码时,你可能还需要了解扩展方法如何依赖静态类型(而非动态类型),以及如何解决 API 冲突。
静态类型和动态类型
你不能对 dynamic
类型的变量调用扩展方法。例如,下面的代码会导致运行时异常:
dart
dynamic d = '2';
print(d.parseInt()); // 运行时异常:NoSuchMethodError
扩展方法可以和 Dart 的类型推断一起使用。下面的代码是可行的,因为变量 v
被推断为 String
类型:
dart
var v = '2';
print(v.parseInt()); // 输出:2
dynamic
类型不行的原因是,扩展方法是根据接收者的静态类型来解析的。由于扩展方法是静态解析的,所以调用它们的速度和调用静态函数一样快。
有关静态类型和动态类型的更多信息,请参阅《Dart 类型系统》。
API 冲突
如果扩展成员与接口或其他扩展成员发生冲突,你有几种选择。
一种选择是更改导入冲突扩展的方式,使用 show
或 hide
来限制暴露的 API:
dart
// 定义了 String 扩展方法 parseInt()
import 'string_apis.dart';
// 也定义了 parseInt(),但隐藏了 NumberParsing2 扩展方法
import 'string_apis_2.dart' hide NumberParsing2;
void main() {
// 使用 'string_apis.dart' 中定义的 parseInt()
print('42'.parseInt());
}
另一种选择是显式应用扩展,这样代码看起来就好像扩展是一个包装类:
dart
// 两个库都定义了 String 扩展,且都包含 parseInt(),并且扩展名称不同
import 'string_apis.dart'; // 包含 NumberParsing 扩展
import 'string_apis_2.dart'; // 包含 NumberParsing2 扩展
void main() {
// print('42'.parseInt()); // 这样不行
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());
}
如果两个扩展有相同的名称,那么你可能需要使用前缀来导入:
dart
// 两个库都定义了名为 NumberParsing 的扩展,且都包含扩展方法 parseInt()。
// 其中一个 NumberParsing 扩展(在 'string_apis_3.dart' 中)还定义了 parseNum()
import 'string_apis.dart';
import 'string_apis_3.dart' as rad;
void main() {
// print('42'.parseInt()); // 这样不行
// 使用 string_apis.dart 中的 NumberParsing 扩展
print(NumberParsing('42').parseInt());
// 使用 string_apis_3.dart 中的 NumberParsing 扩展
print(rad.NumberParsing('42').parseInt());
// 只有 string_apis_3.dart 中有 parseNum()
print('42'.parseNum());
}
如示例所示,即使你使用前缀导入,也可以隐式调用扩展方法。只有在显式调用扩展以避免名称冲突时,才需要使用前缀。
实现扩展方法
使用以下语法来创建扩展:
extension <扩展名称>? on <类型> { // <扩展名称> 是可选的
(<成员定义>)* // 可以提供一个或多个 <成员定义>
}
例如,下面是如何为 String
类实现一个扩展:
lib/string_apis.dart
dart
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
double parseDouble() {
return double.parse(this);
}
}
扩展的成员可以是方法、getter、setter 或运算符。扩展还可以有静态字段和静态辅助方法。要在扩展声明外部访问静态成员,需要通过声明名称来调用它们,就像调用类变量和方法一样。
无名称扩展
在声明扩展时,你可以省略名称。无名称扩展仅在声明它们的库中可见。由于它们没有名称,所以不能通过显式应用来解决 API 冲突。
dart
extension on String {
bool get isBlank => trim().isEmpty;
}
注意: 你只能在扩展声明内部调用无名称扩展的静态成员。
实现泛型扩展
扩展可以有泛型类型参数。例如,下面的代码为内置的 List<T>
类型扩展了一个 getter、一个运算符和一个方法:
dart
extension MyFancyList<T> on List<T> {
int get doubleLength => length * 2;
List<T> operator -() => reversed.toList();
List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}
类型 T
是根据调用这些方法的列表的静态类型来绑定的。
扩展类型
扩展类型是一种编译时的抽象概念,它用一个不同的、仅在静态层面有效的接口来“包装”现有的类型。扩展类型是静态 JavaScript 互操作性的重要组成部分,因为它们可以轻松修改现有类型的接口(这对于任何形式的互操作性都至关重要),而无需付出实际创建包装器的代价。
扩展类型对底层类型(称为表示类型)的对象所允许的操作集合(即接口)进行了约束。在定义扩展类型的接口时,你可以选择复用表示类型的某些成员,忽略其他成员,替换一些成员,并添加新的功能。
以下示例包装了 int
类型,创建了一个仅允许对 ID 号有意义的操作的扩展类型:
dart
extension type IdNumber(int id) {
// 包装 'int' 类型的 '<' 运算符:
operator <(IdNumber other) => id < other.id;
// 例如,不声明 '+' 运算符,
// 因为加法对于 ID 号没有意义。
}
void main() {
// 如果没有扩展类型的约束,
// 'int' 类型会使 ID 号面临不安全的操作:
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // 这样做可行,但对于 ID 号不应该被允许。
var safeId = IdNumber(42424242);
safeId + 10; // 编译时错误:没有 '+' 运算符。
myUnsafeId = safeId; // 编译时错误:类型不匹配。
myUnsafeId = safeId as int; // 可以:运行时强制转换为表示类型。
safeId < IdNumber(42424241); // 可以:使用包装后的 '<' 运算符。
}
注意: 扩展类型的作用与包装类相同,但不需要在运行时创建额外的对象。当需要包装大量对象时,创建额外对象的成本可能很高。由于扩展类型仅在静态层面有效,并且在运行时会被编译掉,因此它们本质上是零成本的。
扩展方法(也简称为“扩展”)是一种与扩展类型类似的静态抽象。然而,扩展方法会直接为其底层类型的每个实例添加功能。扩展类型则不同,扩展类型的接口仅适用于静态类型为该扩展类型的表达式。默认情况下,它们与底层类型的接口是不同的。
语法
声明
使用 extension type
声明和一个名称,后面跟着括号内的表示类型声明来定义一个新的扩展类型:
dart
extension type E(int i) {
// 定义操作集合。
}
表示类型声明 (int i)
指定了扩展类型 E
的底层类型是 int
,并且表示对象的引用名为 i
。该声明还引入了:
- 一个隐式的表示对象的 getter,其返回类型为表示类型:
int get i
。 - 一个隐式的构造函数:
E(int i) : i = i
。
表示对象使扩展类型能够访问底层类型的对象。该对象在扩展类型的主体中是可见的,你可以使用其名称作为 getter 来访问它:
- 在扩展类型主体内部,使用
i
(或在构造函数中使用this.i
)。 - 在外部,使用属性提取
e.i
(其中e
的静态类型是扩展类型)。
扩展类型声明也可以像类或扩展一样包含类型参数:
dart
extension type E<T>(List<T> elements) {
// ...
}
构造函数
你可以在扩展类型的主体中选择性地声明构造函数。表示声明本身是一个隐式构造函数,因此默认情况下它替代了扩展类型的未命名构造函数。任何额外的非重定向生成式构造函数都必须在其初始化列表或形式参数中使用 this.i
来初始化表示对象的实例变量。
dart
extension type E(int i) {
E.n(this.i);
E.m(int j, String foo) : i = j + foo.length;
}
void main() {
E(4); // 隐式未命名构造函数。
E.n(3); // 命名构造函数。
E.m(5, "Hello!"); // 带有额外参数的命名构造函数。
}
或者,你可以为表示声明构造函数命名,这样主体中就有空间定义一个未命名构造函数:
dart
extension type const E._(int it) {
E(): this._(42);
E.otherName(this.it);
}
void main2() {
E();
const E._(2);
E.otherName(3);
}
你还可以使用与类相同的私有构造函数语法 _
来完全隐藏构造函数,而不仅仅是定义一个新的构造函数。例如,如果你只希望客户端使用 String
来构造 E
,即使底层类型是 int
:
dart
extension type E._(int i) {
E.fromString(String foo) : i = int.parse(foo);
}
你还可以声明转发生成式构造函数或工厂构造函数(工厂构造函数也可以转发到子扩展类型的构造函数)。
成员
在扩展类型的主体中声明成员来定义其接口,方式与定义类成员相同。扩展类型的成员可以是方法、getter、setter 或运算符(不允许使用非外部实例变量和抽象成员):
dart
extension type NumberE(int value) {
// 运算符:
NumberE operator +(NumberE other) =>
NumberE(value + other.value);
// Getter:
NumberE get myNum => this;
// 方法:
bool isValid() => !value.isNegative;
}
表示类型的接口成员默认不是扩展类型的接口成员。要使表示类型的单个成员在扩展类型上可用,你必须在扩展类型定义中为其编写声明,就像 NumberE
中的 operator +
一样。你还可以定义与表示类型无关的新成员,如 i
getter 和 isValid
方法。
实现(implements)
你可以选择性地使用 implements
子句来:
- 在扩展类型上引入子类型关系。
- 将表示对象的成员添加到扩展类型接口中。
implements
子句引入了一种适用性关系,类似于扩展方法与其 on
类型之间的关系。适用于超类型的成员也适用于子类型,除非子类型有一个同名的声明。
扩展类型只能实现以下类型:
- 其表示类型。这会使表示类型的所有成员隐式地对扩展类型可用。
dart
extension type NumberI(int i)
implements int{
// 'NumberI' 可以调用 'int' 的所有成员,
// 以及它在此处声明的其他任何成员。
}
- 其表示类型的超类型。这会使超类型的成员可用,但不一定使表示类型的所有成员都可用。
dart
extension type Sequence<T>(List<T> _) implements Iterable<T> {
// 比 List 更好的操作。
}
extension type Id(int _id) implements Object {
// 使扩展类型不可为空。
static Id? tryParse(String source) => int.tryParse(source) as Id?;
}
- 另一个对相同表示类型有效的扩展类型。这允许你在多个扩展类型之间复用操作(类似于多重继承)。
dart
extension type const Opt<T>._(({T value})? _) {
const factory Opt(T value) = Val<T>;
const factory Opt.none() = Non<T>;
}
extension type const Val<T>._(({T value}) _) implements Opt<T> {
const Val(T value) : this._((value: value));
T get value => _.value;
}
extension type const Non<T>._(Null _) implements Opt<Never> {
const Non() : this._(null);
}
阅读“使用”部分以了解更多关于 implements
在不同场景下的效果。
@redeclare
在扩展类型中声明一个与超类型成员同名的成员,这与类之间的重写关系不同,而是一种重新声明。扩展类型的成员声明会完全替换任何同名的超类型成员。不可能为同一个函数提供另一种实现。
你可以使用 @redeclare
注解来告诉编译器你是有意使用与超类型成员相同的名称。如果实际上并非如此,例如某个名称拼写错误,分析器会发出警告。
dart
extension type MyString(String _) implements String {
// 替换 'String.operator[]'
@redeclare
int operator [](int index) => codeUnitAt(index);
}
你还可以启用 lint annotate_redeclares
来在声明一个隐藏了超接口成员且未使用 @redeclare
注解的扩展类型方法时得到警告。
使用
要使用扩展类型,像创建类的实例一样调用构造函数来创建实例:
dart
extension type NumberE(int value) {
NumberE operator +(NumberE other) =>
NumberE(value + other.value);
NumberE get next => NumberE(value + 1);
bool isValid() => !value.isNegative;
}
void testE() {
var num = NumberE(1);
}
然后,你可以像操作类对象一样调用该对象的成员。
扩展类型有两种同样有效但本质上不同的核心用例:
- 为现有类型提供扩展接口。
- 为现有类型提供不同的接口。
注意: 在任何情况下,扩展类型的表示类型都不是其扩展类型的子类型,因此在需要扩展类型的地方不能互换使用表示类型。
1. 为现有类型提供扩展接口
当扩展类型实现其表示类型时,你可以认为它是“透明的”,因为它允许扩展类型“看到”底层类型。
透明的扩展类型可以调用表示类型的所有成员(未重新声明的成员),以及它定义的任何辅助成员。这为现有类型创建了一个新的、扩展的接口。这个新接口适用于静态类型为扩展类型的表达式。
这意味着你可以调用表示类型的成员(与非透明扩展类型不同),如下所示:
dart
extension type NumberT(int value)
implements int {
// 没有显式声明 'int' 的任何成员。
NumberT get i => this;
}
void main () {
// 都可以:透明性允许在扩展类型上调用 'int' 的成员:
var v1 = NumberT(1); // v1 类型:NumberT
int v2 = NumberT(2); // v2 类型:int
var v3 = v1.i - v1; // v3 类型:int
var v4 = v2 + v1; // v4 类型:int
var v5 = 2 + v1; // v5 类型:int
// 错误:扩展类型接口对表示类型不可用
v2.i;
}
你还可以有一个“基本透明”的扩展类型,它通过重新声明超类型的给定成员名称来添加新成员并调整其他成员。例如,这允许你在方法的某些参数上使用更严格的类型,或者使用不同的默认值。
另一种基本透明的扩展类型方法是实现表示类型的超类型。例如,如果表示类型是私有的,但它的超类型定义了对客户端来说重要的接口部分。
2. 为现有类型提供不同的接口
非透明的扩展类型(即不实现其表示类型的扩展类型)在静态层面上被视为一个全新的类型,与它的表示类型不同。你不能将其赋值给它的表示类型,并且它不会暴露其表示类型的成员。
例如,考虑我们在“使用”部分声明的 NumberE
扩展类型:
dart
void testE() {
var num1 = NumberE(1);
int num2 = NumberE(2); // 错误:不能将 'NumberE' 赋值给 'int'。
num1.isValid(); // 可以:调用扩展成员。
num1.isNegative(); // 错误:'NumberE' 未定义 'int' 成员 'isNegative'。
var sum1 = num1 + num1; // 可以:'NumberE' 定义了 '+'。
var diff1 = num1 - num1; // 错误:'NumberE' 未定义 'int' 成员 '-'。
var diff2 = num1.value - 2; // 可以:可以通过引用访问表示对象。
var sum2 = num1 + 2; // 错误:不能将 'int' 赋值给参数类型 'NumberE'。
List<NumberE> numbers = [
NumberE(1),
num1.next, // 可以:'next' getter 返回类型 'NumberE'。
1, // 错误:不能将 'int' 元素赋值给列表类型 'NumberE'。
];
}
你可以通过这种方式使用扩展类型来替换现有类型的接口。这允许你为新类型的约束设计一个合理的接口(如引言中的 IdNumber
示例),同时还能受益于简单预定义类型(如 int
)的性能和便利性。
这种用例尽可能接近包装类的完全封装(但实际上只是一种有一定保护的抽象)。
类型考量
扩展类型是一种编译时的包装结构。在运行时,扩展类型完全没有痕迹。任何类型查询或类似的运行时操作都作用于表示类型。
这使得扩展类型是一种不安全的抽象,因为你总是可以在运行时找出表示类型并访问底层对象。
动态类型测试(e is T
)、类型转换(e as T
)和其他运行时类型查询(如 switch (e) ...
或 if (e case ...)
)都针对底层表示对象进行计算,并根据该对象的运行时类型进行类型检查。当 e
的静态类型是扩展类型,以及针对扩展类型进行测试时(case MyExtensionType(): ...
),都是如此。
dart
void main() {
var n = NumberE(1);
// 'n' 的运行时类型是表示类型 'int'。
if (n is int) print(n.value); // 输出 1。
// 可以在运行时对 'n' 使用 'int' 方法。
if (n case int x) print(x.toRadixString(10)); // 输出 1。
switch (n) {
case int(:var isEven): print("$n (${isEven ? "even" : "odd"})"); // 输出 1 (odd)。
}
}
类似地,在这个示例中,匹配值的静态类型是扩展类型:
dart
void main() {
int i = 2;
if (i is NumberE) print("It is"); // 输出 'It is'。
if (i case NumberE v) print("value: ${v.value}"); // 输出 'value: 2'。
switch (i) {
case NumberE(:var value): print("value: $value"); // 输出 'value: 2'。
}
}
在使用扩展类型时,了解这一特性很重要。始终要记住,扩展类型在编译时存在且有意义,但在编译过程中会被擦除。
例如,考虑一个表达式 e
,其静态类型是扩展类型 E
,E
的表示类型是 R
。那么,e
的值的运行时类型是 R
的子类型。甚至类型本身也会被擦除;在运行时,List<E>
与 List<R>
完全相同。
换句话说,真正的包装类可以封装被包装的对象,而扩展类型只是对被包装对象的一种编译时视图。虽然真正的包装更安全,但扩展类型的优势在于可以避免创建包装对象,这在某些场景下可以显著提高性能。
可call对象
为了让 Dart 类的实例能够像函数一样被调用,你需要实现 call()
方法。
call()
方法允许定义了该方法的任何类的实例模拟函数的行为。这个方法支持与普通函数相同的功能,比如参数和返回类型。
以下是一个示例,WannabeFunction
类定义了一个 call()
函数,它接受三个字符串,将它们用空格连接起来,并在末尾添加一个感叹号。你可以点击“运行”来执行这段代码。
dart
class WannabeFunction {
String call(String a, String b, String c) {
return '$a $b $c!';
}
}
void main() {
var instance = WannabeFunction();
var result = instance('Hello', 'World', 'Dart');
print(result); // 输出: Hello World Dart!
}
在这个示例中,WannabeFunction
类实现了 call()
方法。在 main
函数中,我们创建了 WannabeFunction
类的一个实例 instance
,然后像调用函数一样调用这个实例,传入三个字符串参数。最终,call()
方法会处理这些参数并返回一个拼接好的字符串,这个字符串被打印输出。