Flutter 基础体系 · 第 25/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Dart 泛型:类型参数、边界、协变、运行时类型和 API 设计

泛型(generic)允许代码在保留类型信息的同时,适用于多种具体类型。List<int>Future<User>Map<String, double> 和 Flutter 中的 FutureBuilder<T> 都是泛型实例。

泛型解决的不是“少写几个类型名”这么简单,而是同时影响:

  • 编译期类型检查;
  • 空安全;
  • 子类型关系和赋值规则;
  • 回调函数的参数与返回值;
  • 运行时类型测试;
  • 公共 API 的可用性与错误边界。

Dart 3 的类型系统默认启用 sound null safety。下文代码以当前稳定 Dart 3 语言能力为基础,除特别说明外,不依赖 Flutter 专属 API。


一、先建立类型参数的基本模型

1.1 类型参数是“由调用者确定的类型变量”

下面的 T 是类型参数:

T identity<T>(T value) {
  return value;
}

调用时可以显式指定 T

final int a = identity<int>(42);
final String b = identity<String>('hello');

也可以依赖类型推断:

final int a = identity(42);
final String b = identity('hello');

identity(42) 而言,编译器从实参 42 的静态类型 int 推导出:

T = int

于是调用实例化后相当于:

int identity<int>(int value) => value;

这里的“实例化”是类型系统上的推导结果,不意味着每次调用都生成一份独立的机器代码。

类型参数通常出现在三类位置:

// 泛型函数
T first<T>(List<T> values) => values.first;

// 泛型类
class Box<T> {
  final T value;

  Box(this.value);
}

// 泛型类型别名
typedef Predicate<T> = bool Function(T value);

使用泛型类时,可以显式写出类型参数:

final Box<int> numberBox = Box<int>(42);

也可以由构造参数推断:

final Box<int> numberBox = Box(42);

Dart 的构造器推断会结合构造参数和目标上下文共同工作。例如:

final List<String> names = <String>['Ada', 'Grace'];

右侧也可以写成:

final List<String> names = ['Ada', 'Grace'];

但在复杂表达式中,不应盲目依赖推断。显式类型参数通常能让公共 API 的意图更清楚,也能避免推导出过宽的类型。


1.2 dynamicObject?ObjectNever 不是一回事

理解泛型前,必须区分几种顶层类型。

Object?:任何可以作为 Dart 值的对象

Object? value = null;
value = 1;
value = 'text';

Object? 包含可空值,因此 null 也合法。

但从 Object? 读取成员时,编译器不会假设它有业务方法:

Object? value = 'text';

// value.length; // 编译错误

需要先检查类型:

if (value is String) {
  print(value.length);
}

Object:非空的任意对象

Object value = 1;

// value = null; // 编译错误

ObjectObject? 更适合表达“调用者必须提供一个非空对象”。

dynamic:关闭大部分静态检查

dynamic value = 'text';

print(value.length); // 运行时成功
print(value.notExisting()); // 编译通过,运行时报错

dynamic 不是“比 Object? 更通用的安全类型”,而是告诉分析器:这个值的成员访问和调用延迟到运行时检查。

Never:不可能正常产生值

Never fail(String message) {
  throw StateError(message);
}

Never 常用于总是抛异常或永不返回的函数。它是所有类型的子类型,因此可以出现在需要任意返回类型的位置:

String readOrFail(String? value) {
  return value ?? fail('value is null');
}

这里 fail 返回 Never,可以作为 String 分支使用。


二、类型边界:限制 T 能代表什么

2.1 没有边界时,不能调用 T 未知的成员

考虑以下代码:

T maximum<T>(T a, T b) {
  // return a.compareTo(b); // 编译错误
  return a;
}

虽然很多类型都可能实现 compareTo,但 Dart 不能因为“常见”就假设任意 T 都有这个方法。

类型参数默认只保证满足它的上界。为了告诉编译器 T 至少具备某组能力,可以使用 extends 指定边界:

T maximum<T extends Comparable<T>>(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;
}

这里的边界可以拆成三部分:

T extends Comparable<T>

含义是:

  1. T 是一个类型变量;
  2. T 必须是 Comparable<T> 的子类型;
  3. 因此 T 可以调用 compareTo(T other)

调用示例:

void main() {
  print(maximum<int>(3, 7)); // 7
  print(maximum<String>('dart', 'flutter')); // 按字符串排序结果
}

