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(健全空安全);
  • ObjectdynamicNevervoid、泛型和函数类型;
  • Records 与模式匹配;
  • switch 表达式、穷尽性检查和代数数据类型风格;
  • 类、构造函数、继承、接口和类修饰符;
  • mixinmixin classwith 的组合规则;
  • 扩展方法和扩展类型的边界;
  • 在 Flutter、Android、iOS、桌面和 Web 中的语言层差异。

示例默认可以放入 bin/main.dart,通过以下命令执行:

dart format bin/main.dart
dart analyze
dart run

在 Flutter 项目中,对应命令通常是:

flutter analyze
flutter test
flutter run

这些命令分别负责格式化、静态分析、测试和运行。dart analyzeflutter 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
}

这里发生了两个步骤:

  1. value is int 检查运行时对象是否为 int
  2. 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 的位置使用,则称 ST 的子类型,记作:

S<:TS <: T

例如:

int integer = 1;
num number = integer;
Object object = integer;

可以推导出:

int<:num<:Objectint <: num <: Object

原因是:

  • 所有 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 可以赋值到 Stringint 或其他类型的位置。它表达的是控制流事实:执行到这个表达式后,后续路径不会继续。


3. dynamicObject 不同

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. varfinalconst

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; // 类型上允许

这意味着如果:

int<:numint <: num

则通常有:

List<int><:List<num>List<int> <: List<num>

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 不允许为 nullString? 允许为 null。因此以下代码无法通过静态分析:

String? name = 'Alice';

// print(name.length); // 编译错误

原因是 name 的可能值集合包含:

{String}{null}\{String\} \cup \{null\}

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. 取得待匹配值;
  2. 检查值是否符合模式;
  3. 将内部数据绑定到局部变量。

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)。流程是:

  1. 先判断 value 是否为 int
  2. 成功后绑定 number
  3. 再判断 number > 0
  4. 两者都成功才进入分支。

逻辑或模式要求各分支绑定的变量结构一致,否则后续代码无法确定变量是否存在:

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. extendsimplementswith

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:要求子类继承其实现,而不能只把它当作接口实现;相关直接子类型也必须遵守 basefinalsealed 等限制;
  • 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. mixinmixin 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

关键路径如下:

  1. LoadState 是 sealed 根类型;
  2. LoadingLoadedFailed 是有限状态;
  3. switch 根据对象模式识别具体子类;
  4. Loaded 分支进一步读取 items
  5. when items.isEmpty 优先匹配空列表;
  6. 后一个 Loaded 分支处理非空列表;
  7. Failed 分支读取错误对象;
  8. 所有状态都被覆盖,因此无需 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 的 FutureStream、事件循环和 Isolate 属于异步与并发模型;它们仍然使用本文讲述的类型系统:

Future<String> loadName() async {
  return 'Alice';
}

Future<String> 表示未来完成并产生 String;它不是 String 本身。必须通过 awaitthen 获取结果:

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 键;
  • usernull
  • 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,同时接受其运行时失败路径;
  • 需要限制外部继承方式:使用 baseinterfacefinalsealed

Dart 3 的这些能力并不是互相独立的语法集合。类型系统决定值能否流动,空安全决定哪些路径必须处理 null,模式把类型检查和数据解构结合起来,类与 Mixin 决定实现如何组织,扩展则提供不改变原类型的静态 API。把这些机制组合起来,才能在 Flutter 项目中构建可分析、可维护且跨平台行为明确的代码。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。