Dart
是一种面向对象的编程语言,支持类和基于混入(Mixin
)的继承机制。在 Dart
中除 Null
以外的所有类都继承自 Object
类。
1. 基本用法
Dart
中类的基本用法和大部分面向对象语言差不多,这里不作详细介绍,直接从一个简单的示例开始:
// 导入依赖库
import 'dart:math';
class Point {
// 成员变量
double? x; // 默认值初始值为null
double y = 0; // 初始值为0
// 构造函数
Point(double x, double y) {
this.x = x;
this.y = y;
}
//成员方法
double distanceTo(Point other) {
double dx = (this.x ?? 0) - (other.x ?? 0);
var dy = this.y - other.y;
return sqrt(dx * dx + dy * dy);
}
// 静态方法
static double distanceBetween(Point a, Point b) {
return a.distanceTo(b);
}
// 重写toString方法
@override
String toString() {
return 'Point($x, $y)';
}
}
调用也很简单,代码如下:
void main() {
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
print(p1.distanceTo(p2)); // 5.0
print(Point.distanceBetween(p1, p2)); // 5.0
print(p1); // Point(0.0, 0.0)
print(p2); // Point(3.0, 4.0)
print(p2.y); // 4.0
}
单看上述代码,几乎分不清究竟是 JavaScript
、Java
, 还是 Dart
,下文主要介绍一些 Dart
相对独特的地方。
2. 构造函数
构造函数是用于创建类实例的特殊函数,Dart
支持多种类型的构造函数,如 生成式构造函数(Generative constructors
)、默认构造函数(Default constructors
)、命名构造函数(Named constructors
)、常量构造函数(Constant constructors
)、重定向构造函数(Redirecting constructor
) 以及 工厂构造函数(Factory constructors
) 等,在命名上又有 ClassName
和 ClassName.identifier
两种形式。
乍一看,概念多且晦涩,但实际上,面向对象编程语言的核心内容都差不多,所谓新概念不过是换了个说法而已,接下来,看看下面的具体用法,就什么都清楚了。
2.1 生成式构造函数
相较于上述示例中的常规用法,为简化构造函数声明以及为成员变量赋值,Dart
给出了一种更简洁的语法,代码如下:
class Point {
// ...
// 构造函数
Point(this.x, this.y);
// ...
}
这就是 生成式构造函数,也是最常用的声明方式,其中,this.x
和 this.y
并非表达式,而是参数声明,不可参与运算,也就是说,Point(this.x + 1, this.y);
是不合法的,而 Point(double? this.x, double this.y);
却是可以的。
另外,如有必要,生成式构造函数 也可以有函数体:
Point(this.x, this.y) {
print('Point($x, $y)');
}
创建对象时,new
关键字可以省略,事实上,省略反而更为常见,代码如下:
Point p1 = Point(0, 0); // 等价于 new Point(0, 0);
Point p2 = Point(3, 4);
2.2 默认构造函数
和大多数面向对象语言一样,如果类中没有显式声明构造函数,就会使用默认构造函数。默认构造函数是一种无参数、无名称的生成式构造函数。
2.3 命名构造函数
使用命名构造函数,可为一个类实现多个构造函数,或提供更清晰的语义说明。说到底,其本质就是解决构造函数重载的问题。因为 Dart
不支持函数(方法)重载(函数名相同,但参数列表不同),构造函数也是函数,自然也不能重载。命名构造函数形如 ClassName.identifier
,具体代码如下:
class Point {
// ...
// 生成式构造函数
Point(this.x, this.y);
// 命名构造函数
Point.origin() : x = 0, y = 0;
// 命名构造函数
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}
// ...
}
其中,Point.origin
和 Point.fromJson
就是命名构造函数,具备了更明确的语义,而 :
后面的是初始化列表,这个在下文成员变量的章节再作具体说明。
创建对象时的代码如下:
var origin = Point.origin(); // 等价于 new Point.origin();
var p = Point.fromJson({'x': 1, 'y': 2}); // 等价于 new Point.fromJson({'x': 1, 'y': 2});
2.4 常量构造函数
用于创建编译时常量实例。若类需生成恒定不变的对象,应将其设为编译时常量。实现方式为:定义 const
构造函数,且所有实例变量必须声明为 final
,具体代码如下:
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final double x, y;
const ImmutablePoint(this.x, this.y);
}
创建对象时也需要使用 const
关键字:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a, b));// 二者是同一个实例
在常量上下文中,构造器或字面量前的 const
关键字可省略。如:
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
可简化为:
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
然而,常量构造函数并不总是创建常量实例,当在非常量上下文中调用时,它们创建的将是普通对象。
var a = const ImmutablePoint(1, 1); // 常量上下文,创建常量实例
var b = ImmutablePoint(1, 1); // 非常量上下文,创建普通实例
assert(!identical(a, b)); // 二者是不同实例
2.5 重定向构造函数
构造函数可重定向至同一类中的其他构造函数。重定向构造函数没有函数体,其调用方式是在冒号(:
)后使用 this
而非类名。例如,前面的 Point.origin
可修改为如下形式的重定向构造函数:
Point.origin() : this(0, 0);
当然,这里的 this
会重定向到生成式构造函数,如果希望重定向到命名构造函数,则应该替换成对应的this.identifier
,如:
Point.origin() : this.fromJson({'x': 0, 'y': 0});
创建对象的方法和命名构造函数一样,就不再重复举例了。
2.6 工厂构造函数
当遇到下面两种情况时,可使用 factory
关键字定义工厂构造函数:
构造函数并非总是创建类的新实例。虽然工厂构造函数不能返回
null
,但可以返回:- 来自缓存的现有实例(非创建新实例)
- 子类型的新实例
- 构造实例的逻辑比较消耗系统资源,包括参数校验或任何无法在初始化列表中处理的其他操作。
以下示例包含两个工厂构造函数:
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) {
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
。
使用工厂构造函数的方式和普通构造函数一样:
var logger = Logger('UI');
logger.log('Button clicked');
var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);
3. 成员变量
3.1 实例变量
在 Dart
中,实例变量的调用和其他语言没什么两样,如 p1.x
,在有些情况下,也可使用 ?.
(空安全语法) 代替 .
以避免可能出现的空引用异常,如 p1?.x
,具体就不展开介绍了,这里主要关注一下实例变量的声明和初始化。
实例变量必须在执行构造函数体之前完成初始化。Dart
可通过如下三种方式初始化变量:
- 在声明变量的同时对其进行初始化,即在变量声明的同时给变量赋值(如
double y = 0;
),如果此时不赋值,就需要将变量声明为可空类型(如double? x;
,本质上也赋了值,因为默认值为null
),如果即不赋值也不声明为可空类型,则必须通过后面两种方式中的一种进行初始化; 使用初始化形式参数。示例代码如下:
class Point { // 成员变量,既没有赋值,也没有申明为可空类型 double x; double y; // 使用初始化形式参数 Point(this.x, this.y); }
使用初始化列表。初始化列表在
:
之后,多个初始化式之间需用逗号分隔。示例代码如下:class Point { // 成员变量,既没有赋值,也没有申明为可空类型 double x; double y; // 使用初始化列表 Point() : x = 0, y = 0; }
初始化形式参数和初始化列表都在构造函数体执行之前执行,二者任选其一,但不可同时使用。
3.1.1 final
实例变量也可被声明为 final
,表示变量初始化之后不可修改,此类变量必须被严格赋值一次(显示赋值,默认值不行),也就是说,不能写成 final double? x;
,但可以写成 final double? x = null;
或 final double x = 0;
。
除此之外,final
实例变量初始化的位置和普通实例变量也是一样的,示例如下:
class ProfileMark {
// 1. 声明时赋值
final DateTime start = DateTime.now();
final String name;
// 2. 初始化形式参数赋值
ProfileMark(this.name);
// 3. 初始化列表赋值
ProfileMark.unnamed() : name = '';
}
3.1.2 late
前面提到的都是在构造函数体执行前完成实例变量的初始化,这是 Dart
语法要求的,但在有些场景中,我们希望 延迟初始化(如在构造函数体中或更晚的时候),这时就要用到 late
关键字了。示例代码如下:
class Point {
// 成员变量
late double x;
late double y;
// 变量前加上 late 后,就可以在函数体中初始化了
Point(double x, double y) {
this.x = x;
this.y = y;
}
}
这种方式虽然更加灵活,但使用起来也要更加小心,因为如果初始化的时机不对,就很有可能用到未赋值的变量,从而导致程序异常。
3.1.3 late final
实例变量还可以同时增加 late final
关键字限定。顾名思义,就是延迟初始化一个不可变的实例变量。由于这里同时用到了 late
和 final
两个关键字,所以更要小心使用,因为无论是后续用到了未赋值的变量,还是对变量多次赋值,都会导致程序异常。
3.2 静态变量
静态变量又叫类变量,通过 static
关键字修饰。静态变量直到被使用时才会初始化。示例如下:
class Queue {
static const initialCapacity = 16;
// ···
}
void main() {
assert(Queue.initialCapacity == 16);
}
4. 成员方法
4.1 实例方法
对象上的实例方法可以访问实例变量和 this
,文章开头示例中的 distanceTo()
方法就是实例方法的一个例子,具体声明和普通函数一样,详见 Dart 语法要点(2) —— 函数,这里就不再赘述了。
4.2 静态方法
静态方法(类方法)不操作于实例之上,因此无法访问 this
。但静态方法可以访问静态变量。文章开头示例中的 distanceBetween()
方法就是静态方法的一个例子,静态方法需直接通过类进行调用。
4.3 运算符
大多数运算符实则为具有特殊名称的实例方法。声明运算符,请使用内置标识符 operator
后接要定义的运算符。以下示例定义了向量加法(+
)、减法(-
)以及相等判断(==
):
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));
}
4.4 Getter 和 Setter
Getter
和 Setter
是用于对对象属性进行读写访问的特殊方法。每个实例变量均拥有一个隐式的 getter
方法,若变量非 final
还会具备对应的 setter
方法。通过 get
和 set
关键字,可以显示实现属性访问器。
class Rectangle {
double left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
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);
}
4.5 抽象方法
实例方法、Getter
及 Setter
方法均可声明为抽象方法,用于定义接口而将具体实现交由其他类完成。抽象方法只能存在于抽象类或 Mixin
中。
abstract class Doer {
// 抽象方法
void doSomething();
}
class EffectiveDoer extends Doer {
void doSomething() {
// 提供具体实现
}
}
5. 私有成员
Dart
中没有类似其他语言中的 public
、protected
、private
等访问修饰符,而是通过在任何标识符(变量、方法、类)前添加下划线(_
)前缀来实现基于库的私有化的。也就是说,Dart
中默认都是公开的,无法设置为“受保护”类型,即使加上下划线(_
)前缀,也和传统意义上类级别的私有不同,Dart
中的私有是库(文件)级别的,也就是说在同一个库(文件)中,不存在私有成员。
class Person {
String _name;
Person(this._name);
}
void main() {
Person p = Person('张三');
print(p._name); // 可以正常访问
}
上述示例中,p._name
可以正常访问,但如果将 Person
类分离到一个单独的文件中,p._name
就不合法了。
另外,不能将私有字段用作命名初始化形参。命名参数不能以下划线开头。
6. 继承
使用 extends
创建子类,并使用 super
引用父类。还是以 Point
类为例,这里再定义一个 Point3D
子类:
class Point3D extends Point {
double z = 0;
Point3D(double x, double y, double z) : super(x, y) {
this.z = z;
}
@override
double distanceTo(Point other) {
if (other is Point3D) {
double dx = (this.x ?? 0) - (other.x ?? 0);
var dy = this.y - other.y;
var dz = this.z - other.z;
return sqrt(dx * dx + dy * dy + dz * dz);
} else {
return super.distanceTo(other);
}
}
var a = () => 1;
@override
String toString() {
return 'Point3D($x, $y, $z)';
}
}
子类不会继承父类的命名构造函数。若要创建一个包含超类中已定义命名构造函数的子类,需在子类中实现该构造函数。
Dart
按以下顺序执行构造函数:
- 初始化列表
- 超类的无名无参构造函数
- 主类的无参构造函数
若超类没有未命名且无参的构造函数,则需手动调用超类的某个构造函数。应在构造函数体(若有)之前,在冒号(:
)后指定要调用的超类构造函数。
class Person {
final String name;
final int age;
// 从 JSON 数据构造的命名构造函数
Person.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? 'Unknown',
age = json['age'] ?? 0;
}
class Employee extends Person {
Employee() : super.fromJson(fetchDefaultData());
// ...
}
为避免将每个参数显式传递至构造函数的 super
调用,可使用超类初始化参数将参数直接转发给指定或默认的超类构造函数。此特性不可与重定向构造函数同时使用。超类初始化参数的语法和语义类似于初始化形式参数。
若超类构造函数调用包含位置参数,则超类初始化参数不得采用位置参数形式。
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);
}
6.1 方法重写
子类可以重写实例方法(包括操作符)、getter
和 setter
,如上述 Point3D
中的 distanceTo()
和 toString()
方法,可以使用 @override
来注解方法重写。
如果希望重写==
操作符,则也应同时重写Object
类的hashCode
获取器。
6.2 abstract、interface 和 abstract interface
修饰符 | 声明方式 | 能否实例化 | 能否被继承(extends ) | 能否被实现(implements ) | 典型用途 | 版本要求 |
---|---|---|---|---|---|---|
abstract | abstract class A { ... } | ❌ | ✅ | ✅ | 作为基类,提供部分实现 | 所有 Dart 版本 |
interface | interface class B { ... } | ✅ | ❌ | ✅ | 作为纯接口,禁止继承 | Dart 3.0+ |
abstract interface | abstract interface class C { ... } | ❌ | ❌ | ✅ | 作为纯接口,禁止继承,且不能实例化 | Dart 3.0+ |
在 Dart 3.0
之前,Dart
中没有专门的 interface
关键字,任何类(无论是普通类还是抽象类)都隐式定义了一个接口,该接口包含类及其实现的所有接口的所有实例成员。
// 一个 Person 类,其隐式接口包含 greet() 方法。
class Person {
// 接口成员,但仅在此库内可见。
final String _name;
// 构造函数不包含在接口中。
Person(this._name);
// 在接口中
String greet(String who) => 'Hello, $who. I am $_name.';
}
// 实现 Person 接口
class Impostor implements Person {
String greet(String who) => 'Hi $who. Do you know who I am?';
}
虽然可以这么做,但并不推荐,根据上述表格可知,现在可以使用 abstract class
来定义抽象类,用 abstract interface class
来定义接口,相对而言,interface class
的定位就比较尴尬,因此并不常用。
8. 混入(Mixin)
混入 是一种可在多个类层次结构中复用代码的机制,其核心功能是批量提供成员实现。
尽管每个类(除顶级类 Object?
之外)都有且只有一个超类,但依然可以将一段完整的代码实现(一个混入),“混合”到多个毫无继承关系的类中,让这些不同的类都能获得这段代码提供的功能。
8.1 使用与定义混入
要使用 混入,需要使用 with
关键字后接一个或多个混入名称。以下示例展示两个使用混入(即作为混入子类)的类:
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
子句,且禁止声明任何生成式构造函数。例如:
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');
}
}
}
8.2 在混入中定义抽象成员
可在混入中声明抽象方法,使用该混入的任何类型必须实现该抽象方法。例如:
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
来访问混入子类中的状态:
mixin NameIdentity {
// 声明抽象 getter - 不提供实现
String get name;
@override
int get hashCode => name.hashCode;
// 可访问到最终子类的 name
@override
bool operator ==(other) => other is NameIdentity && name == other.name;
}
class Person with NameIdentity {
final String _name;
Person(this._name);
// 实现getter方法
@override
String get name => _name;
}
8.3 实现接口
当混入依赖某接口的成员(方法 / 属性)却不想自己实现时,可通过 implements
子句声明 “依赖该接口”,但不提供接口成员的具体实现 —— 这样一来,任何使用该混入的类,都必须同时实现这个接口(即定义接口的所有成员),从而间接满足混入对成员的依赖需求。这与 “在混入中声明抽象方法” 的效果类似,都是通过语法约束强制依赖成员被定义,区别在于前者借助已有的接口规范,后者需重新定义抽象方法。
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.");
}
}
8.4 on 子句
on
子句是混入的 “安全保障”,有两个作用:
- 当混入里有
super
调用时,用on
明确这个super
指向哪个类; - 强制所有使用该混入的类必须是这个类的子类,确保
super
调用的成员一定存在。
如果混入里没有 super
调用,就不需要用 on
子句了,用了反而会多此一举地增加约束。
class Musician {
musicianMethod() {
print('Playing music!');
}
}
mixin MusicalPerformer on Musician {
performerMethod() {
print('Performing music!');
super.musicianMethod();
}
}
class SingerDancer extends Musician with MusicalPerformer { }
在此示例中,由于 SingerDancer
继承自 Musician
,因此可以混入 MusicalPerformer
。
相关推荐
- Dart 语法要点(2) —— 函数 2025-09-15
- Dart 语法要点(1) —— 注释、变量、常量、数据类型 2025-09-14
- Dart 开发环境搭建 2025-09-12
评论0
暂时没有评论