intString 都实现了与自身比较的能力,因此满足 Comparable<T> 边界。

这类边界常被称为 F-bounded 形式,因为类型参数出现在自己的边界内部:

T extends Comparable<T>

它表达的是“T 能与另一个同类型的 T 比较”,而不仅是“T 具有某个不相关的比较接口”。


2.2 边界决定函数体中允许使用的操作

下面是一个带边界的容器:

class NumericBox<T extends num> {
  final T value;

  NumericBox(this.value);

  double doubled() => value.toDouble() * 2;
}

调用:

final NumericBox<int> a = NumericBox(10);
final NumericBox<double> b = NumericBox(2.5);

T extends num 保证了 value 至少具有 num 的成员,例如 toDouble()

但以下代码不成立:

// final NumericBox<String> box = NumericBox('text');
// 编译错误:String 不是 num 的子类型

边界并不会把 T 变成固定的 num。在 NumericBox<int> 中,T 仍然是 int;在 NumericBox<double> 中,Tdouble


2.3 可空边界会改变 API 的空安全语义

以下两个函数对 T 的约束不同:

T requireValue<T extends Object>(T value) {
  return value;
}

T? optionalValue<T extends Object>(T? value) {
  return value;
}

T extends Object 表示 T 本身必须是非空类型,因此:

final String name = requireValue<String>('Ada');
// final String bad = requireValue<String>(null); // 编译错误

T? 表示返回值可以为空:

final String? name = optionalValue<String>(null);

如果 API 设计的语义是“调用者传入的元素必须非空”,使用 T extends Object 能把这个约束表达在类型系统中;如果元素允许为空,则应明确使用 T?T extends Object?

例如:

class NonEmptyCache<T extends Object> {
  T? _value;

  void put(T value) {
    _value = value;
  }

  T? get value => _value;
}

这个缓存不允许写入 null,但缓存本身在尚未写入时可以没有值。


2.4 边界不等于运行时验证

下面的边界只在编译期约束调用者:

T parse<T extends Object>(T value) => value;

它不会在运行时检查某个字符串是否“看起来像”某个 T。更不能通过边界自动把 JSON 转成任意类型。

例如:

T decode<T>(Object json) {
  // 不能根据 T 自动知道如何把 json 解析为 T
  throw UnimplementedError();
}

原因是 T 只描述目标类型,不包含构造对象、解析字段或处理错误的规则。一个 User 需要字段映射,一个 DateTime 需要字符串解析,一个枚举需要值查找;类型参数本身不提供这些业务逻辑。

因此实际 API 通常接收解析器:

T decode<T>(
  Object? input,
  T Function(Object? input) fromJson,
) {
  return fromJson(input);
}

三、泛型类型的子类型关系与协变

3.1 协变的形式化定义

S <: T 表示 ST 的子类型。

如果一个泛型类型构造器 C 对类型参数是协变的,则有:

S <: T
推出
C<S> <: C<T>

Dart 的常见泛型类默认按协变处理。因此:

class Animal {}

class Cat extends Animal {}

List<Cat> cats = <Cat>[Cat()];
List<Animal> animals = cats;

赋值成立,因为:

Cat <: Animal
List<Cat> <: List<Animal>

同理:

Future<Cat> loadCat() async => Cat();

Future<Animal> loadAnimal() = loadCat;

Future<Cat> 可以作为 Future<Animal> 使用,因为异步结果只会“产出”一个 Cat,而 Cat 当然也是 Animal


3.2 为什么可变 List 仍然可以协变

初学者通常会提出一个正确的疑问:

List<Cat> 允许添加 Cat,但 List<Animal> 允许添加任意 Animal。如果前者能赋值给后者,是否会把 Dog 写进 List<Cat>

答案是:Dart 保留了这种赋值关系,但通过运行时检查阻止非法写入。

class Animal {}

class Cat extends Animal {}

class Dog extends Animal {}

void main() {
  final List<Cat> cats = <Cat>[Cat()];
  final List<Animal> animals = cats;

  animals.add(Dog()); // 运行时抛出类型错误
}

中间状态可以写成:

原始对象的真实类型:List<Cat>
视图变量的静态类型:List<Animal>
写入值的实际类型:Dog
List<Cat> 要求写入值:Cat
结果:运行时类型错误

