ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Dart List 集合操作全攻略:从增删改查到 Flutter OpenHarmony 避坑指南

Dart List 集合操作全攻略:从增删改查到 Flutter OpenHarmony 避坑指南 在 OpenHarmony 设备上跑 Flutter 项目被中文和集合操作反复折腾之后我决定把 Dart 的 List 单独拉出来写一篇。上一篇日记梳理了变量、函数和流程控制这一篇专门聊集合类型里出场率最高的 List。日常写页面基本离不开列表数据无论是聊天消息、商品列表还是日志记录背后都是 List 在撑。这篇日记会从 List 的创建方式一直拆到增删改查、函数式处理还会带上我踩过的运行时报错和 Flutter for OpenHarmony 场景下的几个小提醒适合刚看完 Dart 基础语法、准备动手写列表功能的朋友。1. 集合类型怎么选为什么日常开发总是先碰到 List1.1 List 在 Dart 集合体系中的定位Dart 的集合类型主要有三大件List、Set、Map。List 是有序可重复的集合每个元素都有固定索引修改、遍历都靠索引打交道Set 强调唯一性适合去重和快速判断是否存在Map 则是键值对结构适合按 key 查数据。实际开发里List 出现的频率远高于另外两者因为服务端返回的列表数据、UI 中渲染的条目数据天然就是有序数组List 可以直接对应上。理解 List 的底层定位有助于你后续理解 Set 和 Map 的差异。List 跟数据源直接绑定比如 JSON 解析出来的数组通常就是 List 再转成 List 。又比如 Flutter 里 ListView.builder 是根据 index 从 List 中取数据来构建 item。可以说掌握 List就等于掌握了 Flutter UI 数据驱动的基础设施。从另一个角度看List 也是理解集合操作的“标准模板”。很多函数式方法比如 map、where、reduce在 Set 和 Iterable 上同样适用。学会 List 的这些方法之后换到其他集合类型几乎是无痛的。这也是我建议先啃 List 的原因。1.2 List 的常见初始化方式与选型思考这几个构造方式开发中都很常见先列一下实际写法// 方式一字面量 var fruits [apple, banana, orange]; // 方式二声明类型后续添加元素 Listint scores []; scores.add(98); // 方式三固定长度列表 var fixed Listint.filled(3, 0, growable: false); // 方式四生成式列表 var squares Listint.generate(10, (index) index * index); // 方式五由其他 Iterable 转换 var copy List.of(scores);使用字面量时Dart 会基于上下文推断元素类型也可以单独指定泛型。开发中我常遇到有人直接把 JSON 中的数组赋值给一个 List 但运行时报类型错误原因就是 List 无法安全转成 List 。此时可以手动做类型转换或者使用 cast 方法但更好的做法是在解析时就逐字段处理。固定长度列表使用 growable: false 创建。固定长度列表不能调用 add、remove只能修改已有元素的值否则会抛 UnsupportedError。它一般用于算法题中需要对核心缓存区限定位数的场景日常业务开发用得少。但理解这个参数有助于排查奇怪的报错。生成式列表特别适合构造“初始有规律数据”的集合比如日历控件生成 31 个格子、排行榜生成 1 到 100 的编号。注意 generate 的第二个参数是生成器函数可以读取当前索引比 for 循环手动 add 更简洁也没有临时变量的副作用。1.3 const List 和 final List 的坑用 const 修饰 List 表示编译期常量整个列表不可变且内容也必须是常量。例如const ListString weekdays [一, 二, 三, 四, 五]; weekdays.add(六); // 编译报错用 final 修饰 List 变量只保证变量本身不能重新赋值但列表内容仍然可以修改final ListString tags [a]; tags.add(b); // 合法tags 列表变成了 [a,b] tags [c]; // 报错final 变量不能重新赋值初学者往往以为加了 final 列表就不能增删了其实不是。真正要限制增删可以使用 List.unmodifiable 或 List.of 的包装形式比如final readOnlyList List.unmodifiable([a, b]); readOnlyList.add(c); // 运行时报错在 OpenHarmony 应用里如果某个列表配置是从本地资源加载且不允许运行时修改使用 List.unmodifiable 更安全。我习惯在封装配置数据时用它能有效防止意外修改共享数据。2. List 的增删改查每一个方法背后的边界条件2.1 增加元素add、addAll、insert、insertAll往 List 里加元素最常用的是 add一次添加一个元素。要批量添加则用 addAll比如把一页接口返回的数据追加到当前列表尾部。insert 可以在指定位置插入单个元素insertAll 可以在指定位置插入一组元素ListString steps [登录, 下载]; steps.add(解压); // [登录, 下载, 解压] steps.addAll([校验, 启动]); // [登录, 下载, 解压, 校验, 启动] steps.insert(0, 准备物料); // [准备物料, 登录, 下载, 解压, 校验, 启动] steps.insertAll(2, [回调, 刷新]); // 索引 2 处插入每次 insert 到头部会让后面的元素整体后移数据量大时会影响性能。如果业务逻辑频繁在头部插入数据建议考虑使用 LinkedList或者先统一 add 再反转又或者直接用 reverse 后的列表。不过日常几十条元素差距可忽略不必过早优化。向固定长度列表 add 会抛出 UnsupportedError。如果你的列表来源是 API 返回的 List.unmodifiable同样不能 add。这类错误我在真机上遇到过主要发生在把“上游不可变列表”直接透传到底层组件时组件内部尝试追加占位数据。解决办法是使用原始列表复制一份可变列表再操作。2.2 删除元素remove、removeAt、removeWhere、clearremove 是按值删除只删除第一个匹配到的元素返回值是布尔值表示是否删掉了某个元素。removeAt 是按索引删除注意索引越界会抛 RangeError。removeLast 直接删除最后一项并返回它。clear 清空整个列表。条件删除最强硬的是 removeWhere接受一个回调函数遍历并移除所有满足条件的元素Listint nums [1, 2, 3, 4, 5, 6]; nums.removeWhere((num) num % 2 0); // 删除偶数剩下 [1,3,5]这样删除有个隐含的性能点Dart 的实现是在一次遍历中完成筛选并重建内部存储比在 for 循环里逐个 removeAt 要高效得多。我早期写过一个反例在循环里调用 removeAt不仅代码复杂还可能出现删错元素的问题// 反例正序遍历删除索引 i会导致后续元素下标前移 for (int i 0; i list.length; i) { if (list[i] target) { list.removeAt(i); } }这种写法会跳过某些元素。比如 [a, b, c]删除 b索引 1后c 落到索引 1而下一次循环 i 变成 2c 就被跳过了。正确做法是使用 removeWhere或者倒序遍历删除。还有一种常见情况只删除满足条件的首个元素。Dart 没有直接的 removeFirstWhere可以先用 indexWhere 找索引再 removeAt或者先 indexOf 再 removeAtint index list.indexWhere((item) item.id targetId); if (index ! -1) { list.removeAt(index); }indexWhere 和 indexOf 返回 -1 表示没找到这一步别漏掉判断。2.3 修改元素索引赋值、fillRange、replaceRange最直接的修改方式是按索引赋值ListString list [a, b, c]; list[1] B; print(list); // [a, B, c]如果要对一批连续索引统一替换可以使用 replaceRange 或者 fillRange。replaceRange 会用另一组元素替换指定区间的元素元素数量不必与原区间一致所以它也能起到“插入删除”的作用Listint nums [1, 2, 3, 4, 5]; nums.replaceRange(1, 3, [20, 30, 40]); // [1, 20, 30, 40, 4, 5]这里替换了索引 1 到 2 的元素不包含索引 3但新列表里有 3 个元素因此整体长度会增加。fillRange 则用同一个值把指定区间填满nums.fillRange(1, 4, 0); // 索引 1 到 3 都变成 0理解范围区间时要记住 Dart 的区间通常是“前闭后开”也就是 start 包含、end 不包含。这个约定在很多 API 里都有搞混了容易多改一个或少改一个元素。2.4 查询元素索引访问、contains、indexOf、firstWhere查询最常用的是方括号索引访问比如 list[0]。开发中常见的越界问题是读取 list[index] 时 index 等于 length需要养成先判断 index 0 index list.length 的习惯。contains 用来判断列表里是否存在某个值内部会调用 比较。对于自定义对象默认比较的是引用如果希望按字段判断需要重写 和 hashCode或者使用 indexWhere/where 做自定义匹配final targetIndex users.indexWhere((user) user.id 101);indexOf 和 lastIndexOf 用于查找某个元素第一次出现和最后一次出现的索引返回 -1 表示未找到。firstWhere 则更灵活可以按条件找到第一个符合要求的元素并支持 orElse 参数final user users.firstWhere( (u) u.id 101, orElse: () User.empty(), );需要注意 firstWhere 如果没有任何匹配且没有提供 orElse会抛 StateError。实际开发中服务端数据经常有空数组这段异常很常见凡是使用 firstWhere 的地方我都建议补 orElse 或先做空列表判断。查询也常配合 every 和 any。every 判断是否所有元素都满足条件any 判断是否存在至少一个满足条件的元素。这两个方法适合做表单校验之类的逻辑比如判断输入列表里是否有一项为空。3. 函数式操作map、where、reduce 怎么用才顺手3.1 map 和 where 是 Flutter 渲染列表的常客函数式操作的核心思路是把“怎么遍历”这个动作交给框架你只需要声明“对每个元素做什么”。map 负责一对一转换返回的还是一个可迭代对象Listint scores [58, 92, 75, 43]; ListString levels scores.map((s) { if (s 90) return 优秀; if (s 60) return 及格; return 不及格; }).toList();这里必须调用 toList 才能得到 List因为 map 返回的是懒求值的 Iterable。理解“懒求值”非常重要——map 并不是立刻执行回调而是当结果被遍历时才逐个计算。如果你忘记 toList直接用索引访问 map 的结果编译器会报错因为 Iterable 没有 [] 运算符。where 是过滤操作把满足条件的元素留到新集合里Listint nums [1, 2, 3, 4, 5, 6]; Listint evens nums.where((num) num % 2 0).toList(); // [2,4,6]Flutter 里经常用 map 把数据模型转成 Widget再用展开运算符放到 Column 或 ListView 中children: users.map((user) UserCard(user: user)).toList()如果只用了 where 而没有 toList传给 children 也不会有问题因为 children 类型就是 List 实际上 map 返回的 Iterable 可以经过 List.of 转换。严格来说这里写 toList 才是稳妥的。3.2 reduce、fold、expand 的进阶玩法reduce 用于把列表聚合成一个值比如求和、求积、拼接字符串。它有约束返回值类型与列表元素类型一致并且当列表为空时会抛错。所以使用 reduce 前必须保证非空或改用 fold。fold 比 reduce 更灵活因为它允许初始值并可以返回不同类型的结果。例如统计一个字符串列表中所有字符的长度总和ListString words [flutter, openharmony, dart]; int totalLength words.fold(0, (sum, word) sum word.length);第一个参数是初始值第二个参数是累加器。fold 在处理空列表时返回初始值不会报错因此更推荐在聚合逻辑里使用。如果你之前没接触过函数式编程可以把 fold 理解成一个“带初始缓冲区的循环”每一步把当前元素合并进缓冲区最后返回缓冲区。expand 可以把集合里的每个元素展开成多个元素ListListint matrix [[1, 2], [3, 4, 5]]; Listint flat matrix.expand((row) row).toList(); // [1,2,3,4,5]嵌套列表转扁平列表时这个方法非常高效。实际开发中比如你要把分类下的所有商品列表拼成一个完整列表用 expand 一行代码就能实现。3.3 链式调用的性能与可读性取舍Dart 函数式方法可以链式组合比如先 where 过滤再 map 转换再 toListListUser adults users .where((u) u.age 18) .map((u) u.copyWith(role: 成年用户)) .toList();链式调用的问题在于每次调用都会产生一个中间 Iterable 对象。数据量只有几十、几百条时影响可以忽略但如果列表上十万条数据每个中间操作都遍历一遍全列表性能就会下降。此时可以改用普通 for 循环在循环里完成过滤和转换或者使用 lazy 的特性把多个操作串起来只在最后 toList 时遍历一次。从可读性角度看链式调用很直观容易看出“先做什么后做什么”。我推荐在数据处理管道中使用链式调用但在 UI 高频刷新路径上尽量避免复杂链式处理把数据提前在逻辑层转换好不要在 build 方法里做全量转换。值得一提的还有集合类型转换。List 转 Set 可以去重ListString tags [a, b, a, c]; ListString uniqueTags tags.toSet().toList();但注意 toSet 会改变顺序吗Dart 的 LinkedHashSet 默认保持插入顺序所以转换后顺序通常不变。如果数据顺序敏感这个特性很重要。4. Flutter for OpenHarmony 开发中的 List 实践与注意点4.1 列表状态更新与 UI 渲染的常见误区在 Flutter for OpenHarmony 项目里我们习惯把列表数据存在 State 中渲染时用 ListView.builder。常见的错误是直接修改列表元素后忘记调用 setState界面没有刷新ListItemModel _items []; void _removeItem(int index) { setState(() { _items.removeAt(index); }); }这里 setState 包住 removeAt 才会触发重建。另一个常见错误是在 builder 中直接操作列表或做耗时转换这会导致滑动卡顿。最好的做法是提前在 setState 中准备好需要渲染的子数据或者使用 const 组件减少重建范围。还有一个特别隐蔽的坑ListView.builder 的 itemCount 和列表长度不一致。比如你清空列表后界面还保留之前的缓存或者 itemCount 固定写死 10但列表只剩 5 条。OpenHarmony 上的 Flutter 引擎对这类问题的表现可能和 Android 端不完全一样最好在更新数据时用 key 强制刷新或者重新构建列表。4.2 空安全与动态类型转换的坑Dart 的空安全约束在 OpenHarmony 开发的 Flutter 项目中同样生效。从 JSON 解析得到的数据类型大多是 dynamic直接赋值给 List 会有类型转换问题。通常我会用 map 配合强制类型转换ListMyModel models jsonList .map((e) MyModel.fromJson(e as MapString, dynamic)) .toList();这里的 as MapString, dynamic 如果拿到的不是 Map 类型运行时会抛 TypeError。服务端返回异常结构时前端直接崩溃。稳妥做法是加一个 whereType 或逐一校验final validModels jsonList.whereTypeMapString, dynamic().map(...).toList();列表元素可空的场景同样常见比如 ListString? 需要过滤空值。不要直接 removeWhere((s) s null)可以用 whereType () 一步完成ListString nonNull nullableList.whereTypeString().toList();在 OpenHarmony 设备上调试时Dart 层报错信息会出现在 Flutter 日志里日志标签往往和 Android/iOS 稍有差异。如果遇到红色报错先看错误类型比如 RangeError、TypeError、UnsupportedError再定位到对应行。4.3 用 List 换算组件的几种性能小技巧数据量特别大或需要频繁刷新列表时replaceRange 比多次 add/remove 更可控。比如下拉刷新后用整个新列表替换旧列表最直接的是_items ..clear() ..addAll(newList);这样会先清空再加入触发一次重建。如果要追求更平滑的 UI可以比较新旧列表差异用 insertAll/removeRange 精细更新但代码复杂度会上升。我在实际项目中通常先做列表长度判断如果差异太大就直接全量替换差异小就局部更新。另一个技巧是使用 const List 作为固定下拉选项。比如性别选项、学历选项这类不变的数据声明成 const 列表可以减少运行时创建对象数量对应用启动性能有一点帮助。同时因为 const 列表不可修改也不会发生被意外篡改的问题。还有一个细节不要在 build 方法里反复调用 toList 或生成新的列表对象。列表对象每次变化都会导致 Flutter 认为数据源变了触发重建。如果列表数据本质上没变化应该用相同引用或做缓存判断。5. 常见运行时报错与排查速查表5.1 我实际碰到的几个典型报错先记录一个最常见的运行时报错UnsupportedError: Unsupported operation: add。这个错误通常是用了固定长度列表或不可变列表然后调用 add。排查思路是先打印 list.runtimeType确认它的类型是 _FixedLengthList 还是 _UnmodifiableList再做可变列表复制var modifiableList List.of(fixedList);还有 StateError: No element这个错误大多来自 firstWhere 在空列表中查找或者 reduce 处理空列表。加 orElse 能解决 firstWhere 一类的问题reduce 则建议换成 fold配合空列表返回初始值。Concurrent modification error 是另一个坑。在遍历 List 的同时修改 List比如for (var item in list) { if (item target) { list.remove(item); } }Dart 的 for-in 遍历基于迭代器遍历过程中调用 remove 会触发 ConcurrentModificationError。解决办法是使用 removeWhere或者先在 for 循环里找出需要删除的项收集起来循环结束后统一删除final toRemove String[]; for (var item in list) { if (item target) toRemove.add(item); } list.removeWhere((item) toRemove.contains(item));其实 Dart 支持在集合字面量里完成这个操作比如使用 where 生成新列表再替换引用同样可以避免并发修改问题。5.2 List 相关异常问题速查表下面按错误类型、常见场景、解决方向整理一份速查表方便排查时直接对照。错误类型典型触发场景解决方向RangeError索引越界比如访问 list[list.length]先判断 index 是否在有效范围内UnsupportedError对固定/不可变列表调用 add、remove用 List.of 转成可变列表再操作StateErrorfirstWhere 无匹配且无 orElse、reduce 空列表补 orElse或改用 foldTypeError把 List 当成 List 使用解析 JSON 时逐项转换ConcurrentModificationError遍历列表时同步修改列表用 removeWhere 或收集后统一删除Unhandled Exception异步加载列表后未判空直接读取长度列表变量声明时给默认空列表这张表我贴在项目文档里了。每次报错后先查表定位类型比读半天堆栈效率高。5.3 排查技巧善用 debugPrint 打印列表OpenHarmony 设备上打印 List 内容需要注意日志长度。使用 print 输出长列表时日志系统可能截断可以用 debugPrint 替代它会根据平台自动分段打印。对于复杂的模型列表重写 toString 方法能极大方便调试class User { final int id; final String name; override String toString() User(id: $id, name: $name); }这样 debugPrint(users) 就能直接看到每个用户的 id 和 name不用再手动拼接。我个人调试列表逻辑时会临时写一些辅助函数专门打印列表长度、首元素、末元素快速判断数据源是否异常。6. 实用操作心得操作列表中我最有体会的一点优先使用不可变列表约束共享数据只在需要修改的边界处创建可变副本。Dart 的 List API 虽然简单但组合出的运行时错误五花八门新手很容易栽在索引越界和遍历删除上。另一个经验是凡是函数式链式操作最终都要想清楚结果类型。map 和 where 返回 Iterable不是 Listreduce 和 fold 返回聚合值不是列表expand 返回扁平 Iterable往往还需要 toList 才能继续使用。这一节写下来我自己也把之前随手写的 toList 调用梳理了一遍发现不少冗余转换可以让它更精简。对于要学 Flutter for OpenHarmony 的朋友建议从今天开始把 List 操作分成“查询型”和“修改型”两类来看。查询型操作尽量用 map、where、firstWhere 等函数式方法代码可读性好修改型操作则看清列表可变性多留意边界条件。对这些细节的掌握程度往往决定了你写列表代码时是从容还是不断救火。
返回列表