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 dynamic、Object?、Object 和 Never 不是一回事
理解泛型前,必须区分几种顶层类型。
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; // 编译错误
Object 比 Object? 更适合表达“调用者必须提供一个非空对象”。
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>
含义是:
T是一个类型变量;T必须是Comparable<T>的子类型;- 因此
T可以调用compareTo(T other)。
调用示例:
void main() {
print(maximum<int>(3, 7)); // 7
print(maximum<String>('dart', 'flutter')); // 按字符串排序结果
}
int 和 String 都实现了与自身比较的能力,因此满足 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> 中,T 是 double。
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 表示 S 是 T 的子类型。
如果一个泛型类型构造器 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
为了同时容纳 int 和 double,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
int 和 String 没有一个合适的共同 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" 转成整数 1;as 只是类型检查和类型视图转换,不是数据转换。
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,首先得到的通常是 dynamic、List<dynamic> 或 Map<String, dynamic>,应尽早解析和验证,而不是把 dynamic 传播到整个业务层。
七、泛型 API 的端到端示例:JSON 列表解析
一个可用的泛型解析 API 需要三部分:
- 输入数据;
- 目标元素类型
T; - 把单个 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)
每一步成立的原因是:
jsonDecode返回动态 JSON 结构;decodeList<User>明确指定T = User;decoded is List验证顶层结构;.map<T>(decodeItem)要求每个元素都经过User.fromJson;User.fromJson验证对象结构、字段类型和空值;- 只有全部元素解析成功,函数才返回
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 的对象布局、调试信息和优化方式可能不同。
因此跨平台代码应依赖 is、as、显式解析器和稳定的数据协议,而不是依赖类型名称字符串或某个平台上的运行时打印结果。
九、常见失败方式与诊断路径
9.1 把 List<Cat> 当成可以任意写入的 List<Animal>
final List<Cat> cats = <Cat>[];
final List<Animal> animals = cats;
animals.add(Dog()); // 运行时错误
诊断步骤:
- 查看对象的真实类型是
List<Cat>; - 查看当前变量的静态类型是
List<Animal>; - 找到写入操作
animals.add(Dog()); - 确认协变只保证了赋值关系,不保证任意写入;
- 将 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 没有像某些语言那样普遍使用 in、out 声明泛型类的方差。工程上应通过 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边界对类型参数的限制;is和as的类型检查语义;- 空安全下
T、T?、Object、Object?的静态约束; - 类型推断必须满足参数和边界约束。
运行时行为
这些会在执行期间发生:
- 协变可变集合的非法写入检查;
as失败时抛出类型错误;is List<int>的运行时测试;- JSON 字段类型不符合预期时抛出解析异常;
covariant缩窄参数在调用时进行检查。
实现和平台相关表现
这些不应被当作稳定契约:
runtimeType.toString()的具体文本;- AOT、JIT 和 Web 中类型对象的内部表示;
- 编译器是否对某个泛型函数做内联;
- 不同平台上的性能和内存布局;
- 调试输出中的泛型类型格式。
因此,生产代码应让静态类型承担主要约束,让运行时检查承担外部数据和动态边界的验证,不要把调试表示当成业务协议。
十二、一个实用的设计判断顺序
设计泛型 API 时,可以按以下顺序推导:
-
这个 API 产生
T、消费T,还是同时读写T?
只产生时通常更容易安全协变;消费时要检查函数参数方向;读写可变对象时要特别警惕协变导致的运行时检查。 -
函数体需要
T的哪些能力?
如果需要compareTo、toJson或某个领域操作,应使用边界或显式回调,不要依赖dynamic。 -
null是否是合法业务值?
通过T extends Object、T?和返回值类型把这个决定表达出来。 -
运行时是否需要把外部数据转换成
T?
类型参数不能自动完成构造,通常需要T Function(Object?)、工厂对象或注册表。 -
失败发生在编译期还是运行时?
可由类型系统表达的错误应尽量编译期发现;JSON、插件、数据库和平台通道等外部输入必须在运行时验证。 -
API 是否暴露了不必要的具体容器?
只需要遍历就使用Iterable<T>,只需要异步结果就使用Future<T>,不要用dynamic代替正确的泛型结构。
泛型真正的价值,是把“数据是什么类型”“允许哪些操作”“如何在类型之间传递”以及“错误在哪一层被发现”同时编码进 API。掌握类型参数、边界、协变、函数类型方向和运行时检查后,Dart 泛型就不再只是集合上的尖括号,而会成为 Flutter 应用中数据流和组件接口的核心约束工具。
系列导航与关联阅读
- 系列入口:Flutter 完整学习路线:从 Dart 与 Widget 到多端架构和应用发布
- 上一篇:Dart 集合:List、Set、Map、Iterable、扩展和复杂度
- 下一篇:Dart 错误处理:Exception、Error、StackTrace、Zone 和契约
官方资料
本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论