ARTICLE DETAIL

资讯详情

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

鸿蒙ArkTS中Map和Set:灵活数据存储与查找实战指南

鸿蒙ArkTS中Map和Set:灵活数据存储与查找实战指南 做鸿蒙应用开发久了我有个很深的体会很多零基础的同学卡住的地方往往不是语法多难而是不知道“什么场景该用什么数据结构”。数组一把梭到底遇到查找、去重、一一对应的场景写出来的代码又慢又绕自己看着都难受。第十九节想聊的Map和Set就是专门解决这类问题的。鸿蒙应用开发里的数据存储除了基础的变量、数组、对象之外Map和Set是你迟早要解锁的两个灵活武器。这堂课不讲虚的直接上原理、上代码、上实战新手也能拿来就用。Map和Set在鸿蒙的ArkTS生态里用起来其实和TypeScript非常接近但有自己的一些类型和响应式细节需要注意。这篇博客会从最基础的概念讲起配合真实可运行的代码场景把“为什么用它”“怎么用它”“什么时候别用它”一次说清楚。1. 为什么说Map和Set是“灵活数据存储”的关键1.1 数组和对象解决不了的问题做应用开发时最常见的数据存储需求无非三种按顺序存一堆东西、按名字取一个东西、快速判断某个东西存不存在。数组擅长第一种对象勉强能处理第二种但越用越别扭。先说说数组的尴尬。假设你在鸿蒙应用里维护一份联系人列表每来一个联系人要判断“这个人是不是已经在列表里了”。用数组找通常就是从头到尾遍历一遍数据量小还好几百上千条之后明显卡顿。更要命的是如果需求变成“根据联系人ID直接拿出手机号”用数组你得先循环找ID再取对应数据代码绕了一大圈维护起来也费劲。对象的局限则体现在“键”上。普通对象的键会被强制转成字符串你想存一个数字类型的ID取出来时发现变成了字符串类型判断直接翻车。还有原型链的坑创建对象时自带一堆继承属性做hasOwnProperty判断时容易误伤。频繁增删键值对的时候对象的性能表现也不够稳定。Map和Set就是为解决这些场景而生的。Map适合做“键-值”对应Set适合做“唯一值集合”。它们都不是替代数组和对象而是补上那两块短板让数据存储更灵活、更高效。1.2 它们和普通容器的核心区别为了更直观地对比我把常用数据结构的特点整理成了表格数据结构适用场景查找速度键的限制顺序保证数组有序列表、需要索引访问慢需遍历数字索引有普通对象简单键值对、配置项快仅字符串或Symbol无保证Map任意类型键的键值对很快按哈希定位任意类型按插入顺序Set去重、存在性判断很快按哈希定位任意类型按插入顺序举个例子Map就像你手机里的“通讯录”每个人名对应一个号码直接翻到名字就能找到号码不用从头把整个通讯录念一遍。Set更像“打卡机”每个工号刷一次重复刷卡不会记两次想知道这个人今天来没来过一查就知道。实际开发中这两个结构经常会互相配合用。比如先用Set记录用户已收藏的商品ID再用Map保存商品ID到商品详情的映射操作起来非常顺手。这也是为什么说它们能“解锁灵活数据存储”的关键所在。2. 先玩透Map键值对的万能工具箱2.1 创建、写入、读取的完整套路在鸿蒙ArkTS里创建一个Map非常简单。和TypeScript一样你可以给它标注键和值的类型这样写起来更安全也减少了运行时出错的概率。我强烈建议新手从一开始就养成写泛型类型的习惯别偷懒写一个裸的new Map()。// 创建一个账号ID到用户名的映射 let userMap: Mapstring, string new Map(); // 写入数据 userMap.set(u1001, 张三); userMap.set(u1002, 李四); // 读取数据如果不存在会返回undefined let name: string | undefined userMap.get(u1001); console.log(用户名 name); // 输出用户名张三 // 判断某个键是否存在 if (userMap.has(u1003)) { console.log(该用户存在); } else { console.log(该用户不存在); } // 获取当前有多少条数据 console.log(用户总数 userMap.size);注意几个小细节第一size是属性不是方法别写成size()。第二get方法取不到值时返回undefined这在TypeScript严格模式下会提醒你处理空值。第三set方法调用后会返回Map本身所以你还可以链式写let fruitMap: Mapstring, number new Map(); fruitMap.set(apple, 3) .set(banana, 2) .set(orange, 5);这种链式写法在连续添加多个值时很清爽代码也显得干净利落。删除和清空是我在实际开发里经常用到的操作。删除单个键用delete(key)如果键不存在会静默失败返回false。清空全部则用clear()。有一点要特别提醒Map的键比较使用的是“严格相等”逻辑对于基本类型字符串、数字、布尔值没问题但如果键是对象两个内容一样的对象不会被当成同一个键因为它们的引用地址不同。这一点非常容易踩坑后面我会专门展开讲。2.2 遍历的三种方式与应用技巧Map的遍历方式不止一种选对了场景代码能简洁不少。最通用的是forEach它会把值、键、Map本身都作为参数传进回调函数let cityMap: Mapstring, string new Map(); cityMap.set(bj, 北京); cityMap.set(sh, 上海); cityMap.set(gz, 广州); cityMap.forEach((value: string, key: string) { console.log($ {key} - $ {value}); });看到这里可能有同学会问回调参数为什么是“值在前、键在后”这是Map的forEach设计如此刚开始容易记反我建议干脆背下来第一个参数是值第二个参数是键。如果不想用forEach也可以用for...of配合解构来遍历这种写法在需要中途跳出循环时更方便for (let [key, value] of cityMap) { if (key sh) { console.log(找到了上海停止遍历); break; } }不过要留意ArkTS的某些严格模式或低版本API下直接对Map做for...of迭代可能需要编译器支持。我在鸿蒙开发中更推荐先用Array.from(cityMap.entries())把Map转成数组再遍历兼容性更稳代码可读性也不错。另一个很实用的技巧是只遍历键或只遍历值let keys: string[] Array.from(cityMap.keys()); let values: string[] Array.from(cityMap.values()); console.log(keys); // [bj, sh, gz] console.log(values); // [北京, 上海, 广州]把键单独取出来组成数组在写下拉筛选器、标签列表时非常方便。2.3 Map的类型约束与动态更新鸿蒙的ArkTS对类型要求比纯TypeScript更严格所以给Map标注泛型几乎是必须的。假如你初始化时不写类型直接往里塞不同类型的值在编译阶段就很容易报错。这也是零基础同学从JavaScript思维转过来时最不适应的点之一。// 这样写可能会报错因为类型推断不一致 let dataMap new Map(); dataMap.set(score, 98); dataMap.set(name, Tom);上面的写法在JS里没问题但在ArkTS里Map的类型一旦被推断后续插入不同类型的值就可能触发类型错误。正确做法是在声明时就明确类型let dataMap: Mapstring, number | string new Map(); dataMap.set(score, 98); dataMap.set(name, Tom);这一条对新手来说很重要写代码的时候先想清楚每类数据长什么样再决定泛型怎么写能省掉后面一大串排查时间。如果你需要在界面状态里动态更新Map还有一个“响应式”的坑要注意。Map本身的变化不会自动触发UI重新渲染因为ArkUI的状态管理默认监视的是变量引用变化而不是Map内部结构变化。想触发页面刷新常用的做法是“先拷贝、再修改、最后重新赋值”State userMap: Mapstring, string new Map(); function updateUser(key: string, newName: string) { let newMap: Mapstring, string new Map(this.userMap); newMap.set(key, newName); this.userMap newMap; // 重新赋值触发UI渲染 }这种方式虽然多了一次拷贝但对绝大多数应用场景来说性能完全够用而且逻辑清晰、不易出错。3. Set的威力去重和存在性判断一网打尽3.1 快速去重不用手写循环Set的核心特性就是“唯一性”。往同一个Set里重复添加同一个值时最终只会保留一份。这个特性用于数组去重简直是降维打击。新手可能会写这样的代码去重遍历数组每拿到一个元素先判断结果数组里有没有没有才push进去。逻辑没问题但代码冗长而且效率低。用Set一行就搞定了let ids: number[] [1, 2, 3, 2, 1, 4, 5, 4]; let uniqueIds: number[] Array.from(new Set(ids)); console.log(uniqueIds); // [1, 2, 3, 4, 5]这里用Array.from把Set转回数组简洁又清晰。如果你在ArkTS中遇到数组展开符...new Set(ids)支持不稳定Array.from是最稳妥的替代方案。Set的去重基于严格相等SameValueZero所以在基本类型上表现非常稳定。但如果去重的对象内容相同而引用不同Set会认为是两个不同元素这点和Map的键判断是一致的。也就是说对象数组的去重不能依赖Set直接完成需要配合序列化或其他手段。3.2 存在性判断Set比数组快得多判断“某个值是否已经存在”是开发里特别高频的需求。比如用户输入了一个搜索关键词你希望历史记录里同一个词只展示一次同时还能快速判断这次搜索是不是新的。用Set的has方法时间复杂度是O(1)即使历史记录里有一万条每次判断也只需要一次哈希定位。而用数组的includes则要遍历整个数组数据一多差异就很明显。let historySet: Setstring new Set(); historySet.add(鸿蒙); historySet.add(ArkTS); historySet.add(状态管理); // 判断搜索词是否已经存在 function isNewKeyword(keyword: string): boolean { return !historySet.has(keyword); } console.log(isNewKeyword(鸿蒙)); // false console.log(isNewKeyword(地图)); // true这种写法相比数组查找代码意图也表达得更直接。别人读你的代码时一眼就能看出“这里维护的是一个唯一集合”而不是“一个可能重复的列表”。3.3 集合运算并集、交集、差集Set除了去重还能很方便地做集合运算。假设一个电商应用里你有一批“营销活动商品ID”和一批“热销商品ID”想找出既参加活动又热销的商品就能用交集let activityIds: Setnumber new Set([101, 102, 103, 104]); let hotIds: Setnumber new Set([103, 104, 105, 106]); // 交集既在活动商品里又是热销商品 let intersection: number[] Array.from(activityIds).filter((id) hotIds.has(id)); console.log(intersection); // [103, 104] // 并集所有参加活动或热销的商品 let union: number[] Array.from(new Set([...Array.from(activityIds), ...Array.from(hotIds)])); console.log(union); // [101, 102, 103, 104, 105, 106] // 差集只在活动商品里但不在热销商品里 let difference: number[] Array.from(activityIds).filter((id) !hotIds.has(id)); console.log(difference); // [101, 102]可能有同学看到并集的写法觉得有点绕因为要先把两个Set转数组再合并、再去重。这在ArkTS里是兼容性最好、也最容易理解的方式。如果你更习惯函数式写法也可以直接用forEach手动添加效果一样let unionSet: Setnumber new Set(); activityIds.forEach((id) unionSet.add(id)); hotIds.forEach((id) unionSet.add(id));这样写虽然行数多了一点但每一步都很直观新手阅读起来压力小。集合运算在实际业务中非常常用比如权限比对、标签筛选、黑白名单等掌握之后会特别省事。4. 在鸿蒙应用里实战Map/Set的综合应用4.1 场景一用Map管理商品信息与购物车光讲API很枯燥我们直接放到一个鸿蒙应用场景里。假设你在做一个简单的购物车页面商品ID是唯一的需要根据ID快速拿到商品名称、价格、数量。用Map再合适不过。interface Goods { id: number; name: string; price: number; count: number; } Entry Component struct ShoppingCart { State goodsMap: Mapnumber, Goods new Map(); aboutToAppear(): void { // 模拟初始化数据 const goodsList: Goods[] [ { id: 1, name: 鸿蒙编程入门, price: 49, count: 1 }, { id: 2, name: 智能手环, price: 199, count: 2 }, { id: 3, name: 蓝牙耳机, price: 299, count: 1 } ]; goodsList.forEach((item: Goods) { this.goodsMap.set(item.id, item); }); } increaseCount(id: number): void { let newMap: Mapnumber, Goods new Map(this.goodsMap); const goods: Goods | undefined newMap.get(id); if (goods) { goods.count 1; newMap.set(id, goods); this.goodsMap newMap; // 重新赋值触发UI更新 } } getTotalPrice(): number { let total: number 0; this.goodsMap.forEach((goods: Goods) { total goods.price * goods.count; }); return total; } build() { Column() { Text(购物车商品总数 this.goodsMap.size) .fontSize(16) .margin(10) this.goodsMap.forEach((goods: Goods) { Row() { Text(goods.name) .layoutWeight(1) Text( goods.price * goods.count) Button() .onClick(() this.increaseCount(goods.id)) } .width(100%) .padding(10) }) Text(总价 this.getTotalPrice()) .fontSize(20) .fontWeight(FontWeight.Bold) .margin(20) } .width(100%) .padding(10) } }这段代码里有几个非常值得注意的点。第一State goodsMap的类型是Mapnumber, Goods页面显示的数据量由Map管理每个商品ID对应一个商品对象查找和更新都很顺手。第二increaseCount方法里没有直接改this.goodsMap而是先拷贝了一份再修改最后重新赋值因为这样UI才会感知到变化。第三在build里用forEach渲染列表虽然界面逻辑简单但你已经体会到Map在“ID到实体”映射上的优势了——不需要维护数组下标增删商品都不会错位。如果你有删除商品的需求也很简单deleteGoods(id: number): void { let newMap: Mapnumber, Goods new Map(this.goodsMap); newMap.delete(id); this.goodsMap newMap; }这样删除后UI自动刷新其他商品ID并不受影响这在数组场景里需要额外处理索引变化Map实现起来省心不少。4.2 场景二用Set记录已收藏文章再来看一个实际又贴近生活的例子资讯类App的文章收藏功能。用户可以收藏多篇文章同一篇文章不能重复收藏还要能快速判断某篇文章是否已收藏。用Set就很贴切。Entry Component struct ArticleList { State favoriteSet: Setnumber new Set(); toggleFavorite(articleId: number): void { let newSet: Setnumber new Set(this.favoriteSet); if (newSet.has(articleId)) { newSet.delete(articleId); } else { newSet.add(articleId); } this.favoriteSet newSet; // 触发UI更新 } build() { Column() { Text(已收藏 this.favoriteSet.size 篇文章) .fontSize(16) .margin(10) List() { ForEach([101, 102, 103, 104, 105], (id: number) { ListItem() { Row() { Text(文章 id) .layoutWeight(1) Text(this.favoriteSet.has(id) ? 已收藏 : 收藏) .onClick(() this.toggleFavorite(id)) } .padding(10) } }, (id: number) id.toString()) } } .width(100%) } }这里用Set保存“已收藏文章ID”天然去重再也不用担心重复收藏的问题。同时界面判断某篇文章的状态只需调用has快速又直观。如果你还想在本地持久化保存这些收藏数据记得Set不能直接被序列化成JSON。我通常的做法是把Set转成数组存到Preferences或AppStorage里let favoriteArray: number[] Array.from(this.favoriteSet); // 保存favoriteArray到本地存储 // 读取时再把数组转回Set let loadedSet: Setnumber new Set(favoriteArray);这一招在开发中是必备的否则直接JSON.stringify一个Set得到的是{}数据全丢排查时还很难发现。4.3 Map/Set结合使用标签筛选系统真正熟练之后你会发现Map和Set经常组合出现。比如一个笔记应用每篇笔记可以有多个标签。Set适合表示“标签集合”Map适合表示“标签ID到标签详情的映射”。interface TagInfo { id: number; name: string; color: string; } // 标签ID到标签信息的映射 let tagInfoMap: Mapnumber, TagInfo new Map(); tagInfoMap.set(1, { id: 1, name: 生活, color: #FF0000 }); tagInfoMap.set(2, { id: 2, name: 工作, color: #00FF00 }); tagInfoMap.set(3, { id: 3, name: 学习, color: #0000FF }); // 某篇笔记拥有的标签ID集合 let noteTagIds: Setnumber new Set([1, 3]); // 渲染某篇笔记的标签 function renderTags(): string[] { let tagNames: string[] []; noteTagIds.forEach((id: number) { const tag: TagInfo | undefined tagInfoMap.get(id); if (tag) { tagNames.push(tag.name); } }); return tagNames; } console.log(renderTags()); // [生活, 学习]这个组合的好处是Set负责快速判断“是否有某个标签”Map负责通过ID拿到标签的完整信息。多层数据关联时这种写法非常清晰比在数组里反复find要高效得多也更容易维护。5. 零基础最容易踩的坑和排查技巧5.1 类型标注不严导致编译失败ArkTS和TypeScript一样强调静态类型。如果你写let map new Map();后往里塞不同类型的值轻则编辑器提示重则编译失败。我见过太多新手在这里卡住。排查技巧看到Type string is not assignable to type number这类报错先看看Map的泛型是怎么写的。如果不想让类型太死板就明确写成联合类型比如Mapstring, string | number。千万别为了省事把所有类型都标成any那样以后维护起来会非常痛苦。5.2 Map/Set不自动触发UI刷新前面反复提到修改Map或Set内部的元素不会触发ArkUI的重新渲染。这是新手最容易忽略的问题。典型的错误写法是这样this.userMap.set(u1001, 王五); // 直接修改UI不会更新正确做法是“拷贝-修改-赋值”三步走。虽然多写了几行代码但保证UI同步排查逻辑时也更容易理清数据流。实际项目里如果数据量很大、频繁更新可以考虑用更高效的状态管理方案但零基础阶段还是先把这种模式练熟。5.3 JSON不支持序列化Map/Set如果你尝试JSON.stringify(new Map())得到的结果会是{}。Set同样如此。这在持久化存储、网络传输时是个大坑。解决方案也很明确转换后再序列化。Map可以转成数组每个元素是[key, value]的二元组let mapArray: [number, string][] Array.from(userMap.entries()); let jsonStr: string JSON.stringify(mapArray); console.log(jsonStr); // [[u1001,张三],[u1002,李四]] // 从数组恢复Map let restoredMap: Mapnumber, string new Map(mapArray as [number, string][]);Set转数组前面已经讲过直接用Array.from。这算是Map/Set最统一的“落地”方式建议当成固定套路来用。5.4 forEach里删除元素容易意外跳过在遍历Map或Set时如果你在回调里删除了当前元素某些情况下后续元素会被跳过导致结果不符合预期。这种问题很难一眼看出来因为不是所有场景都会触发。安全做法是先收集要删除的键/值遍历结束后再统一删除。let deleteKeys: string[] []; userMap.forEach((value: string, key: string) { if (value 临时用户) { deleteKeys.push(key); } }); deleteKeys.forEach((key: string) { userMap.delete(key); });这套“先标记后删除”的方法同样适用于数组和其他集合容器是处理遍历时修改数据的通用安全策略。5.5 对象当键时按引用比较如果你用对象作为Map的键或者向Set添加对象判断是否相同时Set/Map比较的是对象引用而不是对象内容。两个结构完全相同的对象只要不是同一个引用就会被当成两个不同的键。interface Point { x: number; y: number; } let pointSet: SetPoint new Set(); let p1: Point { x: 1, y: 2 }; let p2: Point { x: 1, y: 2 }; pointSet.add(p1); pointSet.add(p2); console.log(pointSet.size); // 2因为p1和p2引用不同如果你需要按内容去重比如坐标点去重最简单的方式是先把对象序列化成字符串再存let pointSet: Setstring new Set(); pointSet.add(JSON.stringify(p1)); pointSet.add(JSON.stringify(p2)); console.log(pointSet.size); // 1使用时要权衡一下对象引用比较符合“内存层面的唯一性”适合保存实体本身字符串化比较适合“内容层面的唯一性”适合去重判断。两个方向没有绝对优劣看业务需求选择就好。5.6 速查表Map和Set常用API对照把最常用的操作整理成一张表方便你随时查阅操作Map方法Set方法说明添加元素set(key, value)add(value)已存在时更新/不重复添加读取元素get(key)无Set直接has判断判断存在has(key)has(value)都是O(1)删除元素delete(key)delete(value)不存在时返回false清空clear()clear()删除所有元素获取数量sizesize别忘了是属性遍历forEach / entries / keys / valuesforEach / keys / values注意参数顺序转成数组Array.from(map.entries())Array.from(set)持久化常用看到没Map和Set的API设计高度对称只要把Map常用的set、get、has、delete、size掌握Set的add、has、delete、size几乎是顺手就会完全零负担。从实际开发角度给你的一点建议最后再分享一个我自己的习惯写代码之前先花十秒钟想一想数据结构。数组不是唯一选择对象也不是万能的。看到“一一对应”优先考虑Map看到“唯一且需要快速判断存在”优先考虑Set。这样写出来的代码不仅性能更好逻辑也更清晰。Map/Set的价值不是让你完全抛弃数组和对象而是在合适的场景里用更顺手的工具。零基础阶段把这些结构熟悉之后后面接触复杂状态管理、本地数据库、网络缓存时思路会开阔很多。实操中哪怕一开始用不熟练多写几个小页面很快就形成肌肉记忆了。祝你在鸿蒙应用开发的路上越走越顺。
返回列表