这不是“完全静态安全的不可变协变”,而是 Dart 的协变泛型与运行时参数检查共同形成的行为。代码能够通过静态分析,并不代表所有写操作都能成功。

如果希望 API 自然地支持协变,优先暴露只读输入:

int totalLength(Iterable<String> values) {
  var total = 0;
  for (final value in values) {
    total += value.length;
  }
  return total;
}

调用者可以传入:

final List<String> names = ['Ada', 'Grace'];
print(totalLength(names));

但如果 API 需要修改集合,应该让写入方向明确:

void addAnimal(List<Animal> animals, Animal value) {
  animals.add(value);
}

调用者不应把它当成“任何 List<Cat> 都能安全接受”的函数。


3.3 函数类型的参数是逆变的

函数类型的规则与普通泛型容器不同。

设函数类型为:

(A) -> R

它表示接收 A,返回 R

若:

B <: A
且
R <: Q

则:

(A) -> R <: (B) -> Q

也就是说:

  • 参数位置是逆变的;
  • 返回值位置是协变的。

用具体类型推导:

Cat <: Animal

函数:

void handleAnimal(Animal animal) {}

可以赋值给只承诺传入 Cat 的回调:

void Function(Cat) handler = handleAnimal;

因为 handleAnimal 能处理任意 Animal,当然能处理 Cat

反过来则不安全:

void handleCat(Cat cat) {}

// void Function(Animal) handler = handleCat;
// 编译错误

调用者通过 void Function(Animal) 这个类型承诺可能传入 Dog,而 handleCat 无法处理 Dog

返回值方向则相反:

Cat createCat() => Cat();

Animal Function() creator = createCat;

产生 Cat 的函数可以当作产生 Animal 的函数使用。

完整关系是:

class Animal {}

class Cat extends Animal {}

void consumeAnimal(Animal animal) {}
Cat produceCat() => Cat();

void main() {
  void Function(Cat) consumer = consumeAnimal;
  Animal Function() producer = produceCat;

  consumer(Cat());
  final Animal animal = producer();
}

3.4 covariant 允许重写时缩窄参数,但会引入运行时检查

Dart 默认要求子类重写方法时遵守函数参数的安全方向。下面的重写不安全:

class Animal {}

class Cat extends Animal {}

class Dog extends Animal {}

class Handler {
  void handle(Animal animal) {}
}

class CatHandler extends Handler {
  // @override
  // void handle(Cat cat) {}
  // 默认情况下不允许这样缩窄参数类型
}

可以使用 covariant 显式声明:

class CovariantHandler {
  void handle(covariant Animal animal) {}
}

class CatHandler extends CovariantHandler {
  @override
  void handle(Cat cat) {
    print('handle cat');
  }
}

现在:

void main() {
  CovariantHandler handler = CatHandler();

  handler.handle(Cat()); // 成功
  handler.handle(Dog()); // 运行时类型错误
}

covariant 的含义不是“让程序更安全”,而是把“参数必须是 Cat”的检查推迟到运行时。它适用于确实需要表达更窄参数约束的框架或继承层次,但公共 API 应谨慎使用,因为调用者看到的静态父类类型可能比实际实现接受的类型更宽。


四、类型推断:编译器如何决定 T

类型推断不是猜测,而是根据上下文收集约束。

看这个函数:

T choose<T>(T first, T second) {
  return first;
}

调用:

final result = choose(1, 2.5);

两个实参分别提供约束:

int <: T
double <: T

为了同时容纳 intdouble,Dart 可能推导出它们的共同上界,通常表现为 num。实际代码中,可以通过目标类型让意图更明确:

final num result = choose<num>(1, 2.5);

另一个例子:

T convert<T>(T value) => value;

final Object value = convert<Object>('text');

显式指定 T = Object 后,函数返回值的静态类型就是 Object,即使实际对象仍然是 String

类型推断不会违反边界:

T maxValue<T extends Comparable<T>>(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;
}

// maxValue(1, 'text');
// 编译错误:无法找到同时满足边界和参数要求的 T

intString 没有一个合适的共同 T 能满足 T extends Comparable<T> 并保证两个参数都可按同一 T 比较。

如果推断结果不符合 API 语义,应使用显式类型参数或调整参数类型,而不是用 dynamic 强行消除错误。


五、泛型在运行时是否存在

5.1 Dart 的泛型类型信息是可运行时观察的

