Collections(集合)
Dart 使用集合保存一组相关数据。最常用的集合类型是 List、Set 和 Map:List 按顺序保存元素,Set 保证元素不重复,Map 使用键查找对应的值。
这三种类型都支持泛型。为集合声明元素、键和值的类型,可以让静态分析器在编译前发现错误。
void main() {
List<String> topics = ['types', 'records'];
Set<String> tags = {'dart', 'language'};
Map<String, int> scores = {'types': 90, 'records': 95};
print(topics);
print(tags);
print(scores);
}
List:有顺序的元素序列
List<T> 按插入顺序保存元素,允许重复,并通过从 0 开始的索引访问元素。列表字面量 [] 创建的是可增长列表。
void main() {
final topics = <String>['types', 'records', 'types'];
print(topics[0]); // types
print(topics.length); // 3
topics.add('collections');
topics.insert(1, 'variables');
topics.remove('types'); // 删除第一个匹配项
print(topics); // [variables, records, types, collections]
}
读取或修改不存在的索引会抛出 RangeError。访问列表首尾元素前,应先确认列表不为空:
void main() {
final topics = <String>[];
if (topics.isNotEmpty) {
print(topics.first);
}
}
固定长度列表
固定长度列表可以替换已有元素,但不能增加或删除元素。List.filled 默认创建固定长度列表:
void main() {
final progress = List<int>.filled(3, 0);
progress[1] = 80;
// progress.add(100); // 运行时抛出 UnsupportedError
print(progress); // [0, 80, 0]
}
需要让 List.filled 创建可增长列表时,可以传入 growable: true。
Set:不重复的元素集合
Set<T> 中不会同时存在两个相等的元素,适合去重和成员检查。Set 不提供按索引访问的语义。
void main() {
final tags = <String>{'dart', 'types', 'dart'};
print(tags); // {dart, types}
print(tags.contains('dart')); // true
tags.add('collections');
tags.remove('types');
print(tags); // {dart, collections}
}
集合运算可以表达两个 Set 之间的关系:
void main() {
final learned = <String>{'variables', 'types', 'records'};
final planned = <String>{'records', 'collections', 'functions'};
print(learned.intersection(planned)); // {records}
print(learned.union(planned));
print(planned.difference(learned)); // {collections, functions}
}
空花括号 {} 默认表示 Map<dynamic, dynamic>,不是 Set。创建空 Set 时必须提供类型参数:
void main() {
var emptyMap = {}; // Map<dynamic, dynamic>
var emptySet = <String>{}; // Set<String>
print(emptyMap.runtimeType);
print(emptySet.runtimeType);
}
Map:键值映射
Map<K, V> 使用唯一的键关联值。给已有键赋值会替换原来的值,使用不存在的键读取时会返回 null。
void main() {
final scores = <String, int>{
'types': 90,
'records': 95,
};
scores['types'] = 96;
scores['collections'] = 92;
int? score = scores['functions'];
print(score); // null
print(scores['types']); // 96
}
即使值类型 V 不可空,map[key] 的结果仍是 V?,因为对应的键可能不存在。如果 null 本身也是合法的值,应使用 containsKey 区分“键不存在”和“键存在但值为 null”:
void main() {
final notes = <String, String?>{'collections': null};
print(notes['collections']); // null
print(notes['unknown']); // null
print(notes.containsKey('collections')); // true
print(notes.containsKey('unknown')); // false
}
putIfAbsent 只在键不存在时计算并添加值。遍历 entries 可以同时读取键和值:
void main() {
final scores = <String, int>{'types': 90};
scores.putIfAbsent('records', () => 95);
for (final entry in scores.entries) {
print('${entry.key}: ${entry.value}');
}
}
类型推断与泛型
Dart 通常能根据字面量元素推断集合类型,但显式写出泛型参数能让空集合和公共 API 的意图更清楚。
void main() {
var names = ['Ada', 'Lin']; // List<String>
var ids = <int>{}; // Set<int>
var scores = <String, int>{}; // Map<String, int>
names.add('Mia');
ids.add(1);
scores['types'] = 90;
}
泛型类型会限制可以加入集合的值:
void main() {
final scores = <int>[90, 95];
// scores.add('100'); // 编译错误:String 不能作为 int 加入列表
scores.add(100);
print(scores);
}
避免在没有必要时使用不带类型参数的集合或 dynamic,否则部分类型错误会被推迟到运行时。
展开元素与集合控制流
展开运算符 ... 可以把另一个集合的元素放入集合字面量。源集合可能为 null 时,使用空安全展开运算符 ...? 跳过它。
void main() {
const basics = ['variables', 'types'];
List<String>? optionalTopics;
final topics = <String>[
...basics,
...?optionalTopics,
'collections',
];
print(topics); // [variables, types, collections]
}
集合字面量中可以直接使用 if 和 for,这种写法称为集合控制流。它们创建元素,不是先创建集合再进行修改。
void main() {
const includeAdvanced = true;
const lessonNames = ['types', 'records'];
final menu = <String>[
'intro',
if (includeAdvanced) 'generics',
for (final name in lessonNames) 'lesson: $name',
];
print(menu);
}
Map 字面量也支持展开、if 和 for:
void main() {
const baseScores = {'types': 90};
const extraTopics = ['records', 'collections'];
final scores = <String, int>{
...baseScores,
for (final topic in extraTopics) topic: 95,
};
print(scores);
}
遍历、筛选与转换
List、Set 和 Map 都可以用 for-in 遍历。对于元素集合,常用的 where 负责筛选,map 负责转换:
void main() {
final scores = <int>[72, 90, 85, 96];
final labels = scores
.where((score) => score >= 90)
.map((score) => 'score: $score')
.toList();
print(labels); // [score: 90, score: 96]
}
where 和 map 返回 Iterable,计算通常会在迭代时才发生。需要独立的列表或集合时,调用 toList() 或 toSet() 完成收集。
常用查询方法还包括:
| 方法或属性 | 作用 |
|---|---|
isEmpty、isNotEmpty | 判断集合是否为空 |
contains(value) | 判断是否包含某个元素 |
any(test) | 是否至少有一个元素满足条件 |
every(test) | 是否所有元素都满足条件 |
firstWhere(test) | 查找第一个满足条件的元素 |
fold(initial, combine) | 从初始值开始累计结果 |
在遍历集合期间直接改变集合长度可能抛出 ConcurrentModificationError。需要按条件删除元素时,优先使用集合提供的方法:
void main() {
final scores = <int>[72, 90, 85, 96];
scores.removeWhere((score) => score < 80);
print(scores); // [90, 85, 96]
}
final、const 与不可修改视图
final 限制变量只能赋值一次,不会让变量引用的集合变成不可修改:
void main() {
final topics = <String>['types'];
topics.add('collections'); // 可以修改集合
// topics = ['records']; // 编译错误:不能重新给 final 变量赋值
print(topics);
}
const 集合是编译时常量,不能添加、删除或替换元素:
void main() {
const topics = <String>['types', 'collections'];
// topics.add('records'); // 运行时抛出 UnsupportedError
print(topics);
}
对于运行时已有的数据,可以使用 List.unmodifiable、Set.unmodifiable 或 Map.unmodifiable 创建不可修改的集合:
void main() {
final source = <String>['types'];
final readOnly = List<String>.unmodifiable(source);
source.add('records');
print(readOnly); // [types]
// readOnly.add('collections'); // 运行时抛出 UnsupportedError
}
这些限制是浅层的。如果集合元素本身是可变对象,仍可能通过元素引用修改其内部状态。
集合相等性
Dart 核心库中的 List、Set 和 Map 默认不会按全部内容进行深度比较。两个分别创建但内容相同的集合,使用 == 比较通常会得到 false:
void main() {
final first = <int>[1, 2];
final second = <int>[1, 2];
final alias = first;
print(first == second); // false
print(first == alias); // true
}
需要按内容比较时,应根据业务需要逐项比较,或使用提供对应集合相等性能力的库。不要仅看到元素相同就假定两个集合通过 == 相等。
与 TypeScript 的关键差异
Dart 的 List、Set 和 Map 可以分别类比 TypeScript 中常用的数组、Set 和 Map,但类型检查和字面量语法并不完全相同。
List<String> topics = ['types'];
Set<String> tags = {'dart'};
Map<String, int> scores = {'types': 90};
let topics: string[] = ['types']
let tags: Set<string> = new Set(['dart'])
let scores: Map<string, number> = new Map([['types', 90]])
| 对比项 | Dart | TypeScript |
|---|---|---|
| 列表 | 使用 List<T>,字面量为 [] | 通常使用 T[] 或 Array<T> |
Set 字面量 | 使用 <T>{...} | 需要调用 new Set(...) |
| 键值字面量 | {key: value} 可以创建 Map | {} 创建普通 JavaScript 对象;Map 需要构造 |
| 缺失的键 | map[key] 返回可空值 | Map.get(key) 返回可能为 undefined 的值 |
| 条件元素 | 集合字面量中可直接使用 if 和 for | 通常使用展开、条件表达式或数组方法组合 |
TypeScript 的对象可以承担部分字符串键映射场景,但 Dart 的 Map 是独立集合类型,不应把它理解成 Dart 对象的字面量写法。
常见误区
误区一:把空 {} 当成 Set
空 {} 默认创建 Map<dynamic, dynamic>。空集合应根据用途明确写成 <String>{} 或 <String, int>{}。
误区二:认为 final 会冻结集合
final 只阻止变量重新赋值,集合内容仍可修改。需要常量集合时使用 const,需要运行时不可修改的副本时使用 unmodifiable 构造函数。
误区三:忽略 Map 查询结果可空
Map<K, V> 的 [] 返回 V?。即使 V 不可空,也必须处理键不存在的情况。
误区四:忘记 map 返回 Iterable
map 的结果不是自动生成的 List。API 明确需要列表时,应调用 toList()。
误区五:遍历时修改集合长度
在 for-in 或集合迭代方法运行期间添加、删除元素可能导致并发修改错误。应使用 removeWhere 等专用方法,或先创建副本再修改原集合。
小结
List有顺序、允许重复并支持索引;Set保证元素不重复;Map使用唯一键关联值。- 泛型参数约束元素、键和值的类型,空集合尤其应该明确类型。
- 展开运算符以及集合中的
if、for可以直接组合出新的集合。 where和map返回Iterable,需要具体集合时再调用toList()或toSet()。final不会冻结集合,const和unmodifiable也只提供浅层限制。Map查询可能返回null,核心集合默认也不会按照全部内容进行深度相等比较。