3.1 List、Set、Map 集合操作实战


3.1 List、Set、Map 集合操作实战

本节摘要:List有序可重复,Set无序去重并自带集合运算,Map按键存取。本节用拾光清单的真实数据把三种容器的高频操作过一遍,重点讲清"按访问模式选容器"的判断方法与常见误用。

学习目标:阅读完本节,你应当能够:为去重、排序、按键查询三类需求各选出正确的容器;熟练使用List切片、Set运算、Map遍历;识别"平行列表对齐"这类结构坏味道并给出替代方案。

数据装进容器之后

上一章的任务还是零散变量。真正的应用里,任务从来成批出现:今日清单是列表,标签池是集合,标签到预算的分配是映射。容器选对,后面的加工就顺;选错,处处别扭。判断依据只有一条——你的访问模式是什么。按位置访问、保持顺序,用List;只关心"有没有"、频繁查重,用Set;按键取值、按键分组,用Map。

一、List:顺序是它的一切

List的日常操作围绕"位置"展开。增删改查之外,切片与拼接也高频出现:

void main() { var today = ['整理书桌', '回复邮件', '采购水果']; today.insert(0, '晨间冥想'); // 指定位置插入 today.remove('回复邮件'); // 按值删除第一个匹配 today.removeLast(); // 删末尾 print(today); // [晨间冥想, 整理书桌] var urgent = ['处理线上告警']; var merged = [...urgent, ...today]; // 展开拼接,不改原列表 print(merged); var topTwo = merged.take(2).toList(); // 前 n 个 var rest = merged.skip(1).toList(); // 跳过 n 个 print('置顶: $topTwo 其余: $rest'); var sorted = [...merged]..sort(); // 拷贝再排序,原列表不动 print(sorted); }

三个细节值得划线。其一,展开操作符...把一个列表拆开并入另一个,构造新列表时极顺手;配合空感知展开...?,源列表可能为null也不报错。其二,take与skip返回的是惰性序列,链上接toList才落地——这个特性第二章讲过类型,第三章末尾讲性能。其三,sort会原地排序,想保留原顺序就先拷贝,级联..sort()让拷贝与排序连成一句。

排序自定义比较器是必备技能:sort的参数是接收两个元素的函数,返回负数表示前者应排前面。按任务耗时从长到短排:

class Item { final String title; final int minutes; Item(this.title, this.minutes); @override String toString() => '$title($minutes)'; } void main() { var items = [ Item('整理书桌', 20), Item('深度工作', 90), Item('回复邮件', 15), ]; items.sort((a, b) => b.minutes.compareTo(a.minutes)); // 降序 print(items); // [深度工作(90), 整理书桌(20), 回复邮件(15)] }

b.minutes.compareTo(a.minutes)这个"反着比"的写法是降序的惯用法,升序则把a与b对调。compareTo返回三态:负、零、正,正好承载"小于、等于、大于"。

二、Set:去重与集合运算

Set最常用的场景有两个:剔除重复、判断归属。Dart的Set还提供完整的集合运算,标签交叉分析这类需求一行解决:

void main() { var workTags = {'工作', '邮件', '会议'}; var lifeTags = {'生活', '采购', '邮件'}; // 去重:List 转 Set 再转回 var raw = ['工作', '生活', '工作', '生活', '学习']; var distinct = raw.toSet().toList(); print(distinct); // [工作, 生活, 学习] // 交集:两个领域共有的标签 print(workTags.intersection(lifeTags)); // {邮件} // 并集与差集 print(workTags.union(lifeTags)); print(workTags.difference(lifeTags)); // {工作, 会议} // 归属判断:Set 的 contains 是常数时间 print(lifeTags.contains('采购')); // true }

归属判断的性能差异是List与Set的分水岭:List的contains从头扫到尾,元素越多越慢;Set基于哈希,查询时间与元素数量基本无关。需要反复判断"在不在",哪怕只有几千个元素,也值得换成Set。

三、Map:按键的世界

Map的核心能力是按键存取,高频配套还有:keys与values取视图、forEach遍历键值、putIfAbsent缺才写入。工程里最常见的进阶用法是"分组统计"——按某个键把列表归堆:

void main() { var costs = {'整理书桌': 20, '深度工作': 90, '回复邮件': 15}; costs.forEach((title, minutes) { print('$title 预计 $minutes 分钟'); }); // 分组:按首字归类(简化示意) var words = ['整理', '回复', '采购', '打豆豆']; var groups = <String, List<String>>{}; for (var w in words) { var key = w.substring(0, 1); groups.putIfAbsent(key, () => []).add(w); } print(groups); // {整: [整理], 回: [回复], 采: [采购], 打: [打豆豆]} }

putIfAbsent(key, () => []).add(w)是分组的惯用句式:键不存在时先挂一个空列表,再往里追加。读熟这个句式,任何"按X分组"的需求都能直接落笔。

三种容器的选型对比

三种容器的选型对比

避坑提醒:平行列表是结构坏味道的重灾区。用titles与dones两个列表靠下标对齐,一旦中途插入删除,对齐就静悄悄错位,而且没有任何编译期能拦。正确姿势是把关联字段收进一个任务对象(第四章建模),或至少收成Map。本部分早期示例沿用平行列表只为聚焦语法,工程代码请勿模仿。

关键心法:容器选型是数据结构的局部决策,却影响全局代码形态。写下容器声明前先问一句:这批数据最频繁的操作是什么?答案指向哪种容器,就用哪种。

本节要点回顾

  • 访问模式决定容器:按位用List、查重用Set、按键用Map。
  • 展开操作符构造新列表...?连null都不怕;sort原地生效,保序先拷贝。
  • 降序惯用反向比较:b比a,升序对调。
  • Set的contains是常数时间:高频归属判断别用List硬扫。
  • putIfAbsent加add是分组句式:读熟它,按X分组张口就来。

容器操作熟了,但满屏for循环仍然啰嗦。下一节把循环升级成流水线——map、where、fold三件套登场,代码将开始直接说业务话。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U