Dart 泛型不是简单的“编译前文本替换”。可以对具体化的泛型类型进行测试:

void inspect(Object value) {
  if (value is List<int>) {
    print('这是一个只包含 int 的列表视图');
  }

  if (value is List<String>) {
    print('这是一个只包含 String 的列表视图');
  }
}

运行:

void main() {
  inspect(<int>[1, 2, 3]);
  inspect(<String>['a', 'b']);
}

is List<int> 是运行时类型测试,不能简单理解成“泛型参数在编译后全部消失”。

同样,as 可以检查类型并转换视图:

List<int> toIntList(Object value) {
  return value as List<int>;
}

若实参不是符合要求的列表,运行时会抛出类型错误:

toIntList(<String>['1']);
// 运行时类型错误

这并不会把字符串 "1" 转成整数 1as 只是类型检查和类型视图转换,不是数据转换。


5.2 runtimeType 可以观察类型,但不应依赖字符串名称

每个对象都有 runtimeType

void main() {
  final value = <int>[1, 2, 3];

  print(value.runtimeType);
  print(value is List<int>);
}

输出中的类型名称通常类似:

List<int>
true

但是 runtimeType.toString() 的具体文本格式不是稳定的序列化协议。不要这样写业务逻辑:

if (value.runtimeType.toString() == 'User') {
  // 不推荐
}

原因包括:

  • 编译器可能改变类型名称;
  • AOT、JIT 和 Web 编译产物的显示形式可能不同;
  • 混淆、压缩或树摇优化可能影响调试文本;
  • 泛型类型名称的格式不适合跨版本持久化。

应使用静态类型测试:

if (value is User) {
  // ...
}

或者使用显式注册表:

typedef Decoder<T> = T Function(Object? json);

class DecoderRegistry {
  final Map<Type, Decoder<Object?>> _decoders = {};

  void register<T>(Decoder<T> decoder) {
    _decoders[T] = (json) => decoder(json);
  }

  T decode<T>(Object? json) {
    final decoder = _decoders[T];

    if (decoder == null) {
      throw StateError('No decoder registered for $T');
    }

    return decoder(json) as T;
  }
}

这里使用 Type 作为键,而不是使用类型名称字符串。需要注意的是,这种注册表是运行时机制,不会自动解决泛型嵌套、版本兼容或错误格式验证问题。


5.3 类型参数不能直接构造对象

以下代码无效:

T create<T>() {
  // return T(); // 编译错误
  throw UnimplementedError();
}

原因是 T 只代表一个类型,不代表一个可调用的无参构造器。即使某个类型通常有默认构造器,类型系统也不会据此允许任意构造。

正确的做法是传入工厂函数:

T create<T>(T Function() builder) {
  return builder();
}

class User {
  User();
}

void main() {
  final user = create<User>(User.new);
  print(user);
}

也可以把工厂函数放在类中:

class FactoryBox<T> {
  final T Function() create;

  FactoryBox(this.create);

  T build() => create();
}

这种设计同时解决了运行时构造和依赖注入问题。


5.4 类型参数不能拥有静态成员

类型参数属于对象实例的类型上下文,而静态成员属于类本身。下面的写法不成立:

class Holder<T> {
  // static T? cached; // 编译错误
}

如果需要每个 Holder<T> 维护独立状态,应把状态放在实例中:

class Holder<T> {
  T? value;

  Holder(this.value);
}

如果需要按类型注册数据,应显式使用 Type 键:

class TypeStore {
  final Map<Type, Object?> _values = {};

  void put<T>(T value) {
    _values[T] = value;
  }

  T? get<T>() {
    return _values[T] as T?;
  }
}

这个示例中的 as T? 是运行时边界。若外部通过不安全方式破坏注册表内容,读取时仍可能失败,因此生产代码通常还会封装写入路径并进行一致性校验。


六、运行时检查、静态类型和实际类型的区别

下面代码中的三个概念不同:

Animal animal = Cat();
  • Animal 是变量的静态类型;
  • Cat 是对象的实际运行时类型;
  • 变量只能直接访问 Animal 声明的成员。
animal.meow(); // 编译错误,Animal 没有声明 meow

如果确认对象确实是 Cat,可以使用类型提升:

if (animal is Cat) {
  animal.meow();
}

或使用显式转换:

final cat = animal as Cat;
cat.meow();

as 可能失败:

