跳到主要内容

Collections(集合)

Dart 使用集合保存一组相关数据。最常用的集合类型是 ListSetMapList 按顺序保存元素,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]
}

集合字面量中可以直接使用 iffor,这种写法称为集合控制流。它们创建元素,不是先创建集合再进行修改。

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 字面量也支持展开、iffor

void main() {
const baseScores = {'types': 90};
const extraTopics = ['records', 'collections'];

final scores = <String, int>{
...baseScores,
for (final topic in extraTopics) topic: 95,
};

print(scores);
}

遍历、筛选与转换

ListSetMap 都可以用 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]
}

wheremap 返回 Iterable,计算通常会在迭代时才发生。需要独立的列表或集合时,调用 toList()toSet() 完成收集。

常用查询方法还包括:

方法或属性作用
isEmptyisNotEmpty判断集合是否为空
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]
}

finalconst 与不可修改视图

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.unmodifiableSet.unmodifiableMap.unmodifiable 创建不可修改的集合:

void main() {
final source = <String>['types'];
final readOnly = List<String>.unmodifiable(source);

source.add('records');
print(readOnly); // [types]

// readOnly.add('collections'); // 运行时抛出 UnsupportedError
}

这些限制是浅层的。如果集合元素本身是可变对象,仍可能通过元素引用修改其内部状态。

集合相等性

Dart 核心库中的 ListSetMap 默认不会按全部内容进行深度比较。两个分别创建但内容相同的集合,使用 == 比较通常会得到 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 的 ListSetMap 可以分别类比 TypeScript 中常用的数组、SetMap,但类型检查和字面量语法并不完全相同。

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]])
对比项DartTypeScript
列表使用 List<T>,字面量为 []通常使用 T[]Array<T>
Set 字面量使用 <T>{...}需要调用 new Set(...)
键值字面量{key: value} 可以创建 Map{} 创建普通 JavaScript 对象;Map 需要构造
缺失的键map[key] 返回可空值Map.get(key) 返回可能为 undefined 的值
条件元素集合字面量中可直接使用 iffor通常使用展开、条件表达式或数组方法组合

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 使用唯一键关联值。
  • 泛型参数约束元素、键和值的类型,空集合尤其应该明确类型。
  • 展开运算符以及集合中的 iffor 可以直接组合出新的集合。
  • wheremap 返回 Iterable,需要具体集合时再调用 toList()toSet()
  • final 不会冻结集合,constunmodifiable 也只提供浅层限制。
  • Map 查询可能返回 null,核心集合默认也不会按照全部内容进行深度相等比较。