类
Dart 是一种基于类和基于 mixin 继承的面向对象语言。每个对象都是一个类的实例,除了 Null 以外的所有类都继承自 Object。基于 mixin 的继承意味着尽管每个类(除了顶级类 Object?)都只有一个超类,但一个类的主体可以在多个类层次结构中重用。扩展方法是一种向类添加功能的方式,而无需更改该类或创建子类。类修饰符允许您控制库如何对类进行子类型化。
使用类成员
对象拥有由函数和数据(分别是方法和实例变量)组成的成员。当你调用一个方法时,你是在一个对象上调用它:这个方法可以访问那个对象的函数和数据。
使用点(.)来引用实例变量或方法:
// 实例化一个Point类型的对象
var p = Point(2, 2);
// 得到y的值
assert(p.y == 2);
// 在对象p上调用distanceTo()方法
double distance = p.distanceTo(Point(4, 4));
使用 ?. 代替 . 来避免当最左操作数为null时引发异常:
// 如果p不为null,则将p对象的y变量传递给a变量,否则传递null给a变量
var a = p?.y;
使用构造函数
你可以使用构造函数来创建一个对象。构造函数的名称可以是 ClassName 或 ClassName.identifier。例如,以下代码使用 Point() 和 Point.fromJson() 构造函数创建了 Point 对象:
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});
以下代码实现了相同的功能,但在构造函数名称前使用了可选的 new 关键字:
var p1 = new Point(2, 2); // 可以省略掉 new 关键字
var p2 = new Point.fromJson({'x': 1, 'y': 2}); // 可以省略掉 new 关键字
某些类提供了常量构造函数。要使用常量构造函数创建编译时常量,请在构造函数名称前加上 const 关键字:
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final double x, y;
const ImmutablePoint(this.x, this.y);
}
// 使用 const 必须在定义的类构造函数中添加 const,要不然无法在实例化时添加 const
var p = const ImmutablePoint(2, 2);
构造两个相同的编译时常量会生成一个单一的规范实例:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
// 打印true,它们是同一个实例!如果没有 const 则不是同一个实例,打印 false
// identical方法在 dart.core 包下,用来检查两个对象引用是否指向同一个对象
print(identical(a, b));
在常量上下文中,你可以省略构造函数或字面量前的 const。例如,以下代码创建了一个常量映射:
// 这里有很多 const 关键字
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
你可以省略除第一个 const 关键字之外的所有 const:
// 只需要一个 const,它用于建立常量上下文
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
如果一个常量构造函数不在常量上下文中,并且在没有 const 的情况下被调用,它会创建一个非常量对象:
var a = const ImmutablePoint(1, 1); // 创建常量
var b = ImmutablePoint(1, 1); // 不会创建常量
print(identical(a, b)); // 打印 false,不是同一个实例!
获取对象的类型
要在运行时获取对象的类型,可以使用 Object 的 runtimeType 属性,它会返回一个 Type 对象。
var a = ImmutablePoint(1, 1);
print('a的类型是${a.runtimeType}'); // 打印:a的类型是ImmutablePoint
请注意!
建议使用类型测试运算符(如 is)来测试对象的类型,而不是使用 runtimeType。在生产环境中,object is Type 比 object.runtimeType == Type 更稳定。
到目前为止,你已经了解了如何使用类。本节的剩余部分将展示如何实现类。
实例变量
以下是声明实例变量的方式:
class Point {
double? x; // 声明实例变量 x,初始值为 null
double? y; // 声明实例变量 y,初始值为 null
double z = 0; // 声明实例变量 z,初始值为 0
}
使用可空类型声明的未初始化实例变量的值为 null。非可空类型的实例变量必须在声明时初始化。
所有实例变量都会生成一个隐式的 getter 方法。非 final 的实例变量和没有初始化器的 late final 实例变量还会生成一个隐式的 setter 方法。更多详情,请参阅 Getters 和 setters。
class Point {
double? x; // 声明实例变量 x,初始值为 null
double? y; // 声明实例变量 y,初始值为 null
}
void main() {
var point = Point();
point.x = 4; // 使用 x 的 setter 方法
print(point.x == 4); // 使用 x 的 getter 方法
print(point.y == null); // 值默认为 null
}
在声明时初始化非 late 实例变量会在实例创建时设置值,这发生在构造函数及其初始化列表执行之前。因此,非 late 实例变量的初始化表达式(在 = 之后)不能访问 this。
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 实例变量:在声明时初始化、使用构造函数参数初始化,或使用构造函数的初始化列表:
class ProfileMark {
final String name;
final DateTime start = DateTime.now();
ProfileMark(this.name);
ProfileMark.unnamed() : name = '';
}
如果你需要在构造函数体开始后为 final 实例变量赋值,可以使用以下方法之一:
- 使用工厂构造函数。
- 使用 late final,但要小心:没有初始化器的 late final 会为 API 添加一个 setter。
隐式接口
每个类都隐式定义了一个接口,该接口包含类的所有实例成员以及它实现的任何接口的成员。如果你想创建一个支持类 B 的 API 但不继承 B 的实现类 A,那么类 A 应该实现 B 的接口。
一个类通过在 implements 子句中声明一个或多个接口来实现这些接口,并提供接口所需的 API。例如:
// 一个 Person 类。隐式接口包含 greet() 方法
class Person {
// 在接口中,但仅在此库中可见
final String _name;
// 不在接口中,因为这是一个构造函数
// 如果Impostor类是通过extends的方式继承Person,则可以继承构造函数,通过implements方式实现Person则不可以
Person(this._name);
// 在接口中
String greet(String who) => 'Hello, $who. I am $_name.';
}
// Person 隐式接口的一个实现
class Impostor implements Person {
@override
String get _name => '';
@override
String greet(String who) => 'Hi $who. Do you know who I am?';
}
// Person 的子类
class Impostor2 extends Person {
Impostor2(super._name);
@override
String greet(String who) => 'Hello, $who. I am $_name.';
}
String greetBob(Person person) => person.greet('Bob');
void main() {
print(greetBob(Person('Kathy'))); // 打印:Hello, Bob. I am Kathy.
print(greetBob(Impostor2("Alexander"))); // 打印:Hello, Bob. I am Alexander.
print(greetBob(Impostor())); // 打印:Hi Bob. Do you know who I am?
}
以下是一个指定类实现多个接口的示例:
class Point implements Comparable, Location {
...
}
类变量和方法
使用 static 关键字来实现类范围的变量和方法。
静态变量
静态变量(类变量)对于类范围的状态和常量非常有用:
class Queue {
static const initialCapacity = 16;
// ···
}
void main() {
print(Queue.initialCapacity == 16); // 打印: true
}
静态变量在首次使用时才会被初始化。
提示!
本页面遵循风格指南的建议,优先使用 lowerCamelCase 作为常量名称。
静态方法
静态方法(类方法)不操作实例,因此无法访问 this。但它们可以访问静态变量。如下例所示,你可以直接在类上调用静态方法:
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 c = Point(2, 2);
// c.distanceBetween(a, b);
var distance = Point.distanceBetween(a, b);
assert(2.8 < distance && distance < 2.9);
print(distance);
}
提示!
对于常用或广泛使用的工具和功能,建议使用顶层函数,而不是静态方法。
顶层函数(Top-level functions)指的是直接定义在库文件的最外层,不属于任何类或对象的方法。它们可以直接被调用,无需通过类的实例来访问。顶层函数在 Dart 中非常有用,尤其是在编写库或模块时,它们可以作为公共接口提供给其他代码使用。
你可以将静态方法用作编译时常量。例如,你可以将静态方法作为参数传递给常量构造函数。
构造函数
构造函数是用于创建类实例的特殊函数。
Dart 实现了多种类型的构造函数。除了默认构造函数外,这些函数使用与类相同的名称。
- 生成构造函数:用于创建新的实例并初始化实例变量。
- 默认构造函数:当没有显式定义构造函数时,用于创建新实例。它不接受参数,也没有名称。
- 命名构造函数:用于明确构造函数的目的,或允许为同一个类创建多个构造函数。
- 常量构造函数:用于创建编译时常量的实例。
- 工厂构造函数:用于创建子类型的新实例,或从缓存中返回现有实例。
- 重定向构造函数:将调用转发给同一个类中的另一个构造函数。
注意!
Dart 不支持构造函数重载,可以用命名构造函数来实现类似功能。
构造函数的类型
生成构造函数
要实例化一个类,请使用生成构造函数。
class Point {
// 用于保存点坐标的实例变量。
double x;
double y;
// 使用初始化形式参数的生成构造函数:
Point(this.x, this.y);
}
默认构造函数
如果你没有声明构造函数,Dart 会使用默认构造函数。默认构造函数是一个没有参数和名称的生成构造函数。
命名构造函数
使用命名构造函数可以为类实现多个构造函数,或者提供更清晰的语义:
const double xOrigin = 0;
const double yOrigin = 0;
class Point {
final double x;
final double y;
// 在构造函数体运行之前设置 x 和 y 实例变量
Point(this.x, this.y);
// 命名构造函数(origin就是这个构造函数的命名名称)
Point.origin() : x = xOrigin, y = yOrigin;
}
子类不会继承父类的命名构造函数。如果需要在子类中使用父类定义的命名构造函数,必须在子类中实现该构造函数。
常量构造函数
如果你的类生成不可变的对象,可以将这些对象定义为编译时常量。为了使对象成为编译时常量,需要定义一个 const 构造函数,并将所有实例变量设置为 final。
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final double x, y;
const ImmutablePoint(this.x, this.y);
}
常量构造函数并不总是创建常量。它们可能在非 const 上下文中被调用。要了解更多信息,请参考关于使用构造函数的部分。
重定向构造函数
构造函数可以重定向到同一个类中的另一个构造函数。重定向构造函数的函数体为空,并在冒号(:)后使用 this 而不是类名来调用目标构造函数。
class Point {
double x, y;
// 这个类的主构造函数
Point(this.x, this.y);
// 委托给主构造函数(相当于调用主构造函数并传值)
Point.alongXAxis(double x) : this(x, 0);
// 注意,重定向构造函数不能有主体,下面是错误的
// Point.alongXAxis(double x) : this(x, 0){
// ...
// }
// 委托给alongXAxis(命名)构造函数
Point.alongXAxis2(double x) : this.alongXAxis2(x);
}
工厂构造函数
在以下两种情况下实现构造函数时,请使用 factory(工厂) 关键字:
-
构造函数并不总是创建其类的新实例。尽管工厂构造函数不能返回 null,但它可能会返回以下内容:
- 从缓存中返回现有实例,而不是创建一个新实例
- 子类型的新实例
-
在构造实例之前,您需要执行一些非简单的工作。这可能包括检查参数或执行无法在初始化列表中处理的其他操作。
小提示!
你也可以使用 late final 来处理 final 变量的延迟初始化(但要小心!)。
以下示例包含两个工厂构造函数。
Logger 工厂构造函数从缓存中返回对象。
Logger.fromJson 工厂构造函数从 JSON 对象初始化一个 final 变量。
class Logger {
final String name;
bool mute = false;
// _cache 是库私有的,外部无法访问和调用,这得益于其名称前的下划线 _
static final Map<String, Logger> _cache = <String, Logger>{};
// 工厂构造函数
factory Logger(String name) {
// putIfAbsent方法查找 key 的值,如果不存在则添加一个新条目,并返回刚添加的新条目(即 Value 的值)
// 类似工厂一样有多个 Logger 对象,有则返回缓存的,没有则添加
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
// 工厂构造函数(可以叫命名工厂构造函数?)
factory Logger.fromJson(Map<String, Object> json) {
// 调用的是上面的 factory Logger(String name) 工厂构造函数
return Logger(json['name'].toString());
}
// _internal 是私有的命名构造函数,外部无法访问和调用,这得益于其名称前的下划线 _
Logger._internal(this.name);
void log(String msg) {
if (!mute) print(msg);
}
}
请注意!
工厂构造函数不能访问 this
像使用其他构造函数一样使用工厂构造函数:
var logger = Logger('UI');
logger.log('Button clicked');
var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);
重定向工厂构造函数
重定向工厂构造函数指定了对另一个类的构造函数的调用,每当有人调用重定向构造函数时,都会使用该构造函数。
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable
看起来普通的工厂构造函数似乎可以创建并返回其他类的实例,这使得重定向工厂构造函数显得不必要。然而,重定向工厂构造函数具有以下几个优势:
- 抽象类可能提供一个常量构造函数,该构造函数使用另一个类的常量构造函数
- 重定向工厂构造函数避免了转发器重复形式参数及其默认值的需要
上面官方表达有点难以理解,结合上面这句话 “重定向工厂构造函数指定了对另一个类的构造函数的调用” 的意思,看下面的代码例子就懂了。
假设我们有一个基类 Animal 和两个子类 Dog 和 Cat。我们希望根据某种条件(如动物的类型)来创建适当的子类实例(符合标注红色内容中调用另一个类(子类)的构造函数的意思)。
class Animal {
final String name;
Animal(this.name);
// 工厂构造函数
factory Animal.create(String type, String name) {
if (type == 'dog') {
return Dog(name);
} else if (type == 'cat') {
return Cat(name);
} else {
throw ArgumentError('Unknown animal type: $type');
}
}
void speak() => print('Some generic animal sound');
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
void speak() => print('Woof! My name is ${this.name}');
}
class Cat extends Animal {
Cat(String name) : super(name);
@override
void speak() => print('Meow! My name is ${this.name}');
}
void main() {
var animal1 = Animal.create('dog', 'Rex');
animal1.speak(); // 输出: Woof! My name is Rex
var animal2 = Animal.create('cat', 'Whiskers');
animal2.speak(); // 输出: Meow! My name is Whiskers
// 如果传入未知类型,会抛出异常
try {
var unknown = Animal.create('bird', 'Tweety');
} catch (e) {
print(e); // 输出: Unknown animal type: bird
}
}
构造函数 Tear-offs
Dart 允许你将构造函数作为参数传递而不调用它。这种特性称为 tear-off(因为你“拆离”了括号),它充当一个闭包,使用相同的参数调用构造函数。
如果 tear-off(拆离)的构造函数与该方法接受的签名和返回类型相同,则可以将 tear-off 用作参数或变量。
Tear-offs(拆离)与 lambda 表达式或匿名函数不同。Lambda 表达式充当构造函数的包装器,而 tear-offs 就是构造函数本身。
使用 Tear-Offs
// 使用命名构造函数的 tear-off
var strings = charCodes.map(String.fromCharCode);
// 使用无名构造函数的 tear-off
var buffers = charCodes.map(StringBuffer.new);
不是 Lambda 表达式
// 而不是使用 lambda 表达式来包装命名构造函数
var strings = charCodes.map((code) => String.fromCharCode(code));
// 而不是使用 lambda 表达式来包装无名构造函数
var buffers = charCodes.map((code) => StringBuffer(code));
实例变量初始化
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.<propertyName> 并省略函数体。this 关键字引用当前实例。
当名称冲突存在时,使用 this。否则,Dart 风格省略 this。生成式构造函数是一个例外,必须在使用初始化形式参数时加上 this 前缀。
正如本指南前面提到的,某些构造函数和构造函数的某些部分无法访问 this。这些包括:
- 工厂构造函数
- 初始化列表的右侧
- 父类构造函数的参数
初始化形式参数还允许你初始化非空或 final 实例变量。这两种类型的变量都需要初始化或提供默认值。
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]);
}
私有字段不能用作命名的初始化形式参数。
class PointB {
// ...
PointB.namedPrivate({required double x, required double y})
: _x = x,
_y = y;
// ...
}
这也适用于命名变量。
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 的,并且仅在初始化变量的作用域内有效。
要执行无法在初始化列表中表达的逻辑,可以创建一个工厂构造函数或静态方法来实现该逻辑。然后,你可以将计算后的值传递给普通构造函数。
构造函数参数可以设置为可空类型,并且不需要初始化。
class PointD {
double? x; // 如果未在构造函数中设置,则为 null
double? y; // 如果未在构造函数中设置,则为 null
// 带有初始化形式参数的生成式构造函数
PointD(this.x, this.y);
@override
String toString() {
return 'PointD($x,$y)';
}
}
使用初始化列表
在构造函数体运行之前,你可以初始化实例变量。使用逗号分隔各个初始化器。
// 初始化列表在构造函数体运行之前设置实例变量
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}
请注意!
初始化列表的右侧不能访问this指针。
在开发过程中,为了验证输入的有效性,可以在初始化列表中使用assert。
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}
初始化列表有助于设置 final 字段。
以下示例在初始化列表中初始化了三个 final 字段。
import 'dart:math';
class Point {
final double x;
final double y;
final double distanceFromOrigin;
// 使用初始化列表初始化 final 字段
Point(double x, double y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}
void main() {
var p = Point(2, 3);
print(p.distanceFromOrigin);
}
构造函数继承
子类(或称为派生类)不会从其父类(或直接基类)继承构造函数。如果一个类没有声明构造函数,它只能使用默认构造函数。
一个类可以继承其父类的参数,这些参数被称为父类参数。
构造函数的工作方式与调用静态方法链有些类似。每个子类可以调用其父类的构造函数来初始化实例,就像子类可以调用父类的静态方法一样。这个过程并不会“继承”构造函数的主体或签名。
非默认的父类构造函数
在 Dart 中,构造函数的执行顺序遵循以下规则:
1、初始化列表
2、超类的无名无参构造函数
3、主类的无参构造函数
如果超类没有无名无参的构造函数,则需要调用超类中的某个构造函数。在构造函数体(如果有)之前,使用冒号(:)指定超类构造函数。
在以下示例中,Employee 类的构造函数调用了其父类 Person 的命名构造函数。
class Person {
String? firstName;
// 父类的命名构造函数
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Employee 类的构造函数,调用父类的命名构造函数
// 该类没有默认构造函数;你必须调用 super.fromJson()
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
void main() {
var employee = Employee.fromJson({});
print(employee);
// 打印:
// in Person
// in Employee
// Instance of 'Employee'
}
由于 Dart 在调用父类构造函数之前会先计算其参数,因此参数可以是表达式,例如函数调用。
class Employee extends Person {
Employee() : super.fromJson(fetchDefaultData()); // fetchDefaultData() 必须是静态方法
// ···
}
请注意!
传递给父类构造函数的参数不能访问 this。例如,参数可以调用静态方法,但不能调用实例方法。
// 不能通过this调用变量传值 Employee() : super.fromDetails(this.xxx) // 不可以调用方法传值(getName是一个类方法) Employee() : super.fromDetails(getName()) // 可以调用静态方法传值(getStaticName是一个静态方法) Employee() : super.fromDetails(getStaticName())
父类参数
为了避免将每个参数都传递到构造函数的 super 调用中,可以使用 super-initializer 参数将参数转发到指定或默认的父类构造函数中。此功能不能与重定向构造函数一起使用。
Super-initializer 参数的语法和语义与初始化形式参数类似。
版本提示!
使用 super-initializer 参数需要语言版本至少为 2.17。如果你使用的是较早的语言版本,则必须手动传递所有父类构造函数的参数。
如果父类构造函数的调用包含位置参数,则 super-initializer 参数不能是位置参数。
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);
// super.x 和 super.y 是 super-initializer 参数语法,子类用父类的构造函数参数作为自己构造函数的参数
Vector3d(super.x, super.y, this.z);
}
为了进一步说明,请考虑以下示例。
// 如果你使用任何位置参数调用父类构造函数(例如 super(0)),则使用 super 参数(例如 super.x)会导致错误
Vector3d.xAxisError(super.x): z = 0, super(0); // 不好的,使用重定向父类构造函数时就不能同时使用 super-initializer 参数的语法
// 下面两种方法是对的
Vector3d.xAxisError(): z = 0, super(0,9); // 方法一,使用重定向父类构造函数
Vector3d.xAxisError(super.x,super.y): z = 0; // 方法二,使用 super-initializer 参数的语法
这个命名构造函数试图两次设置 x 值:一次在父类的构造函数中,一次作为位置参数传递给父类构造函数。由于两者都作用于 x 这个位置参数,因此会导致错误。
当父类构造函数具有命名参数时,你可以将它们分为命名父类参数(如下例中的 super.y)和调用父类构造函数时的命名参数(如 super.named(x: 0))。
class Vector2d {
final double x;
final double y;
// 在{}里面的构造函数参数叫命名参数
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() 方法就是一个实例方法的例子:
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,然后跟上你要定义的运算符。以下示例定义了向量的加法(+)、减法(-)和相等(==):
class Vector {
final int x, y;
Vector(this.x, this.y);
// 将当前 Vector 对象和传进来的 Vector 对象的 x 和 y 变量分别相加的方法
Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
// 将当前 Vector 对象和传进来的 Vector 对象的 x 和 y 变量分别相减的方法
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);
// 判断当前 Vector 对象与传进来的 Vector 对象的 x 和 y 变量值是否都相等的方法(重写基类 Object 的 == 运算符)
@override
bool operator ==(Object other) =>
other is Vector && x == other.x && y == other.y;
// 当重写基类的 == 运算符时也必须重写基类的 hashCode getter 方法,已保持一致(这是因为 Dart(以及其他许多编程语言,如 Java)遵循一个重要的契约)
@override
int get hashCode => Object.hash(x, y);
}
void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);
// v + w 则相当于将两个对象里面的变量相加(内部调用 Vector operator +(Vector v))方法,然后再判断与 Vector(2, 3) 是否相等
print(v + w == Vector(4, 5));
// v - w 则相当于将两个对象里面的变量相减(内部调用 Vector operator -(Vector v))方法,然后再判断与 Vector(0, 1) 是否相等
print(v - w == Vector(0, 1));
// 都打印出 true
}
getter 和 setter 方法
getters 和 setters 是一种特殊的方法,用于提供对对象属性的读写访问。回想一下,每个实例变量都有一个隐式的 getter,如果合适的话,还会有一个隐式的 setter。你可以通过使用 get 和 set 关键字来实现额外的属性,从而创建自定义的 getter 和 setter 方法。
class Rectangle {
double left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定义两个计算属性:right 和 bottom
double get right => left + width; // get 方法不用带 () 括号
set right(double value) => left = value - width;
double get bottom => top + height; // get 方法不用带 () 括号
set bottom(double value) => top = value - height;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
print(rect.left == 3);
rect.right = 12;
print(rect.left == -8);
}
使用 getters 和 setters,你可以从实例变量开始,随后将它们封装到方法中,而无需更改客户端代码。
提示!
诸如递增(++)之类的操作符会以预期的方式工作,无论是否显式定义了 getter。为了避免任何意外的副作用,操作符会精确地调用 getter 一次,并将其值保存在一个临时变量中。
抽象方法
实例方法、getter 方法和 setter 方法都可以是抽象的,它们可以定义接口,但将具体的实现留给其他类来完成。抽象方法只能存在于抽象类或混入(mixins)类中。
要将一个方法定义为抽象方法,可以使用分号(;)代替方法体:
abstract class Doer {
// 定义实例变量和方法…
void doSomething(); // 定义一个 抽象方法
}
class EffectiveDoer extends Doer {
// 实现父类的 doSomething 方法
@override
void doSomething() {
// 提供一个实现,使该方法不再是抽象的
}
}
继承
使用 extends 创建一个子类,并使用 super 来引用父类:
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn(); // 调用父类(Television )的 turnOn 方法
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}
关于 extends 的另一种用法,可以参考泛型页面中关于参数化类型的讨论。
重写成员
子类可以重写实例方法(包括操作符)、getter 和 setter。你可以使用 @override 注解 来明确表示你是有意重写某个成员:
class Television {
// ···
set contrast(int value) {
...
}
}
class SmartTelevision extends Television {
// 重写父类的 contrast 方法,用 @override 注解
@override
set contrast(num value) {
...
}
// ···
}
重写方法的声明必须在多个方面与其重写的方法(或方法)相匹配:
- 返回类型必须与被重写方法的返回类型相同(或是其子类型)。
- 参数类型必须与被重写方法的参数类型相同,或者是其父类型。在前述示例中,SmartTelevision 的 contrast 设置器将参数类型从 int 更改为其父类型 num( num 是 int 的父类型)。
- 如果被重写的方法接受n个位置参数,那么重写的方法也必须接受n个位置参数。
- 泛型方法不能重写非泛型方法,非泛型方法也不能重写泛型方法。
有时您可能希望缩小方法参数或实例变量的类型。这种做法违反了常规规则,类似于向下转型,因为它可能在运行时引发类型错误。然而,如果代码能够保证不会发生类型错误,那么缩小类型是可行的。在这种情况下,您可以在参数声明中使用 covariant 关键字。有关详细信息,请参阅 Dart 语言规范。
请注意!
如果您重写了 == 运算符,那么您也应该重写 Object 的 hashCode getter 方法。有关如何重写 == 和 hashCode 的示例,请参阅 实现映射键 部分。
noSuchMethod()
为了检测或响应代码尝试使用不存在的方法或实例变量的情况,您可以重写 noSuchMethod() 方法:
class A {
// 除非您重写 noSuchMethod 方法,否则使用不存在的成员将导致 NoSuchMethodError 错误。
@override
void noSuchMethod(Invocation invocation) {
print(
'您尝试使用了一个不存在的成员: '
'${invocation.memberName}',
);
}
}
void main(){
dynamic a = A(); // 当接收类型是 dynamic 时
a.abc(); // 调用不存在的 abc 方法就会触发调用 noSuchMethod 方法
}
除非满足以下条件之一,否则您无法调用未实现的方法:
-
接收者的静态类型是 dynamic 。
-
接收者的静态类型定义了未实现的方法(抽象方法也可以),并且接收者的动态类型实现了 noSuchMethod() 方法,且该实现与 Object 类中的实现不同。
更多信息,请参阅非正式的 noSuchMethod 转发规范。
混入(Mixins)
Mixins 是一种定义代码的方式,这些代码可以在多个类层次结构中重复使用。它们旨在批量提供成员实现。
要使用 mixin,请使用 with 关键字,后跟一个或多个 mixin 名称。以下示例展示了两个使用 mixin(或者说,是 mixin 的子类)的类:
// Musician 类继承了 Performer 类并组合了 Musical(一个mixin)
class Musician extends Performer with Musical {
// ···
}
// Maestro 类继承了 Person 类并组合了 Musica、Aggressive、Demented(三个 mixin)
class Maestro extends Person with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
要定义一个 mixin,请使用 mixin 声明。在极少数情况下,如果您需要同时定义一个 mixin 和一个类,可以使用 mixin class 声明。
注意!
mixin Musician{} // 是一个 mixin
mixin class Musician{} // 是一个 mixin 类,多了 class 关键字
Mixin 和 mixin 类不能包含 extends 子句,并且不能声明任何生成构造函数。
例如:
// 定义一个 mixin
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');
}
}
}
指定 mixin 可以在自身上调用的成员
有时 mixin 依赖于能够调用方法或访问字段,但无法自行定义这些成员(因为 mixin 不能使用构造函数参数来实例化自己的字段)。
以下部分将介绍不同的策略,以确保 mixin 的任何子类都定义了 mixin 行为所依赖的成员。
在 mixin 中定义抽象成员
在 mixin 中声明抽象方法会强制任何使用该 mixin 的类型定义其行为所依赖的抽象方法。
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');
}
}
访问 mixin 子类中的状态
声明抽象成员还允许您通过调用在 mixin 上定义为抽象的 getter 来访问 mixin 子类中的状态:
/// 可以应用于任何具有 `[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);
}
void main() {
/// Person 组合了 NameIdentity,所以可以使用 == 运算符判断两个 Person 是否相等
var a = Person("张三");
var b = Person("李四");
print(a == b); // 打印 false
}
实现一个接口
与将 mixin 声明为抽象类似,在 mixin 上添加 implements 子句但实际上不实现接口,也可以确保为 mixin 定义任何成员依赖项:
abstract interface class Tuner {
void tuneInstrument();
// 注意:抽象接口类的方法哪怕有方法体,实现类都会强制要求去重写实现,所以没必要写方法体
}
/// Guitarist mixin通过implements子句实现了抽象接口类Tuner,但它不用实现Tuner
/// 的tuneInstrument方法,而是由Guitarist mixin组合的类(也可以称子类)来实现。
///
/// 类似Guitarist mixin也是一个抽象类或抽象接口
///
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.");
}
}
void main(){
var pr = PunkRocker();
pr.playSong();
// 打印
// Don't bother, being out of tune is punk rock.
// Strums guitar majestically.
}
使用 on 子句来声明一个超类
on 子句的作用是定义 super 调用的解析类型。因此,只有在需要在 mixin 中使用 super 调用时,才应该使用它。
on 子句强制要求任何使用 mixin 的类也必须是 on 子句中指定类型的子类。如果 mixin 依赖于超类中的成员,这可以确保在使用 mixin 的地方这些成员是可用的。
class Musician {
musicianMethod() {
print('Playing music!');
}
}
/// 由于 mixin 不能通过 extends 继承超类,则可以通过 on 拥有超类。
/// 但 MusicalPerformer 组合的类(也可以称子类)则必须继承这个超类
///
mixin MusicalPerformer on Musician {
performerMethod() {
print('Performing music!');
super.musicianMethod(); // 可以忽略super,例如 musicianMethod();
}
}
class SingerDancer extends Musician with MusicalPerformer { }
main() {
SingerDancer().performerMethod();
// 打印:
// Performing music!
// Playing music!
SingerDancer().musicianMethod();
// 打印:
// Playing music!
}
在这个例子中,只有继承 Musician 类的类才能使用 MusicalPerformer mixin。因为 SingerDancer 继承了 Musician,所以 SingerDancer 可以混入 MusicalPerformer。
类、mixin 还是 mixin 类?
版本提示!
mixin 类声明需要至少 3.0 的语言版本。
mixin 声明定义了一个 mixin。类声明定义了一个类。mixin 类声明定义了一个既可以作为普通类使用,也可以作为 mixin 使用的类,它们具有相同的名称和相同的类型。
mixin class Musician {
// ...
}
class Novice with Musician { // 将 Musician 用作 mixin
// ...
}
class Novice extends Musician { // 将 Musician 用作类
// ...
}
适用于类或 mixin 的任何限制同样适用于 mixin 类:
- Mixin 不能包含 extends 或 with 子句,因此 mixin 类也不能包含这些子句。
- 类不能包含 on 子句,因此 mixin 类也不能包含 on 子句。
枚举
枚举类型,通常称为枚举或enum,是一种特殊的类,用于表示固定数量的常量值。
提示!
所有枚举类型都会自动继承 Enum 类。它们也是密封的(sealed),这意味着枚举不能被继承、实现、混入,或者以其他方式显式实例化。
抽象类和混入(mixins)可以显式地实现或继承 Enum,但除非它们被枚举声明实现或混入,否则实际上不会有任何对象能够实现该抽象类或混入的类型。
声明简单的枚举
要声明一个简单的枚举类型,请使用 enum 关键字并列出需要枚举的值:
enum Color { red, green, blue }
小提示!
在声明枚举类型时,你也可以使用尾随逗号(即在最后一个枚举值加上逗号),以帮助防止复制粘贴错误。
声明增强型枚举
Dart 还允许枚举声明定义带有字段、方法和常量构造函数的类,这些类仅限于固定数量的已知常量实例。
要声明一个增强型枚举,需遵循与普通类相似的语法,但有一些额外的要求:
- 实例变量必须是 final 的,包括通过混入(mixins)添加的变量。
- 所有的生成构造函数必须是常量构造函数。
- 工厂构造函数只能返回枚举类型中固定的、已知的枚举实例之一。
- 没有其他类可以继承 Enum,因为 Enum 已经被自动继承。
- 不能重写 index、hashCode 以及相等运算符 ==。
- 不能在枚举中声明名为 values 的成员,因为它会与自动生成的静态 values getter 冲突。
- 枚举的所有实例必须在声明的开头部分声明,并且至少要声明一个实例。
增强型枚举中的实例方法可以使用 this 来引用当前的枚举值。
以下是一个声明增强型枚举的示例,其中包含多个实例、实例变量、getter 以及一个实现的接口:
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);
/// 枚举的生成构造函数由枚举值实例,
/// 例如上面的:car(tires: 4, passengers: 5, carbonPerKilometer: 400)
/// 这样是错误的:Vehicle(tires: 1,passengers: 1,carbonPerKilometer: 1)
///
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。
使用枚举
像访问其他静态变量一样访问枚举值:
final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
print('Your favorite color is blue!');
}
枚举中的每个值都有一个 index getter,它返回该值在枚举声明中从零开始的位置。
例如,第一个值的索引是 0,第二个值的索引是 1。
print(Color.red.index == 0);
print(Color.green.index == 1);
print(Color.blue.index == 2);
要获取所有枚举值的列表,请使用枚举的 values 常量。
List<Color> colors = Color.values;
print(colors[2] == Color.blue);
你可以在 switch 语句中使用枚举,如果没有处理枚举的所有值,你会收到一个警告:
var aColor = Color.blue;
// 在switch中必须处理完所有的枚举值,即case子句列举完每个
// 枚举值,如果不处理完可以使用default子句
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 属性:
print(Color.blue.name); // 'blue'
你可以像访问普通对象的成员一样访问枚举值的成员:
print(Vehicle.car.carbonFootprint);
扩展方法
扩展方法为现有的库添加功能。你可能在不知不觉中就已经使用了扩展方法。例如,当你在 IDE 中使用代码补全时,它会建议扩展方法以及常规方法。
如果观看视频有助于你学习,可以查看这个关于扩展方法的概述。
概述
当你使用别人的 API 或实现一个广泛使用的库时,通常不切实际或不可能直接修改 API。但你仍然可能希望添加一些功能。
例如,考虑以下将字符串解析为整数的代码:
int num = int.parse('42');
如果能够将这种功能直接放在 String 类中,可能会更好——更简短,也更容易与工具结合使用:
int num = '42'.parseInt();
为了实现这种功能,你可以导入一个包含 String 类扩展的库:
import 'string_apis.dart';
void main() {
print('42'.parseInt()); // 使用扩展方法
}
扩展不仅可以定义方法,还可以定义其他成员,例如 getter、setter 和 运算符。此外,扩展可以具有名称,这在发生 API 冲突时会很有帮助。以下是如何使用一个名为 NumberParsing 的扩展来操作字符串,并实现 parseInt() 扩展方法的示例:
/// lib/string_apis.dart
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
下一节将介绍如何使用扩展方法。之后的部分将讨论如何实现扩展方法。
使用扩展方法
与所有 Dart 代码一样,扩展方法也位于库中。你已经了解了如何使用扩展方法——只需导入包含扩展方法的库,然后像普通方法一样使用它即可:
// 导入一个包含 String 扩展的库
import 'string_apis.dart';
void main() {
print('42'.padLeft(5)); // 使用 String 方法
print('42'.parseInt()); // 使用扩展方法(String类型没有parseInt方法,只有String的扩展库才有)
}
通常,了解这些内容就足以使用扩展方法了。但在编写代码时,你可能还需要了解扩展方法如何依赖于静态类型(而不是 dynamic 类型),以及如何解决 API 冲突。
静态类型和 dynamic 类型
你不能在 dynamic 类型的变量上调用扩展方法。例如,以下代码会导致运行时异常:
dynamic d = '2';
print(d.parseInt()); // 运行时异常:NoSuchMethodError
扩展方法确实可以与 Dart 的类型推断一起使用。以下代码是有效的,因为变量 v 被推断为 String 类型:
var v = '2';
print(v.parseInt()); // 输出: 2
dynamic 类型无法使用扩展方法的原因是,扩展方法是根据接收者的静态类型解析的。由于扩展方法是静态解析的,因此它们的调用速度与调用静态函数一样快。
有关静态类型和 dynamic 类型的更多信息,请参阅 Dart 类型系统。
API 冲突
如果一个扩展成员与接口或其他扩展成员发生冲突,那么你有以下几种选择。
一种选择是更改导入冲突扩展的方式,使用 show 或 hide 来限制暴露的 API:
// 定义 String 的扩展方法 parseInt()
import 'string_apis.dart';
// 同样定义了 parseInt(),但通过隐藏 NumberParsing2 来隐藏该扩展方法。
import 'string_apis_2.dart' hide NumberParsing2;
void main() {
// 使用 string_apis.dart 中定义的 parseInt()
print('42'.parseInt());
}
另一种选择是显式地应用扩展,这会使代码看起来像是扩展是一个包装类:
// 两个库都定义了包含 parseInt() 的 String 扩展,但这些扩展具有不同的名称
import 'string_apis.dart'; // 包含 NumberParsing 扩展
import 'string_apis_2.dart'; // 包含 NumberParsing2 扩展。
void main() {
// print('42'.parseInt()); // 无法正常工作
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());
}
如果两个扩展具有相同的名称,那么你可能需要使用前缀来导入:
// 两个库都定义了名为 NumberParsing 的扩展,其中包含扩展方法 parseInt()。
// 其中一个 NumberParsing 扩展(在 string_apis_3.dart 中)还定义了 parseNum()。
import 'string_apis.dart';
import 'string_apis_3.dart' as rad; // rad为前缀(也成为别名)
void main() {
// print('42'.parseInt()); // 无法正常工作
// 使用 string_apis.dart 中的 ParseNumbers 扩展
print(NumberParsing('42').parseInt());
// 使用 string_apis_3.dart 中的 ParseNumbers 扩展
print(rad.NumberParsing('42').parseInt());
// 只有 string_apis_3.dart 中定义了 parseNum()
print('42'.parseNum());
}
如示例所示,即使使用前缀导入,你也可以隐式调用扩展方法。只有在显式调用扩展方法时需要使用前缀来避免名称冲突。
实现扩展方法
使用以下语法创建扩展:
extension <扩展名称>? on <type> { // <扩展名称> 是可选的
(<成员定义>)* // 可以提供 一个或多个 <成员定义>
}
例如,以下是如何在 String 类上实现扩展的示例:
/// lib/string_apis.dart
extension NumberParsing on String {
/// 将字符串数字转成整数
int parseInt() {
return int.parse(this);
}
/// 将字符串数字转成浮点型数
double parseDouble() {
return double.parse(this);
}
}
扩展的成员可以包含方法、getter、setter 或运算符。扩展还可以拥有静态字段和静态辅助方法。若要在扩展声明之外访问静态成员,需像调用类变量和类方法一样,通过声明名称进行调用。
未命名扩展
在声明扩展时,可以省略名称。未命名扩展仅在其声明的库内可见。由于没有名称,它们不能用于显式解决API冲突。
extension on String {
bool get isBlank => trim().isEmpty;
}
提示!
未命名扩展的静态成员只能在扩展声明内部调用。
实现泛型扩展
扩展可以拥有泛型类型参数。例如,以下代码通过 getter、运算符和方法对内置的 List<T> 类型进行了扩展:
extension MyFancyList<T> on List<T> {
// 获取List数组的双倍长度
int get doubleLength => length * 2;
// 返回一个倒序的独立的List(toList方法返回一个新建副本、完全独立的List,修改也不影响原List)
List<T> operator -() => reversed.toList();
// 分割元素
List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}
main() {
List<int> list = [0,1,2];
List<int> list2 = -list; // -list表示-运算符
print("list元素:$list"); // 打印:list元素:[0, 1, 2]
print("list2元素:$list2"); // 打印:list2元素:[2, 1, 0]
print("双倍长度:${list.doubleLength}"); // 打印:双倍长度:6
print("分割元素:${list.split(1)}"); // 打印:split:[[0], [1, 2]]
}
泛型类型参数 T 的绑定取决于调用扩展方法时列表的静态类型。
资源
关于扩展方法的更多信息,请参考以下内容:
扩展类型
扩展类型是一种编译时抽象机制,它通过一个不同的、仅静态的接口来 “封装” 现有类型。它们是静态JS互操作的主要组成部分,因为可以轻松修改现有类型的接口(这对任何类型的互操作都至关重要),而无需承担实际包装器的开销。
扩展类型对底层类型(称为表示类型)的对象可用操作集(或接口)实施约束。在定义扩展类型的接口时,您可以选择重用表示类型的部分成员,省略其他成员,替换其他成员,并添加新的功能。
以下示例通过封装 int
类型,创建了一个仅允许身份证号相关操作的扩展类型:
extension type IdNumber(int id) {
// 封装了 'int' 类型的 '<' 运算符
operator <(IdNumber other) => id < other.id;
// 例如:未声明 + 运算符,因为身份证号码不支持加法运算
}
void main() {
// 若无扩展类型的约束机制,int 类型会将身份证号码暴露于不安全操作中:
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // 虽然这种操作可以通过编译,但身份证号码不应允许此类运算
var safeId = IdNumber(42424242);
safeId + 10; // 编译时错误:未定义 '+' 运算符
myUnsafeId = safeId; // 编译时类型错误:类型不匹配
myUnsafeId = safeId as int; // 合法操作:运行时向表示类型转型
safeId < IdNumber(42424241); // 合法操作:使用封装的 '<' 运算符
}
提示!
扩展类型与包装类的用途相同,但无需创建额外的运行时对象。当需要包装大量对象时,这种额外开销可能非常显著。由于扩展类型是纯静态的,且在运行时会被完全编译优化,因此本质上不会产生性能开销。
扩展方法(也称为 “扩展” )是一种类似于扩展类型的静态抽象。不过,扩展方法会直接为其底层类型的所有实例添加功能。扩展类型则不同;扩展类型的接口仅适用于静态类型为该扩展类型的表达式。默认情况下,它们与其底层类型的接口是不同的。
语法
声明
用 extension type 声明定义一个扩展类型,后跟一个名称,再在括号内声明其表示类型:
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 的静态类型为该扩展类型)。
扩展类型声明也可以像类或扩展一样包含类型参数:
extension type E<T>(List<T> elements) {
// ...
}
构造函数
您可以在扩展类型的主体中可选地声明构造函数。表示声明本身就是一个隐式构造函数,因此默认情况下它会替代扩展类型的未命名构造函数。任何额外的非重定向生成构造函数都必须在其初始化列表或形式参数中使用 this.i 来初始化表示对象的实例变量。
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!"); // 带额外参数的命名构造函数
}
或者,您也可以为表示声明构造函数指定名称,这样在扩展类型主体中就可以额外定义一个无命名构造函数:
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:
extension type E._(int i) {
E.fromString(String foo) : i = int.parse(foo);
}
您还可以声明转发生成构造函数或工厂构造函数(这些构造函数也可以转发到子扩展类型的构造函数)。
成员
在扩展类型的主体中声明成员,可以像定义类成员一样来定义其接口。扩展类型的成员可以包括方法、getter、setter 或运算符(不允许声明非外部实例变量和抽象成员):
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 子句引入了一种适用性关系,类似于扩展方法与其 on 类型之间的关系。适用于超类型的成员同样适用于子类型,除非子类型已显式声明了同名成员。
扩展类型仅允许实现以下内容:
- 它的表示类型。这使得表示类型的所有成员隐式地可用于该扩展类型。
extension type NumberI(int i) implements int{ // 'NumberI' 可以调用 int 的所有成员,以及在此处声明的其他任何成员。 }
- 其表示类型的超类型。这使得超类型的成员可用,而不必包含表示类型的所有成员。
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?; }
- 在同一个表示类型上有效的另一种扩展类型。这允许你在多个扩展类型之间复用操作(类似于多重继承)。
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 注解来告知编译器你是有意使用与父类型成员相同的名称。这样分析器会在实际不符合该情况时,例如名称拼写错误时,向你发出警告。
extension type MyString(String _) implements String {
// 替换 'String.operator[]'
@redeclare
int operator [](int index) => codeUnitAt(index);
}
你也可以启用 annotate_redeclares 静态检查规则(lint),这样当你声明了一个隐藏父接口成员但未添加 @redeclare 注解的扩展类型方法时,编译器会发出警告。
用法
要使用扩展类型,可以像类一样创建实例:通过调用构造函数来实现:
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、为现有类型提供扩展接口。
2、为现有类型提供不同的接口。
提示!
无论如何,扩展类型的表示类型绝不会是其子类型,因此,在需要扩展类型的地方,不能直接使用表示类型作为替代。
1、为现有类型提供扩展接口
当一个扩展类型实现了它的表示类型时,你可以认为它是 '透明' 的,因为这允许扩展类型 '看到' 底层类型。
一个透明的扩展类型可以调用表示类型的所有成员(未被重新声明的),以及它定义的任何辅助成员。这为现有类型创建了一个新的、扩展的接口。新的接口对静态类型是该扩展类型的表达式可用。
这意味着(与非透明扩展类型不同)您可以调用表示类型的成员,如下所示:
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 无法访问扩展类型接口
}
您还可以创建一种 "半透明" 扩展类型,它既能添加新成员,又能通过重新声明父类型的特定成员来适配现有成员。例如,这允许您对方法的某些参数使用更严格的类型约束,或设置不同的默认值:
extension type Celsius(num value) implements num {
// 新增扩展成员
String get symbol => '°C';
// 调整现有成员:强化参数类型
bool operator >(Celsius other) => value > other.value;
// 修改默认值(precision 默认保留小数点2位)
String toStringAsFixed([int precision = 2]) => '${value.toStringAsFixed(precision)}°C';
}
void main(){
final temp = Celsius(37.2);
var str = temp.toStringAsFixed();
print(str); // 打印:37.20°C
print(temp > Celsius(36.5)); // 打印:true
print(temp > 36.5); // 编译错误:参数必须是Celsius类型
}
另一种实现 "半透明" 扩展类型的方式是:让扩展类型实现表示类型的父类型。例如当表示类型本身为私有类型时,可通过父类型向客户端提供必要的公共接口:
// 📁 lib/internal.dart (SDK内部)
class _PrivateImpl extends PublicInterface {
String _secret = "confidential";
void internalMethod() => print(_secret);
@override
void publicMethod() => print("Public data");
}
abstract class PublicInterface {
void publicMethod();
}
// 📁 lib/public.dart (对外暴露)
extension type PublicFacade(_PrivateImpl impl) implements PublicInterface {
void publicMethod() => impl.publicMethod();
}
// 📄 client.dart (使用者视角)
import 'package:my_sdk/public.dart';
void main() {
// ✅ 安全访问
final safe = PublicFacade(_PrivateImpl());
safe.publicMethod(); // 输出: "Public data"
// ❌ 危险操作被禁止
safe.internalMethod(); // 编译错误:未暴露此方法
print(safe._secret); // 编译错误:无法访问私有字段
}
2、为现有类型提供不同的接口
非透明(即未 implement 其表示类型)的扩展类型在静态层面会被视为一个全新的类型,与其表示类型完全不同。你无法将其赋值给表示类型,它也不会公开表示类型的成员。
例如,以我们在 “用法” 部分声明的 NumberE 扩展类型为例:
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():...),这一规则都成立。
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)
}
}
同理,在此示例中,匹配值的静态类型为该扩展类型:
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> 是完全相同的东西。
换句话说,一个真正的包装类可以封装被包装对象,而扩展类型只是被包装对象的一个编译时视图。虽然真正的包装类更安全,但权衡之下扩展类型让你可以选择避免包装对象,这在某些场景中可以大幅提升性能。
可调用的对象
要让你的 Dart 类的实例可以像函数一样被调用,请实现 call() 方法。
任何类只要定义了 call() 方法,其实例就能像函数一样被调用。该方法支持普通函数的所有功能,包括参数传递和返回类型。
在下面的示例中,WannabeFunction 类定义了一个 call() 方法,该方法接收三个字符串参数,用空格将它们连接起来,并在末尾添加一个感叹号。
class WannabeFunction {
String call(String a, String b, String c) => '$a $b $c!';
String getTest() => "******";
}
void main() {
var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');
print(out);
print(wf.getTest());
}