final Dog dog = animal as Dog;
// 运行时类型错误,因为实际对象是 Cat

对于泛型也一样:

Object value = <int>[1, 2, 3];

if (value is List<int>) {
  final List<int> numbers = value;
  print(numbers.first);
}

类型测试成功后,分析器可以在该分支内提升 value 的静态类型。

需要注意集合元素检查的边界。value is List<int> 不能把一个实际包含字符串的动态列表安全地“转换”成整数列表:

dynamic value = <dynamic>[1, 'wrong'];

if (value is List<int>) {
  // 通常不会进入这里,运行时检查会发现元素不满足要求
}

如果数据来自 JSON,首先得到的通常是 dynamicList<dynamic>Map<String, dynamic>,应尽早解析和验证,而不是把 dynamic 传播到整个业务层。


七、泛型 API 的端到端示例:JSON 列表解析

一个可用的泛型解析 API 需要三部分:

  1. 输入数据;
  2. 目标元素类型 T
  3. 把单个 JSON 值转换为 T 的函数。
import 'dart:convert';

List<T> decodeList<T>(
  String source,
  T Function(Object? json) decodeItem,
) {
  final decoded = jsonDecode(source);

  if (decoded is! List) {
    throw const FormatException('Expected a JSON array');
  }

  return decoded
      .map<T>(decodeItem)
      .toList(growable: false);
}

class User {
  final int id;
  final String name;

  User({
    required this.id,
    required this.name,
  });

  factory User.fromJson(Object? json) {
    if (json is! Map<String, dynamic>) {
      throw const FormatException('User must be a JSON object');
    }

    final id = json['id'];
    final name = json['name'];

    if (id is! int || name is! String) {
      throw const FormatException('Invalid User fields');
    }

    return User(id: id, name: name);
  }

  @override
  String toString() => 'User(id: $id, name: $name)';
}

void main() {
  const source = '''
    [
      {"id": 1, "name": "Ada"},
      {"id": 2, "name": "Grace"}
    ]
  ''';

  try {
    final users = decodeList<User>(source, User.fromJson);

    for (final user in users) {
      print(user);
    }
  } on FormatException catch (error) {
    print('Invalid response: $error');
  } on FormatException catch (error) {
    print('Invalid response: $error');
  }
}

上面 try 中的两个 on FormatException 重复,实际代码应保留一个。正确版本如下:

void main() {
  const source = '''
    [
      {"id": 1, "name": "Ada"},
      {"id": 2, "name": "Grace"}
    ]
  ''';

  try {
    final users = decodeList<User>(source, User.fromJson);

    for (final user in users) {
      print(user);
    }
  } on FormatException catch (error) {
    print('Invalid response: $error');
  }
}

预期输出:

User(id: 1, name: Ada)
User(id: 2, name: Grace)

每一步成立的原因是:

  1. jsonDecode 返回动态 JSON 结构;
  2. decodeList<User> 明确指定 T = User
  3. decoded is List 验证顶层结构;
  4. .map<T>(decodeItem) 要求每个元素都经过 User.fromJson
  5. User.fromJson 验证对象结构、字段类型和空值;
  6. 只有全部元素解析成功,函数才返回 List<User>

如果 JSON 顶层不是数组:

{"id": 1, "name": "Ada"}

函数抛出:

FormatException: Expected a JSON array

如果某个元素缺少字段或类型错误:

[{"id": "not-an-int", "name": "Ada"}]

函数抛出:

FormatException: Invalid User fields

这比下面这种写法更可靠:

final users = (jsonDecode(source) as List)
    .cast<Map<String, dynamic>>();

cast 只能做类型视图转换和运行时检查,不能验证字段是否存在,也不能把字符串转换为整数。业务对象的构造和数据校验仍需要显式解析器。


八、泛型与 Flutter API

Flutter 自身大量使用泛型来保证异步数据、状态和回调的一致性。

例如:

Future<String> loadTitle() async {
  return 'Dart';
}

对应:

FutureBuilder<String>(
  future: loadTitle(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    return Text(snapshot.data ?? 'No title');
  },
);

这里的 FutureBuilder<String> 约束了:

snapshot.data 的静态类型接近 String?

因为异步任务可能尚未完成,或者完成结果为空,所以即使 Future<String> 本身返回非空 String,构建过程中的 data 仍可能没有值。

类似地:

