泛型基础
为什么使用泛型
// 不使用泛型
Object getFirst(List list) {
return list[0];
}
// 使用泛型
T getFirst<T>(List<T> list) {
return list[0];
}
基本语法
// 泛型类
class Box<T> {
T value;
Box(this.value);
}
// 泛型方法
T transform<T>(T value) {
return value;
}
泛型类
单类型参数
class Stack<T> {
final List<T> _items = [];
void push(T item) => _items.add(item);
T pop() => _items.removeLast();
bool get isEmpty => _items.isEmpty;
}
// 使用示例
var numberStack = Stack<int>();
var stringStack = Stack<String>();
多类型参数
class Pair<K, V> {
final K first;
final V second;
Pair(this.first, this.second);
}
// 使用示例
var pair = Pair<String, int>('age', 25);
泛型方法
基本用法
T findFirst<T>(List<T> items, bool Function(T) test) {
for (var item in items) {
if (test(item)) return item;
}
throw StateError('No element');
}
// 使用示例
var numbers = [1, 2, 3, 4, 5];
var firstEven = findFirst<int>(numbers, (n) => n.isEven);
泛型方法约束
// 注意:不能写成 T extends Comparable<T>——int 实现的是 Comparable<num> 而不是
// Comparable<int>,那样 max<int> 会编译不过。用 Comparable<dynamic> 放宽上界。
T max<T extends Comparable<dynamic>>(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// 使用示例
void main() {
print(max<int>(10, 20)); // 20
print(max<String>('hello', 'world')); // world
}
泛型接口
abstract class Cache<T> {
T getItem(String key);
void setItem(String key, T value);
void removeItem(String key);
}
class MemoryCache<T> implements Cache<T> {
final Map<String, T> _cache = {};
@override
T getItem(String key) => _cache[key]!;
@override
void setItem(String key, T value) => _cache[key] = value;
@override
void removeItem(String key) => _cache.remove(key);
}
完整示例
// 通用数据结构示例
class DataContainer<T> {
final List<T> _items = [];
final void Function(T)? _onAdd;
final void Function(T)? _onRemove;
DataContainer({
void Function(T)? onAdd,
void Function(T)? onRemove,
}) : _onAdd = onAdd,
_onRemove = onRemove;
void add(T item) {
_items.add(item);
_onAdd?.call(item);
}
bool remove(T item) {
final removed = _items.remove(item);
if (removed) {
_onRemove?.call(item);
}
return removed;
}
List<R> map<R>(R Function(T) transform) {
return _items.map(transform).toList();
}
List<T> where(bool Function(T) test) {
return _items.where(test).toList();
}
T? find(bool Function(T) test) {
for (var item in _items) {
if (test(item)) return item;
}
return null;
}
}
// 使用示例
void main() {
// 创建整数容器
var numbers = DataContainer<int>(
onAdd: (number) => print('Added: $number'),
onRemove: (number) => print('Removed: $number'),
);
numbers.add(1);
numbers.add(2);
numbers.add(3);
// 转换操作
var doubled = numbers.map((n) => n * 2);
print('Doubled: $doubled');
// 过滤操作
var evenNumbers = numbers.where((n) => n.isEven);
print('Even numbers: $evenNumbers');
// 查找操作
var firstEven = numbers.find((n) => n.isEven);
print('First even number: $firstEven');
// 创建字符串容器
var words = DataContainer<String>(
onAdd: (word) => print('Added word: $word'),
onRemove: (word) => print('Removed word: $word'),
);
words.add('hello');
words.add('world');
// 字符串操作
var upperWords = words.map((w) => w.toUpperCase());
print('Upper words: $upperWords');
var longWords = words.where((w) => w.length > 4);
print('Long words: $longWords');
}
泛型约束
extends 关键字
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
void increment() {
if (value is int) {
value = (value + 1) as T;
} else if (value is double) {
value = (value + 1.0) as T;
}
}
}
单上界:Dart 不支持多重约束
// 注意:Dart 的泛型一次只能写一个 extends 上界,
// 不支持 Java 式的 `T extends A & B` 交叉约束写法,那样会直接语法报错。
// 需要多个能力时,把共用的那个抽成一个接口,或收紧到最具体的一个上界:
class JsonBox<T extends Comparable<T>> {
T value;
JsonBox(this.value);
String toJson() => '{"value": "$value"}';
bool isGreaterThan(T other) => value.compareTo(other) > 0;
}
注意事项
- Dart 的泛型在运行时是具化的(reified),类型参数不会被擦除:
[1,2] is List<int>为 true,is List<String>为 false,运行时拿得到真实类型 - 注意类型安全和类型检查
- 合理使用泛型约束
- 避免过度使用泛型
- 注意泛型和异步操作的结合
最佳实践
- 优先使用具体类型而不是 dynamic
- 合理使用泛型约束
- 为泛型参数使用有意义的名称
- 适当使用类型推断
- 注意泛型的性能影响
实战案例:我的代码里泛型无处不在,但只用了一种
翻我的两个项目,泛型出现的地方:
// ygking_json_to_dart_model
void _generateClass(Map<String, dynamic> map, ...) // JSON 对象
final dynamic jsonData = json.decode(jsonStr); // 注意:这里故意不用泛型
// flutter_brightness
StreamController<String>? _controller; // 串口数据流
Stream<String> get dataStream => ... // 对外暴露的类型
有意思的是两种相反的选择:
-
StreamController<String>必须写死泛型。串口吐出来的是文本,我在SerialService内部就把字节转成了 String,流的类型定死为String。这样 UI 层listen到的 data 直接就是String,不用到处as。如果写成StreamController<dynamic>,每个订阅者都要自己判断类型——类型信息在源头就丢了,后面全是坑。 -
json.decode的返回值我故意不加泛型。json.decode返回dynamic,我直接final dynamic jsonData = ...接住,然后用is判断(jsonData is Map<String, dynamic>)。为什么不在 decode 时就定死?因为输入不可信——用户粘进来的可能是对象、可能是数组、可能是个纯字符串,decode 之前我根本不知道。泛型的意义是”给确定的东西加约束”,输入都不确定时,硬加泛型就是自欺欺人。
所以我的泛型观很简单:输出(我生产的数据)泛型写死,输入(别人给的数据)保持 dynamic + is 判断。Map<String, dynamic> 这个组合就是这么来的:key 一定是 String(JSON 规范定的),value 不确定,所以是 dynamic。一句话:泛型不是”越具体越好”,而是”在确定的地方具体,在不确定的地方诚实”。
总结
泛型就干两件事:把”类型”变成参数传进去,让编译器替你检查。记住三个坑就行:约束上界别写错(int 是 Comparable
