Flutter 基础体系 · 第 2/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。
Dart 3 语言基础:类型、空安全、模式、类、Mixin 与扩展
Dart 是一门支持面向对象、泛型、类型推断、异步编程和模式匹配的静态类型语言。Flutter 使用 Dart 作为 UI、状态管理、业务逻辑和平台交互层的主要语言,因此理解 Dart 的类型系统和对象模型,比记忆若干 Flutter Widget API 更基础。
本文使用 Dart 3 语言能力说明以下内容:
- Dart 的静态类型、类型推断和类型层次;
- sound null safety(健全空安全);
Object、dynamic、Never、void、泛型和函数类型;- Records 与模式匹配;
switch表达式、穷尽性检查和代数数据类型风格;- 类、构造函数、继承、接口和类修饰符;
mixin、mixin class与with的组合规则;- 扩展方法和扩展类型的边界;
- 在 Flutter、Android、iOS、桌面和 Web 中的语言层差异。
示例默认可以放入 bin/main.dart,通过以下命令执行:
dart format bin/main.dart
dart analyze
dart run
在 Flutter 项目中,对应命令通常是:
flutter analyze
flutter test
flutter run
这些命令分别负责格式化、静态分析、测试和运行。dart analyze 或 flutter analyze 能发现大量类型错误,但不会替代运行时测试,例如错误的 as 转换和未初始化的 late 字段仍可能在运行时失败。
一、先建立 Dart 的类型模型
1. 静态类型与运行时类型
静态类型是编译器在源码分析阶段看到的类型;运行时类型是对象实际创建出来的类型。
num value = 1;
print(value.runtimeType); // int
变量 value 的静态类型是 num,但表达式 1 创建的是 int 对象,因此运行时类型是 int。
静态类型决定代码在编译时允许做什么:
num value = 1;
// value.isEven; // 编译错误:num 没有 isEven 属性
print(value.toInt()); // 合法
虽然运行时对象是 int,但编译器只能根据变量声明的静态类型 num 检查成员。若要使用 int 特有的成员,需要缩小类型:
num value = 1;
if (value is int) {
print(value.isEven); // value 在此分支内被提升为 int
}
这里发生了两个步骤:
value is int检查运行时对象是否为int;- Dart 的 flow analysis 根据该条件,在分支内部把
value推广(promotion)为int。
类型判断和强制转换的区别也必须明确:
Object value = 'dart';
if (value is String) {
print(value.length); // 安全,已经验证过类型
}
print((value as String).length); // 强制转换,失败时抛出 TypeError
is 是条件检查;as 是断言式转换。as 不会改变对象本身,只改变当前表达式被允许使用的静态类型。如果对象实际不是目标类型,程序会在运行时失败。
2. 子类型关系
如果类型 S 的值可以在所有需要 T 的位置使用,则称 S 是 T 的子类型,记作:
例如:
int integer = 1;
num number = integer;
Object object = integer;
可以推导出:
原因是:
- 所有
int都是num; - 所有
num都是Object; - 因此
int也可以赋值给Object。
反方向不成立:
num number = 1;
// int integer = number; // 编译错误
因为任意 num 可能是 double,不能直接当作 int。必须进行运行时检查:
num number = 1;
if (number is int) {
int integer = number;
print(integer);
}
Dart 中几种重要的类型位置如下:
| 类型 | 含义 |
|---|---|
Object |
所有非空对象的公共超类型 |
Object? |
所有对象,包括 null |
dynamic |
关闭大部分静态检查,成员访问延迟到运行时 |
Never |
不会正常返回的表达式的类型 |
void |
函数调用者不应使用返回值 |
Null |
null 的类型 |
Object 不接受 null:
Object a = 'text';
// Object b = null; // 编译错误
Object? b = null; // 合法
Object? 可以理解为可空类型世界的顶部类型。
Never 是底部类型:
Never fail(String message) {
throw StateError(message);
}
因为 fail 不会返回一个值,所以它可以出现在任何需要某种类型的表达式位置:
String loadName(bool valid) {
if (!valid) {
return fail('invalid user');
}
return 'Alice';
}
这里 fail(...) 的类型是 Never,而 Never 可以赋值到 String、int 或其他类型的位置。它表达的是控制流事实:执行到这个表达式后,后续路径不会继续。
3. dynamic 与 Object 不同
dynamic 不是“任意对象的安全父类型”,而是告诉分析器暂时不要检查成员访问。
dynamic value = 'dart';
print(value.length); // 编译通过,运行时成功
print(value.missing); // 编译通过,运行时 NoSuchMethodError
如果使用 Object:
Object value = 'dart';
// print(value.length); // 编译错误
编译器会迫使你先确认类型:
if (value is String) {
print(value.length);
}
因此:
Object保留静态类型安全;dynamic把错误推迟到运行时;- 外部 JSON、反射式数据或旧代码边界可能需要
dynamic,但进入业务层后应尽快转换为明确类型。
例如,不要让 dynamic 从 JSON 层一直传播到 Widget:
dynamic raw = {'id': 1, 'name': 'Alice'};
更可靠的做法是边界处完成校验:
class User {
final int id;
final String name;
const User({
required this.id,
required this.name,
});
factory User.fromJson(Map<String, Object?> json) {
final id = json['id'];
final name = json['name'];
if (id is! int || name is! String) {
throw FormatException('Invalid user JSON');
}
return User(id: id, name: name);
}
}
这里使用 Map<String, Object?> 而不是 Map<String, dynamic>,使调用方必须检查每个字段的实际类型。
二、类型推断、泛型与函数类型
1. var、final 和 const
var 表示让编译器推断变量类型,不表示变量没有类型:
var count = 1;
// count = 'one'; // 编译错误,count 的推断类型是 int
final 表示变量只能赋值一次:
final now = DateTime.now();
// now = DateTime.now(); // 编译错误
const 表示编译时常量:
const timeout = Duration(seconds: 5);
const numbers = [1, 2, 3];
final 的值可以在运行时确定,const 的值必须在编译时确定:
final currentTime = DateTime.now(); // 合法
// const currentTime = DateTime.now(); // 编译错误
final 只限制变量绑定,不能使对象内部自动不可变:
final list = <int>[1, 2];
list.add(3); // 合法,list 仍指向同一个可变 List
要表达编译时不可变集合,应使用 const:
const list = <int>[1, 2];
// list.add(3); // 运行时错误,const List 不可变
2. 泛型
泛型把类型参数化,避免重复代码和不安全的强制转换:
T first<T>(List<T> values) {
if (values.isEmpty) {
throw StateError('empty list');
}
return values.first;
}
final number = first<int>([1, 2, 3]);
final text = first<String>(['a', 'b']);
调用时的 <int> 通常可以省略:
final number = first([1, 2, 3]); // 推断 T 为 int
泛型约束用于表达“类型必须具备某种能力”:
T maxValue<T extends Comparable<T>>(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
T extends Comparable<T> 表示 T 至少实现了可以和另一个 T 比较的接口。这样 compareTo 调用在静态类型层面是成立的。
集合类型也具有参数:
List<int> numbers = [1, 2, 3];
Map<String, Object?> json = {
'id': 1,
'enabled': true,
};
Set<String> tags = {'dart', 'flutter'};
不要把原始类型和参数化类型混在一起:
List<dynamic> unsafe = [1, 'text'];
List<Object> safer = [1, 'text'];
两者都能存放不同类型,但 List<Object> 仍然要求读取者遵守静态类型规则;List<dynamic> 会让读取结果变成 dynamic,更容易传播错误。
3. 函数也是对象
Dart 中函数可以赋值、传递和返回:
int add(int a, int b) => a + b;
int calculate(int a, int b, int Function(int, int) operation) {
return operation(a, b);
}
final result = calculate(2, 3, add);
print(result); // 5
int Function(int, int) 的含义是:
- 参数有两个;
- 两个参数都是
int; - 返回值是
int。
函数参数可以是位置参数、命名参数或可选参数:
String formatUser(
String name, {
required int age,
String? city,
}) {
return '$name/$age/${city ?? 'unknown'}';
}
print(formatUser('Alice', age: 20));
required 是静态约束,调用者缺少该参数时直接编译失败。String? city 表示调用者可以省略它,函数内部必须处理 null。
4. 泛型的协变与函数参数方向
Dart 的常规泛型类型通常是协变的:
List<int> ints = [1, 2, 3];
List<num> nums = ints; // 类型上允许
这意味着如果:
则通常有:
但 List 是可变集合,这种设计可能产生写入问题。Dart 通过运行时检查保护类型安全:
List<num> nums = <int>[1, 2, 3];
// nums.add(1.5); // 运行时可能抛出类型错误
工程上,如果集合只用于读取,可以使用 Iterable<T> 或 List<T> 的只读抽象;如果 API 需要写入,必须明确接受何种元素类型。
函数类型的参数方向则更严格。一个能接受任意 Object 的函数,可以当作只需要接受 String 的函数使用:
void acceptObject(Object value) {}
void useStringHandler(void Function(String) handler) {
handler('dart');
}
useStringHandler(acceptObject); // 合法
反过来不安全:
void acceptOnlyString(String value) {}
// 不能把它当成 void Function(Object)
因为调用方可能传入一个 int,而函数只能处理 String。这就是函数参数位置的逆变直觉:消费者越通用,越能替代只消费特定类型的函数。
三、健全空安全
1. 可空类型必须显式标记
Dart 的空安全把 null 从普通类型中分离出来:
String name = 'Alice';
String? nickname = null;
String 不允许为 null,String? 允许为 null。因此以下代码无法通过静态分析:
String? name = 'Alice';
// print(name.length); // 编译错误
原因是 name 的可能值集合包含:
而 length 只对 String 定义,不对 null 定义。
可以使用条件判断缩小范围:
String? name = 'Alice';
if (name != null) {
print(name.length);
}
在 if 分支中,分析器知道这条路径上的 name 不是 null,因此将其提升为 String。
2. 空安全运算符的语义
?.:空值传播
String? name;
print(name?.length); // null
如果左侧为 null,整个访问表达式返回 null,否则访问成员。
??:空值回退
String? name;
final displayName = name ?? 'Guest';
如果 name 非空,结果是 name;否则结果是 'Guest'。
??=:仅在为空时赋值
String? cache;
cache ??= 'loaded';
print(cache); // loaded
!:非空断言
String? name = 'Alice';
print(name!.length);
! 的含义是:程序员断言此处一定非空。若断言错误,会在运行时失败:
String? name = null;
// print(name!.length); // 运行时错误
! 不是空安全检查,而是取消静态检查。它适合用于已经由外部不变量保证非空的边界,但不应代替真实的控制流判断。
3. 类型提升的限制
局部变量经过检查后通常可以被提升:
void printLength(String? value) {
if (value == null) return;
print(value.length);
}
但可变字段通常不会被持续提升:
class Profile {
String? name;
void printLength() {
if (name != null) {
// print(name.length);
// 可能无法通过分析:getter 或其他代码可能改变 name
}
}
}
字段可能通过 setter、子类覆盖、getter 或其他控制流发生变化。更可靠的写法是先保存到局部变量:
class Profile {
String? name;
void printLength() {
final currentName = name;
if (currentName != null) {
print(currentName.length);
}
}
}
final 局部变量没有后续重新赋值,分析器更容易证明它的值没有改变。
闭包也会影响提升,因为闭包可能稍后执行:
void example(String? value) {
if (value == null) return;
void callback() {
print(value.length);
}
callback();
}
这种简单场景通常可以分析成功,但如果变量可变、跨越异步边界或捕获关系复杂,就不应依赖“我知道它不会变”这种主观判断,而应保存稳定的非空局部值。
4. late 是延迟初始化,不是可空
class Controller {
late String token;
void use() {
print(token);
}
void initialize() {
token = 'secret';
}
}
late String token 的意思是:声明时暂不赋值,但第一次读取前必须完成赋值。
final controller = Controller();
// controller.use(); // LateInitializationError
late 解决的是初始化顺序问题,不是空值问题。若一个字段确实可能没有值,应写成:
String? token;
late final 只能赋值一次:
class Config {
late final String endpoint;
void initialize(String value) {
endpoint = value;
}
}
第二次赋值会在运行时失败。因此 late 引入了一个运行时状态:
未初始化 -> 已初始化
读取未初始化字段或重复初始化,都会导致错误。Flutter 中常见的 late 使用场景包括 State 内部依赖 initState 初始化的字段,但生命周期路径必须确保初始化先于读取。
5. 可空字段、构造函数与不变量
非空字段必须在构造函数完成前初始化:
class User {
final String name;
final int age;
const User({
required this.name,
required this.age,
});
}
如果允许默认值:
class Settings {
final bool darkMode;
const Settings({
this.darkMode = false,
});
}
如果字段必须由构造参数提供,使用 required 比使用 late 更安全,因为错误会在调用点被静态发现,而不是延迟到运行时。
四、Records:轻量的结构化值
Record 是 Dart 3 中的匿名、不可变、结构化值类型。它可以包含位置字段、命名字段,或者两者同时存在。
(int, String) userInfo() {
return (1, 'Alice');
}
final result = userInfo();
print(result.$1); // 1
print(result.$2); // Alice
命名字段使用名称访问:
({int id, String name}) getUser() {
return (id: 1, name: 'Alice');
}
final user = getUser();
print(user.id);
print(user.name);
Record 的结构类型由字段数量、字段类型和命名字段名称共同决定:
(int, String) a = (1, 'dart');
// (String, int) b = a; // 类型不兼容
Record 适合返回多个相关结果,避免为了临时返回值创建只使用一次的类:
({int success, int failed}) countResults(List<bool> results) {
var success = 0;
var failed = 0;
for (final result in results) {
if (result) {
success++;
} else {
failed++;
}
}
return (success: success, failed: failed);
}
但 Record 不是领域模型的替代品。如果一个值需要行为、文档化语义、序列化规则或稳定的公共 API,命名类通常更清晰。
五、模式匹配
模式(pattern)是一种同时完成“结构检查”和“值绑定”的语法。它可以匹配常量、类型、Record、列表、Map 和对象。
模式的核心过程可以拆成三步:
- 取得待匹配值;
- 检查值是否符合模式;
- 将内部数据绑定到局部变量。
1. Record 模式
final (id, name) = (1, 'Alice');
print(id); // 1
print(name); // Alice
这里左侧的 (id, name) 是 Record 模式,不是普通的 Record 表达式。
命名字段也可以解构:
final (:id, :name) = (id: 1, name: 'Alice');
print(id);
print(name);
在大型代码中可以显式写出变量名,避免阅读者混淆:
final (userId: id, userName: name) = (
userId: 1,
userName: 'Alice',
);
2. 类型模式与 if-case
Object value = 'dart';
if (value case String text) {
print(text.length);
}
if (value case String text) 同时完成:
- 判断
value是否为String; - 如果是,将它绑定为
text; - 只在匹配成功的分支中执行代码。
这比先写 is 再手动转换更接近数据处理意图。
模式可以匹配 Map:
Object json = {
'id': 1,
'name': 'Alice',
};
if (json case {
'id': int id,
'name': String name,
}) {
print('$id: $name');
}
只有当对象是适合匹配的 Map,且两个键存在、值类型正确时,模式才成功。
可选字段可以用逻辑分支处理:
if (json case {
'id': int id,
'name': String name,
'admin': bool admin,
}) {
print('$id $name $admin');
} else {
print('invalid payload');
}
模式本身不会替你提供缺失字段的默认值。默认值应在匹配后使用 ??,或在数据解析层显式处理。
3. 列表模式与剩余元素
final values = [1, 2, 3, 4];
if (values case [final first, final second, ...final rest]) {
print(first); // 1
print(second); // 2
print(rest); // [3, 4]
}
...final rest 捕获剩余元素。若列表长度不足两个元素,模式匹配失败。
列表模式可以表达固定结构:
if (values case [1, 2, 3, 4]) {
print('exact match');
}
这与 values == [1, 2, 3, 4] 不同。普通 List 的 == 默认不是按元素比较,而列表模式检查的是结构和内容。
4. 逻辑模式和守卫条件
模式支持逻辑组合:
Object value = 10;
if (value case int number when number > 0) {
print('positive: $number');
}
when 后面是守卫条件(guard)。流程是:
- 先判断
value是否为int; - 成功后绑定
number; - 再判断
number > 0; - 两者都成功才进入分支。
逻辑或模式要求各分支绑定的变量结构一致,否则后续代码无法确定变量是否存在:
String describe(Object value) {
return switch (value) {
int number when number > 0 => 'positive int',
int _ => 'non-positive int',
String text => 'string: $text',
_ => 'other',
};
}
通配模式 _ 表示忽略值:
final (id, _) = (1, 'unused');
六、switch 表达式与穷尽性检查
Dart 3 的 switch 可以是语句,也可以是表达式。
1. switch 语句
void printType(Object value) {
switch (value) {
case int number:
print('int: $number');
case String text:
print('string: $text');
default:
print('other');
}
}
Dart 的 switch 分支不需要像 C/Java 那样写 break 来防止自动贯穿。每个分支结束后不会隐式执行下一个分支。
2. switch 表达式
String label(Object value) {
return switch (value) {
int number => 'number $number',
String text => 'text $text',
_ => 'unknown',
};
}
表达式形式要求每条可能路径都产生一个值。
3. 常量模式与关系模式
String httpStatus(int code) {
return switch (code) {
200 => 'ok',
>= 400 && < 500 => 'client error',
>= 500 => 'server error',
_ => 'other',
};
}
这里:
200是常量模式;>= 400 && < 500是关系模式和逻辑与;_覆盖剩余输入。
如果缺少 _,且编译器无法证明所有整数都被覆盖,分析器会报告 switch 不穷尽。
七、用 sealed class 表达受限状态集合
sealed class 适合表达有限的状态或事件集合。直接子类必须在同一个 library 中声明,因此分析器能够检查模式是否覆盖所有情况。
sealed class LoadState {}
final class Loading extends LoadState {
const Loading();
}
final class Loaded extends LoadState {
final List<String> items;
const Loaded(this.items);
}
final class LoadError extends LoadState {
final Object error;
const LoadError(this.error);
}
现在可以写出穷尽的状态转换:
String stateLabel(LoadState state) {
return switch (state) {
Loading() => 'loading',
Loaded(items: final items) => 'loaded ${items.length}',
LoadError(error: final error) => 'error: $error',
};
}
如果后来增加:
final class Empty extends LoadState {
const Empty();
}
那么没有处理 Empty() 的 switch 会被静态分析发现。这个性质很适合 Flutter 的 UI 状态:
Loading -> Loaded
Loading -> LoadError
Loaded -> Loading
LoadError -> Loading
与使用字符串状态相比,sealed class 将状态集合和状态携带的数据放在类型系统中。错误不会等到某个 Widget 运行时才暴露。
需要区分两种“穷尽”:
sealed class提供类型层面的有限子类集合;_或default明确表示“其他情况”。
如果总是使用 _,未来新增子类时,编译器可能无法提醒你新增 UI 分支。因此对关键业务状态,显式列出每个子类通常更有价值。
八、类:字段、方法、构造函数和不变量
1. 类的基本组成
class Counter {
int _value = 0;
int get value => _value;
void increment() {
_value++;
}
}
这里:
_value是库级私有字段;value是 getter;increment是实例方法;- 对象的可见状态只能通过类提供的行为改变。
Dart 的下划线私有性是library 级别,不是 Java 或 C++ 意义上的 class 级别。同一个 Dart library 内的其他类也可以访问该私有成员。拆分到不同文件后,是否仍属于同一 library 取决于 library 组织方式。
2. 初始化列表
class Rectangle {
final double width;
final double height;
final double area;
Rectangle(double width, double height)
: width = width,
height = height,
area = width * height;
}
初始化列表在构造函数体执行前运行,适合初始化 final 字段和进行前置检查:
class PositiveNumber {
final int value;
PositiveNumber(this.value) : assert(value > 0);
}
assert 在开发和检查模式下用于验证条件;生产构建中通常不会依赖它完成业务校验。如果输入来自网络、用户或持久化数据,应显式抛出异常,而不是只使用 assert:
class Port {
final int value;
Port(this.value) {
if (value < 1 || value > 65535) {
throw ArgumentError.value(value, 'value');
}
}
}
3. 命名构造函数
class User {
final String name;
final int age;
const User({
required this.name,
required this.age,
});
User.guest()
: name = 'Guest',
age = 0;
}
调用:
const user = User(name: 'Alice', age: 20);
final guest = User.guest();
Dart 没有与类同名的多个构造函数重载;通常使用命名构造函数区分不同创建路径。
4. 工厂构造函数
工厂构造函数不一定返回当前构造过程新创建的对象:
class Logger {
static final Logger _instance = Logger._();
Logger._();
factory Logger() => _instance;
void log(String message) {
print(message);
}
}
Logger() 每次都返回同一个对象。工厂构造函数也常用于根据输入选择不同子类:
abstract class Shape {
const Shape();
factory Shape.circle(double radius) => Circle(radius);
}
class Circle extends Shape {
final double radius;
const Circle(this.radius);
}
工厂构造函数不能直接访问未初始化的实例字段,因为它可能返回已有对象或其他类型实例。
九、继承、实现与类修饰符
1. extends、implements 和 with
class Animal {
void move() {
print('move');
}
}
class Dog extends Animal {
@override
void move() {
print('run');
}
}
extends 建立实现继承关系。子类得到父类的可继承成员,并可以通过 @override 覆盖行为。
implements 只表示接口兼容,不继承实现:
abstract interface class Printable {
String printText();
}
class Report implements Printable {
@override
String printText() => 'report';
}
Report 必须实现 Printable 的所有接口成员。Dart 中每个类都隐式定义了一个接口,因此可以实现具体类的接口,但不会自动获得其方法实现。
2. abstract
abstract class Repository {
Future<String> load();
}
抽象类不能直接实例化,但可以包含字段、已实现方法和构造函数:
abstract class BaseRepository {
void log(String message) {
print(message);
}
Future<String> load();
}
与 abstract interface class 相比,普通抽象类还可以被继承以复用实现。
3. Dart 3 类修饰符
Dart 3 提供了用于限制 API 继承方式的修饰符:
base class BaseService {
void close() {}
}
interface class ServiceContract {
void start();
}
final class ConcreteService {}
sealed class Result {}
它们表达不同的设计约束:
base class:要求子类继承其实现,而不能只把它当作接口实现;相关直接子类型也必须遵守base、final或sealed等限制;interface class:允许其他代码实现其接口,但不允许从库外继承其实现;final class:不允许被继承或实现;sealed class:限制直接子类范围,并支持穷尽性分析,不能直接实例化。
这些修饰符不是性能优化开关,而是 API 继承契约。库作者可以用它们防止外部代码创建自己不承诺支持的子类。
类修饰符对跨库边界尤其重要。某些约束只在声明类的 library 内可自由放宽,库外代码必须遵循修饰符规定。具体组合应以当前 Dart analyzer 的诊断为准,不要依赖“看起来像普通继承”的经验。
十、Mixin:复用实现而不建立主继承关系
Mixin 用于把一组方法和字段组合到类中:
mixin Logger {
void log(String message) {
print('[LOG] $message');
}
}
class UserController with Logger {
void load() {
log('load users');
}
}
UserController 的父类仍然是 Object,而 Logger 的实现被混入到 UserController 中。Mixin 解决的是横向复用,不是“一个类是另一个类的特殊类型”这一建模问题。
1. Mixin 的 super 调用
class Base {
String name() => 'base';
}
mixin First on Base {
@override
String name() => '${super.name()}-first';
}
mixin Second on Base {
@override
String name() => '${super.name()}-second';
}
class Combined extends Base with First, Second {}
void main() {
print(Combined().name()); // base-first-second
}
组合顺序从左到右形成链,最右侧的 Second 最先接收调用:
Combined.name
-> Second.name
-> First.name
-> Base.name
因此 with M1, M2 中,后面的 Mixin 可以覆盖前面的同名成员,并通过 super 调用前一个实现。
2. on 约束
class StateObject {
bool mounted = true;
}
mixin SafeOperation on StateObject {
void checkMounted() {
if (!mounted) {
throw StateError('not mounted');
}
}
}
on StateObject 表示这个 Mixin 只能混入 StateObject 或其子类。这样 Mixin 可以安全访问 mounted:
class Controller extends StateObject with SafeOperation {}
下面的类型不满足约束:
class Other {}
// class Invalid extends Other with SafeOperation {}
on 不是用来声明 Mixin 对外暴露的接口,而是用来限制它可以依赖的宿主类型。
3. mixin 与 mixin class
普通 mixin 主要作为组合单元:
mixin Timestamped {
DateTime get createdAt => DateTime.now();
}
mixin class 同时可以被实例化,也可以被 with 使用:
mixin class Identifiable {
final String id;
Identifiable(this.id);
}
class User with Identifiable {
User(super.id);
}
如果需要构造函数或希望类型既是普通类又是 Mixin,才考虑 mixin class。普通 Mixin 不应被当作拥有完整对象生命周期的独立实体。
4. Mixin 的适用边界
Mixin 适合:
- 日志、缓存、权限检查等横向能力;
- 多个类共享的一组实现;
- 与宿主类存在明确约束的行为。
Mixin 不适合:
- 需要复杂初始化顺序的独立服务;
- 隐式依赖大量宿主字段;
- 多个 Mixin 之间存在难以追踪的覆盖关系。
当多个 Mixin 都覆盖同一个方法时,最终行为依赖组合顺序。改变 with M1, M2 的顺序可能改变业务结果,因此应把 super 调用链当作显式设计的一部分。
十一、扩展:为已有类型增加 API
扩展(extension)可以向已有类型增加方法、getter、setter、操作符和静态成员,而不修改原类,也不创建子类。
extension StringParsing on String {
int? toIntOrNull() => int.tryParse(this);
}
void main() {
print('42'.toIntOrNull()); // 42
print('abc'.toIntOrNull()); // null
}
扩展方法通过静态类型解析:
Object value = '42';
// value.toIntOrNull(); // 编译错误
虽然运行时对象是 String,但变量静态类型是 Object,编译器不会根据运行时类型动态查找扩展。
String value = '42';
print(value.toIntOrNull()); // 合法
扩展方法不是动态派发。若多个扩展提供同名成员,解析可能产生冲突:
extension A on String {
String describe() => 'A';
}
extension B on String {
String describe() => 'B';
}
// 'text'.describe(); // 可能产生扩展冲突
可以使用显式扩展调用:
print(A('text').describe());
print(B('text').describe());
扩展不能添加实例字段,因为对象的内存布局和构造过程属于原类。下面这种需求不能通过扩展完成:
// extension Bad on String {
// int count; // 不允许添加实例字段
// }
如果需要保存状态,应使用包装类:
class UserName {
final String value;
final int normalizedLength;
UserName(this.value) : normalizedLength = value.trim().length;
}
扩展方法与继承的差别
| 能力 | 扩展 | 继承 |
|---|---|---|
| 添加实例字段 | 不可以 | 可以 |
| 覆盖已有实例方法 | 不是真正覆盖 | 可以 |
| 根据运行时子类动态分派 | 不可以 | 可以 |
| 修改原类源码 | 不需要 | 需要建立子类关系 |
| 对已有第三方类型增加便捷 API | 适合 | 通常不适合 |
扩展适合语法便利性,例如日期格式化、集合转换、输入校验;不适合表达必须参与多态分派的领域行为。
扩展类型的区分
较新的 Dart 3 版本还提供扩展类型(extension type)能力。扩展方法是在已有类型上增加静态解析的成员;扩展类型则可以为某个表示值定义新的静态类型接口和约束。两者不是同一概念。
如果项目需要使用扩展类型,应确认当前 Flutter SDK 对应的 Dart SDK 版本和语言实验状态,并让 pubspec.yaml 的 SDK 约束覆盖目标环境。不能因为代码编辑器接受某个语法,就假设所有 CI、开发机和发布构建环境都支持它。
十二、把模式、类和空安全组合起来
下面用一个可运行的示例表示加载状态:
sealed class LoadState {}
final class Loading extends LoadState {
const Loading();
}
final class Loaded extends LoadState {
final List<String> items;
const Loaded(this.items);
}
final class Failed extends LoadState {
final Object error;
const Failed(this.error);
}
String buildMessage(LoadState state) {
return switch (state) {
Loading() => 'Loading...',
Loaded(items: final items) when items.isEmpty => 'No data',
Loaded(items: final items) => 'Items: ${items.join(', ')}',
Failed(error: final error) => 'Failed: $error',
};
}
void main() {
const states = <LoadState>[
Loading(),
Loaded(['Dart', 'Flutter']),
Loaded([]),
Failed('network unavailable'),
];
for (final state in states) {
print(buildMessage(state));
}
}
预期输出:
Loading...
Items: Dart, Flutter
No data
Failed: network unavailable
关键路径如下:
LoadState是 sealed 根类型;Loading、Loaded、Failed是有限状态;switch根据对象模式识别具体子类;Loaded分支进一步读取items;when items.isEmpty优先匹配空列表;- 后一个
Loaded分支处理非空列表; Failed分支读取错误对象;- 所有状态都被覆盖,因此无需
default。
如果把两个 Loaded 分支顺序调换:
Loaded(items: final items) => 'Items...',
Loaded(items: final items) when items.isEmpty => 'No data',
空列表会先被一般分支匹配,后面的特殊分支将失去意义。模式匹配同样遵循从上到下的分支顺序,因此更一般的模式不应放在更具体模式之前。
十三、Dart 与 Flutter 的语言边界
Dart 语言本身在 Android、iOS、Windows、macOS、Linux 和 Web 上保持相同的类型、空安全、类、Mixin 和模式语义。差异主要来自运行时和可用库:
- Android、iOS 和桌面通常可以使用
dart:io,但仍受目标平台能力限制; - Web 不支持
dart:io的文件系统、Socket 等能力; - Web 中应使用浏览器相关库或 Flutter Web 支持的网络抽象;
- Flutter UI 代码通常通过 Dart 编译为移动端、桌面端的原生代码,Web 则编译为 JavaScript 或 WebAssembly 相关产物,具体后端由当前 Flutter 工具链决定;
- Isolate 在 Web 上的并发能力和实现方式与原生平台不同,不能直接假设与 Android/iOS 相同;
- 平台插件可能只实现部分平台,类型系统不会自动保证插件在所有平台都有原生实现。
例如下面的代码在非 Web 环境可能可用,但不能无条件放进跨平台共享层:
import 'dart:io';
String readPlatform() => Platform.operatingSystem;
跨平台项目通常把平台相关实现隔离在条件导入、插件或平台适配层中,而不是让业务模型直接依赖 dart:io。
Dart 的 Future、Stream、事件循环和 Isolate 属于异步与并发模型;它们仍然使用本文讲述的类型系统:
Future<String> loadName() async {
return 'Alice';
}
Future<String> 表示未来完成并产生 String;它不是 String 本身。必须通过 await 或 then 获取结果:
Future<void> main() async {
final name = await loadName();
print(name);
}
Flutter 的 State 生命周期也不会改变 Dart 的空安全规则。initState 中初始化的字段、异步回调中的对象引用和 dispose 后的状态访问,都仍然需要通过类型和控制流正确表达。mounted 检查能够表达 Flutter 生命周期状态,但不会自动修复所有异步竞态。
十四、常见失败表现与诊断方法
1. 把可空值当成非空值
String? name = getName();
// print(name.length); // analyzer 报错
诊断方法是先确认业务语义:
- 如果缺失值合法,使用
??、条件分支或返回可空结果; - 如果缺失值代表输入错误,在边界处抛出异常;
- 只有有可靠不变量时才使用
!。
2. 用 dynamic 消除所有错误
dynamic response = fetchResponse();
final value = response['user']['name'];
这段代码可能在任意一步失败:
response不是 Map;- 没有
user键; user为null;name不是字符串。
应尽早把外部数据转换为明确模型,并让失败发生在解析边界,而不是 Widget 构建过程中。
3. 把扩展当作多态
class BaseValue {}
class SpecialValue extends BaseValue {}
extension BaseExtension on BaseValue {
String describe() => 'base';
}
extension SpecialExtension on SpecialValue {
String describe() => 'special';
}
String printValue(BaseValue value) {
return value.describe();
}
这里不会根据运行时对象自动选择 SpecialExtension。调用解析依据 value 的静态类型 BaseValue。需要运行时多态时,应把行为放入类接口或使用真正的继承覆盖。
4. switch 分支不可达或不穷尽
常见原因包括:
- 先写了
_,导致后面分支永远无法匹配; - 一般类型模式放在具体类型模式前面;
- 新增 sealed 子类后没有补充分支;
when条件只覆盖部分输入,却误以为覆盖了整个类型。
可以通过 dart analyze 检查静态诊断,再用针对边界状态的单元测试验证实际行为。
5. late 在生命周期中被提前读取
class Example {
late final String value;
String read() => value;
}
如果调用顺序不是:
创建对象 -> 初始化 value -> 读取 value
就可能抛出 LateInitializationError。对于构造时必须存在的值,应优先放入构造函数;对于确实可能缺失的值,应使用可空类型。
十五、选择合适的语言机制
可以用以下因果关系做设计判断:
- 需要表达“可能没有值”:使用
T?; - 需要表达“永远不会返回”:使用
Never; - 需要暂时接收未知外部值:使用
Object?,在边界校验;只有兼容动态 API 时才使用dynamic; - 需要返回少量临时结构:使用 Record;
- 需要对 Record、Map、List 或对象进行结构检查和拆解:使用模式;
- 需要表达有限状态集合:使用
sealed class加穷尽switch; - 需要共享横向实现:使用 Mixin;
- 需要为已有类型增加便捷行为:使用扩展;
- 需要运行时多态和可替换行为:使用接口、继承或组合,而不是扩展;
- 需要对象不变量在创建时成立:使用构造函数和初始化列表;
- 需要延迟初始化:使用
late,同时接受其运行时失败路径; - 需要限制外部继承方式:使用
base、interface、final或sealed。
Dart 3 的这些能力并不是互相独立的语法集合。类型系统决定值能否流动,空安全决定哪些路径必须处理 null,模式把类型检查和数据解构结合起来,类与 Mixin 决定实现如何组织,扩展则提供不改变原类型的静态 API。把这些机制组合起来,才能在 Flutter 项目中构建可分析、可维护且跨平台行为明确的代码。
系列导航与关联阅读
- 系列入口:Flutter 完整学习路线:从 Dart 与 Widget 到多端架构和应用发布
- 下一篇:Dart 异步与并发:Future、Stream、Event Loop、Isolate 和取消
- 延伸:Flutter 项目工具链:SDK、Pub、Flavor、代码生成和环境配置
官方资料
本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论