ValueListenableBuilder<int>(
  valueListenable: counter,
  builder: (context, value, child) {
    return Text('$value');
  },
);

value 的静态类型由 ValueListenableBuilder<int> 确定为 int,因此回调中不需要使用 dynamic

Flutter 的 Android、iOS、桌面和 Web 目标不会改变 Dart 泛型的静态子类型规则。例如 Future<Cat> 能否赋给 Future<Animal>,在不同平台上都遵循同一语言规则。

平台差异主要出现在运行时环境和库可用性:

  • dart:io 不适用于 Web;
  • Web 编译器可能以 JavaScript 或其他 Web 目标表示类型信息;
  • runtimeType.toString() 的显示形式不应被当作跨平台协议;
  • AOT、JIT 和 Web 的对象布局、调试信息和优化方式可能不同。

因此跨平台代码应依赖 isas、显式解析器和稳定的数据协议,而不是依赖类型名称字符串或某个平台上的运行时打印结果。


九、常见失败方式与诊断路径

9.1 把 List<Cat> 当成可以任意写入的 List<Animal>

final List<Cat> cats = <Cat>[];
final List<Animal> animals = cats;

animals.add(Dog()); // 运行时错误

诊断步骤:

  1. 查看对象的真实类型是 List<Cat>
  2. 查看当前变量的静态类型是 List<Animal>
  3. 找到写入操作 animals.add(Dog())
  4. 确认协变只保证了赋值关系,不保证任意写入;
  5. 将 API 改为只读 Iterable<Animal>,或真正创建 List<Animal>
final List<Animal> animals = <Animal>[...cats];
animals.add(Dog());

这里使用展开操作创建了新的 List<Animal>,不再与原始 List<Cat> 共享可变存储。


9.2 用 dynamic 逃避泛型错误

dynamic data = getData();
final result = data['name'];

短期内代码可能通过分析,但错误会从编译期推迟到运行时。更好的方式是尽快建立边界:

Object? data = getData();

if (data is Map<String, Object?>) {
  final name = data['name'];

  if (name is String) {
    print(name);
  }
}

如果数据结构复杂,应定义模型和解析函数,而不是在多个页面中重复类型判断。


9.3 误以为 as 会执行转换

final value = '42' as int;

这不会把字符串转换为整数,而是直接抛出类型错误。

数据转换应写成:

final value = int.parse('42');

对泛型 API 也是同样原则:as T 不能凭空生成 T 的值。


9.4 误以为泛型参数一定能用于构造

T make<T>() {
  // return T(); // 无法编译
  throw UnimplementedError();
}

应该注入构造策略:

T make<T>(T Function() constructor) {
  return constructor();
}

这也使测试更容易,因为测试代码可以传入替代构造函数。


十、API 设计中的泛型取舍

10.1 输入尽量使用能表达真实需求的最宽类型

如果函数只遍历元素,不需要索引和修改,使用 Iterable<T>

double average(Iterable<num> values) {
  var sum = 0.0;
  var count = 0;

  for (final value in values) {
    sum += value;
    count++;
  }

  if (count == 0) {
    throw StateError('Cannot calculate average of an empty iterable');
  }

  return sum / count;
}

这样 List<int>Set<double> 等都可以作为输入。

如果函数要求随机访问,应使用 List<T>;如果要求持续异步事件,应使用 Stream<T>;如果只返回一个异步结果,应使用 Future<T>。泛型参数描述元素类型,外层容器则描述数据流和操作能力。


10.2 只读生产者可以利用协变,消费者要关注写入方向

下面的接口只产生数据:

abstract interface class Source<T> {
  T read();
}

由于 Source<Cat> 只产生 Cat,把它当作 Source<Animal> 通常是安全的。

下面的接口接收数据:

abstract interface class Consumer<T> {
  void accept(T value);
}

它的使用方向应遵守函数参数的逆变原则:能接受更宽类型的消费者,可以用于更窄类型的场景;反过来则不安全。

Dart 没有像某些语言那样普遍使用 inout 声明泛型类的方差。工程上应通过 API 的读写职责、回调类型和接口拆分来表达安全方向。


10.3 用边界表达能力,而不是在函数体里假设能力

不推荐:

T sortFirst<T>(List<T> values) {
  values.sort((a, b) {
    // 不能假设 T 支持比较
    return 0;
  });
  return values.first;
}

如果要求元素可比较,应明确边界:

T sortFirst<T extends Comparable<T>>(List<T> values) {
  if (values.isEmpty) {
    throw StateError('values must not be empty');
  }

  values.sort();
  return values.first;
}

不过 T extends Comparable<T> 并不能覆盖所有实际比较关系。例如某些类型实现的是 Comparable<BaseType>,而不是 Comparable<自身类型>。如果 API 的比较规则来自外部,应改为注入比较器:

T minBy<T>(
  Iterable<T> values,
  int Function(T a, T b) compare,
) {
  final iterator = values.iterator;

  if (!iterator.moveNext()) {
    throw StateError('values must not be empty');
  }

  var result = iterator.current;

  while (iterator.moveNext()) {
    if (compare(iterator.current, result) < 0) {
      result = iterator.current;
    }
  }

  return result;
}

这种设计比强行要求 Comparable<T> 更通用,因为排序规则不一定属于元素类型本身。


10.4 公共 API 应明确可空性和错误方式

比较以下两个声明:

T find<T>(Iterable<T> values);

和:

T? tryFind<T>(Iterable<T> values);

前者暗示找不到时会抛出异常,后者明确表达“可能没有结果”。实现必须与声明一致:

T? tryFind<T>(
  Iterable<T> values,
  bool Function(T value) test,
) {
  for (final value in values) {
    if (test(value)) {
      return value;
    }
  }

  return null;
}

如果使用 T extends Object,则可以清楚地区分“没有结果的 null”与“结果本身不允许为 null”:

T? tryFindNonNull<T extends Object>(
  Iterable<T> values,
  bool Function(T value) test,
) {
  for (final value in values) {
    if (test(value)) {
      return value;
    }
  }

  return null;
}

十一、编译期保证、运行时行为与实现差异

需要区分三层事实。

语言和类型系统保证

这些属于 Dart 语言规则:

  • List<Cat>List<Animal> 的协变关系;
  • 函数参数逆变、返回值协变;
  • extends 边界对类型参数的限制;
  • isas 的类型检查语义;
  • 空安全下 TT?ObjectObject? 的静态约束;
  • 类型推断必须满足参数和边界约束。

运行时行为

这些会在执行期间发生:

  • 协变可变集合的非法写入检查;
  • as 失败时抛出类型错误;
  • is List<int> 的运行时测试;
  • JSON 字段类型不符合预期时抛出解析异常;
  • covariant 缩窄参数在调用时进行检查。

实现和平台相关表现

这些不应被当作稳定契约:

  • runtimeType.toString() 的具体文本;
  • AOT、JIT 和 Web 中类型对象的内部表示;
  • 编译器是否对某个泛型函数做内联;
  • 不同平台上的性能和内存布局;
  • 调试输出中的泛型类型格式。

因此,生产代码应让静态类型承担主要约束,让运行时检查承担外部数据和动态边界的验证,不要把调试表示当成业务协议。


十二、一个实用的设计判断顺序

设计泛型 API 时,可以按以下顺序推导:

  1. 这个 API 产生 T、消费 T,还是同时读写 T
    只产生时通常更容易安全协变;消费时要检查函数参数方向;读写可变对象时要特别警惕协变导致的运行时检查。

  2. 函数体需要 T 的哪些能力?
    如果需要 compareTotoJson 或某个领域操作,应使用边界或显式回调,不要依赖 dynamic

  3. null 是否是合法业务值?
    通过 T extends ObjectT? 和返回值类型把这个决定表达出来。

  4. 运行时是否需要把外部数据转换成 T
    类型参数不能自动完成构造,通常需要 T Function(Object?)、工厂对象或注册表。

  5. 失败发生在编译期还是运行时?
    可由类型系统表达的错误应尽量编译期发现;JSON、插件、数据库和平台通道等外部输入必须在运行时验证。

  6. API 是否暴露了不必要的具体容器?
    只需要遍历就使用 Iterable<T>,只需要异步结果就使用 Future<T>,不要用 dynamic 代替正确的泛型结构。

泛型真正的价值,是把“数据是什么类型”“允许哪些操作”“如何在类型之间传递”以及“错误在哪一层被发现”同时编码进 API。掌握类型参数、边界、协变、函数类型方向和运行时检查后,Dart 泛型就不再只是集合上的尖括号,而会成为 Flutter 应用中数据流和组件接口的核心约束工具。


系列导航与关联阅读

官方资料

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