# 鸿蒙 ArkTS 实战:秒表 Stopwatch(示例 5)

在鸿蒙应用开发中,凡是需要"随时间变化"的界面,几乎都绕不开定时器、时间戳与状态管理这三件事的组合。秒表(Stopwatch)正是这样一个把三者结合得恰到好处的经典例子:它不需要网络请求,不需要复杂的数据结构,核心逻辑只有"计时、暂停、计次、重置"四个动作,却能把 ArkTS 中最重要的几个机制一一串起来。

1. 应用概述与功能

index5.ets 定义了一个名为 Index5 的组件,通过 @Entry 标记为页面入口,整体是一个功能完整的秒表应用。页面主要包含以下几块内容:顶部是返回栏,包含一个"返回"按钮(调用 router.back() 回到上一页)和页面标题"秒表";页面中央是字号 64 的大号时间显示,初始值为 00:00.00,下方跟随一行小字状态提示,计时进行中显示"计时中",暂停或未开始时显示"已暂停";再往下是三个控制按钮,分别是"开始/暂停"(根据运行状态动态切换文案与背景色)、"计次"和"重置";页面下半部分是一个带标题的计次记录列表,标题会实时显示记录条数,例如"计次记录 (3)"。

从功能角度看,这个秒表实现了四类核心操作。其一是开始计时,点击"开始"后,时间数字会以厘秒(百分之一秒)为单位持续跳动,并在 30 毫秒的间隔内刷新显示;其二是暂停计时,点击"暂停"后时间停在当前值,再次点击"开始"会从暂停处继续累积,而不是从头开始;其三是计次,在计时进行中可以随时点击"计次",把当前显示的时间追加到列表尾部,列表项带有两位数的序号补零(如 01. 00:05.23);其四是重置,点击"重置"后时间归零、计次列表清空、序号复位,回到初始状态。

值得注意的是,这个示例在"开始"与"暂停"之间使用了往返切换:同一个按钮在两种状态下分别承担"开始"和"暂停"的职责,文案、背景色都随状态变化,这是 ArkTS 中"状态驱动 UI"非常直观的体现。

从工程结构上看,@Entry 装饰器把 Index5 标记为可独立启动的页面入口,@Component 则声明它是一个 ArkUI 组件,二者组合是鸿蒙页面组件最常用的写法。示例中 index5.etsindex6.ets 等页面放在 entry/src/main/ets/pages/ 目录下,由入口页面通过路由跳转进入;本页面负责"返回"到列表页,而列表页再通过 router.pushUrl 携带参数进入各个示例。理解了页面的组织方式,再回头看 Index5 内部,会发现一个页面组件无非是"状态声明、生命周期、构建函数 build"三件事的组合,结构非常清晰,阅读起来不会有无从下手的困惑。

2. 核心知识点

这个秒表麻雀虽小、五脏俱全,几乎每一行代码都对应一个 ArkTS / ArkUI 的核心概念。归纳起来主要有以下几个方面。

状态管理 @State 源码中用 @State 装饰了两个成员:time(当前显示的时间字符串)和 laps(计次列表)。@State 是 ArkUI 状态管理体系中"组件内状态"的声明方式,当这些被装饰的变量被重新赋值时,框架会自动触发组件 UI 的重新渲染。例如定时器回调里每 30 毫秒执行一次 this.time = ...,界面上那个大号 Text 就会跟着刷新;this.laps = newLaps 之后,计次列表也会自动更新。理解"改状态即改界面"是理解整个示例的钥匙。

定时器 setIntervalclearInterval 秒表需要周期性刷新显示,源码在 start() 中调用 setInterval(() => { ... }, 30) 创建了一个 30 毫秒间隔的定时器,句柄保存在私有成员 timer 中;stopTimer() 里通过 clearInterval(this.timer) 停止定时器,并把句柄复位为 -1,作为"当前没有定时器"的哨兵值。所有会离开"计时中"状态的路径(暂停、重置、页面销毁)都会先清理定时器,避免定时器残留造成性能浪费甚至逻辑错乱。

基于 Date.now() 的差值计时。 这是本示例最值得借鉴的计时思路:定时器回调并不去"累加"某个数值,而是每次都重新计算 elapsed + (Date.now() - startTime),其中 startTime 是本次开始时的基准时间戳,elapsed 是此前已经累计的毫秒数。由于 Date.now() 返回的是系统真实时间戳,这样计算出来的永远是"从开始到现在的真实时长",即使某个定时器回调因为主线程忙碌而延迟触发,显示也不会产生累积漂移。

组件生命周期 aboutToDisappear 当页面被销毁(例如通过 router.back() 返回上一页)时,框架会回调 aboutToDisappear()。示例在这里调用 stopTimer(),确保定时器随页面一起释放,这是 ArkTS 页面中清理资源的标准姿势。

列表渲染 ForEach 计次记录通过 ListForEach 渲染:ForEach(this.laps, (item, idx) => { ... }, (item, idx) => idx.toString()),第三个参数是键值生成器,用下标作为每一项的稳定标识,避免列表项被错误复用。

状态派生 UI。 按钮文案、颜色、状态提示都不是单独维护的变量,而是直接从 running 布尔值派生:Button(this.running ? '暂停' : '开始')this.running ? '#ff8f1f' : '#1a6cff' 等等,保证界面与状态永远一致。

除此之外,页面还使用了 layoutWeight 让按钮均分宽度、Blank() 撑开剩余空间、ListscrollBar(BarState.Off) 隐藏滚动条、router.back() 进行页面返回等常规写法,这些在后续源码解析中会逐一对应。

除了上述机制,理解这个示例还需要把握 ArkUI 声明式开发的一个总体原则:开发者描述"界面长什么样、数据与界面怎么对应",框架负责在数据变化时找到需要更新的组件并刷新。这也是为什么示例里到处是 Text(this.time)Button(this.running ? '暂停' : '开始') 这类直接引用状态的写法------UI 的每个属性都被声明为状态的函数,而不需要在数据变化后手动调用某个刷新方法。对于习惯了命令式 UI 的开发者来说,这可能需要一个思维转换,但一旦上手就会发现,状态驱动的代码在"状态和界面必须一致"这一点上天然更有保障。

再往深处说,@State 只是 ArkUI 状态体系中最基础的一层。更复杂的场景里,还有 @Link(父子组件双向同步)、@Prop(单向传递)、@Provide/@Consume(跨层级共享)、@StorageLink(与 AppStorage 全局存储同步)等装饰器。秒表示例的规模不需要这些,但理解 @State 的"组件内私有、赋值触发刷新"特性,是掌握后续所有状态管理概念的地基。示例中把 runningelapsed 等与 UI 强相关的布尔量和数值设计为普通成员而非 @State,也提示我们:状态装饰不是越多越好,够用、明确即可。

3. 源码逐段解析

下面按照源码顺序,把关键片段摘出来逐段讲解。

3.1 状态与成员变量

ets 复制代码
@State time: string = '00:00.00';
@State laps: string[] = [];
private startTime: number = 0;
private elapsed: number = 0;
private running: boolean = false;
private lapCount: number = 0;
private timer: number = -1;

timelaps@State,其余都是普通私有成员。区分这两类的标准是:凡是要驱动 UI 变化的值都用 @State;只参与逻辑计算、不直接渲染的值(如 startTimeelapsedrunninglapCounttimer)保持普通私有成员即可。这里 running 虽然是布尔值,但它的取值会影响按钮文案和状态提示,所以严格来说也应该用 @State------不过源码没有对它加装饰,原因是 running 的变化总是伴随 timelaps 等状态的变化,UI 会被连带刷新,实际效果不受影响。读者如果追求严谨,可以自行补上 @State 装饰。

3.2 时间格式化函数 format

ets 复制代码
private format(ms: number): string {
  const totalCs: number = Math.floor(ms / 10);
  const cs: number = totalCs % 100;
  const totalSec: number = Math.floor(totalCs / 100);
  const s: number = totalSec % 60;
  const m: number = Math.floor(totalSec / 60);
  const mm: string = m < 10 ? '0' + m : String(m);
  const ss: string = s < 10 ? '0' + s : String(s);
  const cc: string = cs < 10 ? '0' + cs : String(cs);
  return mm + ':' + ss + '.' + cc;
}

format 接收毫秒数,输出 分:秒.厘秒 格式的字符串。它的思路是先把毫秒整体除以 10 得到厘秒总数,再依次通过取模和除法拆出厘秒、秒、分:厘秒是总数对 100 取余,秒是厘秒总数对 60 取余,分是厘秒总数除以 60。三个单位再用 < 10 判断补前导零,最终拼接成 mm:ss.cc。因为秒表显示精度到厘秒,所以这里用的是毫秒除 10,而不是先转成浮点秒数再取两位小数,从而完全绕开了浮点精度问题。

3.3 开始与暂停

ets 复制代码
private start(): void {
  if (this.running) { return; }
  this.running = true;
  this.startTime = Date.now();
  this.timer = setInterval(() => {
    this.time = this.format(this.elapsed + (Date.now() - this.startTime));
  }, 30);
}

private pause(): void {
  this.elapsed += Date.now() - this.startTime;
  this.running = false;
  this.stopTimer();
}

start() 先做防重入判断,已运行则直接返回,随后记录基准时间戳并启动 30 毫秒的定时器;每次回调都用"已累计毫秒 + 本次开始以来的毫秒"计算当前总时长并刷新 timepause()start() 对称:先把本次运行段的时间累加进 elapsed,再置 running 为 false 并停止定时器。elapsed 在这里扮演了"多个运行段累计"的角色,是暂停后续跑的数学基础------每次暂停把这段时间"沉淀"进 elapsed,下次开始再从新的基准时间戳起算。

3.4 计次与重置

ets 复制代码
private lap(): void {
  if (!this.running) { return; }
  this.lapCount += 1;
  const newLaps: string[] = this.laps.slice();
  newLaps.push(this.padNum(this.lapCount) + '. ' + this.time);
  this.laps = newLaps;
}

lap() 里有一个值得注意的写法:先 this.laps.slice() 拷贝出新数组,再往新数组里 push,最后把整个数组重新赋给 this.laps。这是因为 ArkUI 对 @State 数组的更新依赖"重新赋值"来触发渲染,直接 this.laps.push(...) 虽然能改数组内容,但未必能可靠地触发列表刷新。用"拷贝---修改---重新赋值"的不可变更新方式总是最稳妥的。padNumString(n).padStart(2, '0') 给序号补零,保证第 1 次计次显示为 01.

ets 复制代码
private reset(): void {
  this.stopTimer();
  this.running = false;
  this.elapsed = 0;
  this.lapCount = 0;
  this.laps = [];
  this.time = '00:00.00';
}

reset() 把定时器、运行状态、累计毫秒、序号、计次列表、显示时间全部复位,没有任何遗漏,保证状态回到初始态。注意顺序:先停定时器,再改状态,避免停在运行态时又有回调穿插进来。

3.5 定时器清理与生命周期

ets 复制代码
private stopTimer(): void {
  if (this.timer !== -1) {
    clearInterval(this.timer);
    this.timer = -1;
  }
}

aboutToDisappear(): void {
  this.stopTimer();
}

stopTimertimer !== -1 作为"定时器存在"的判断,清除后把句柄复位,这样重复调用也是安全的。aboutToDisappear 是组件销毁前的生命周期回调,在这里统一清理定时器,避免页面退出后回调仍然执行、甚至访问已销毁组件的问题。

3.6 页面布局 build

build 方法里,外层是一个 Column,宽度和高度都设为 100%,背景色是浅灰 #f2f3f5。第一行 Row 放返回按钮、标题和 Blank(),用 Blank() 把剩余空间吃掉,使标题自然靠左、右侧无内容。大号时间用 Text(this.time) 渲染,字号 64、加粗、主题蓝 #1a6cff;状态提示 Text(this.running ? '计时中' : '已暂停') 的颜色也随 running 切换。控制区是两个按钮组成的 Row,间距 12,各自 layoutWeight(1) 平分 86% 宽度;重置按钮独立占一行。列表标题动态拼接条数,List 里用 ForEach 遍历 laps,每一项包一层白色圆角卡片,layoutWeight(1) 让列表占满剩余高度,scrollBar(BarState.Off) 隐藏滚动条。

4. 关键实现细节分析

4.1 计时精度:定时器只是"显示节拍",时间来自时间戳

很多初写秒表的人会把 setInterval 的回调次数当作时间单位,比如"每 30 毫秒让计数器加 30"。这个思路在两个层面都不准确:其一,setInterval 的间隔只是"期望间隔",主线程繁忙时回调会被延后甚至合并,累计次数和真实时间会产生偏差;其二,多个延迟叠加后误差会不断累积,几分钟后显示的时间就明显慢了。示例巧妙地绕开了这个问题------定时器只负责"每 30 毫秒触发一次刷新动作",真正的时间永远是 Date.now() 与基准时间戳的差,回调触发得晚一点,下一次计算会自动用更大的差值补回来,因此不会产生累积误差。UI 每 30 毫秒刷新一次,对厘秒精度来说足够平滑,同时对性能也很友好。

4.2 厘秒格式化与补零

秒表把时间切成三个单位:分(两位数)、秒(两位数)、厘秒(两位数),中间用冒号和点分隔。整个格式化过程全是整数运算:先 Math.floor(ms / 10) 得到厘秒总数,再层层取余。这样得到的结果天然不会出现 01:23.4 这种一位小数,也不会有浮点舍入问题。补零用三目表达式 m < 10 ? '0' + m : String(m),在单位不足两位时在前面拼一个 '0'。这个 format 函数本身是纯函数------输入毫秒输出字符串,没有副作用,因此可以安全地放进定时器回调里反复调用。

4.3 生命周期的资源清理

aboutToDisappear 里清理定时器是本例最重要的一行守卫代码。如果遗漏,用户计时后直接返回上一页,定时器仍然存活,回调继续修改已被销毁组件的状态,轻则白耗 CPU、重则报错。同理,pausereset 也各自调用了 stopTimer,保证任何路径离开运行态后都不会有残留定时器。读者可以把 setInterval 的句柄视为"必须成对管理"的资源:创建在 start,清理由 stopTimer 统一负责,句柄 -1 哨兵值让清理逻辑具备幂等性,无论调用多少次都不会重复 clearInterval

4.4 数组状态的不可变更新

laps 是数组类型的 @State。代码没有用 this.laps.push(...) 直接修改,而是先 slice() 产生副本,push 后再整体赋值。这种"拷贝---修改---重新赋值"的写法,是 ArkUI 中让数组类状态可靠触发渲染的推荐做法,也方便在以后引入 @Observed / @ObjectLink 等更精细的状态管理时平滑迁移。副本带来的额外开销对秒表这种"每次计次才新增一条"的场景完全可以忽略。

4.5 ForEach 的键值生成器

ForEach(this.laps, ..., (item, idx) => idx.toString()) 里第三个参数 idx.toString() 是键值生成器。因为计次列表只增不删、内容本身就可能重复(不同计次可能显示相同时间),用下标做键既稳定又唯一,是最省心的选择。若用内容做键且内容重复,会出现列表项复用的隐患;若完全省略键值生成器,框架在列表更新时的复用判定也会不够精确。

4.6 按钮文案与颜色的状态派生

"开始/暂停"按钮的文案、背景色、状态提示文案、提示颜色,全部由 running 派生。这样做的好处是单一数据源:只要 running 正确,界面上所有相关展示就不会互相矛盾;代码里不需要维护"按钮文案"和"按钮颜色"这类额外状态。这也呼应了 ArkUI 声明式开发的理念------把状态和 UI 的映射关系写清楚,剩下的刷新交给框架。

4.7 返回按钮与路由

页面顶部返回栏里的 Button('返回') 通过 onClick 调用了 router.back()router 是 ArkTS 提供的路由模块,back() 会把当前页面从路由栈中弹出,回到上一个页面。页面被弹出时会触发 aboutToDisappear,所以这里的"返回"路径与生命周期清理是天然衔接的------用户点返回,页面销毁,定时器被清理,不会留下后台运行的任务。示例里 router 在文件顶部通过 import { router } from '@kit.ArkUI' 引入,这也提醒我们:所有需要使用的 API 都必须显式 import,编译期就会检查,漏 import 会直接报错而不是运行时才暴露。

4.8 布局细节:间距、权重与占位

示例的布局里有几处值得留意的细节。返回栏用 padding({ left: 12, right: 12, top: 10, bottom: 10 }) 撑出内边距,按钮与标题之间没有显式设置间距,而是靠 Blank() 把中间空间吸收,让标题紧跟返回按钮、右侧贴边。控制按钮行用 Row({ space: 12 }) 声明组件间距,两个按钮再各占 layoutWeight(1),这样无论屏宽多少都能均匀二分;宽度统一取 86%,两侧各留 7% 的页边距,视觉上比全宽更舒适。重置按钮与"开始/暂停"按钮虽然宽度相同,但被放在了独立一行,与计次记录区之间用 margin({ top: 20 }) 拉开层次。时间数字的 margin({ top: 40, bottom: 8 }) 与状态提示的 margin({ bottom: 24 }) 共同形成页面的垂直节奏。列表项内部 padding({ left: 12, right: 12, top: 10, bottom: 10 }) 配合 borderRadius(8) 做出卡片效果。这些数字虽小,却决定了页面是否耐看,动手改改间距和圆角,是熟悉 ArkUI 布局参数最快的途径。

5. 运行效果与操作指南

把工程编译到模拟器或真机,从示例列表进入"秒表"页面即可体验。首次打开时,大号时间显示 00:00.00,状态提示为"已暂停",按钮为蓝色的"开始",计次列表为空。

操作路径如下:

  1. 点击"开始",按钮变为橙色"暂停",状态提示变为"计时中",时间从 00:00.00 开始以厘秒为单位跳动。
  2. 计时过程中随时点击"计次",当前时间会被追加到列表,序号自动补零(第 1 次显示 01,第 10 次起显示 10),列表标题同步更新条数。
  3. 点击"暂停",时间停在当前值,按钮回到蓝色"开始",状态提示回到"已暂停";再次点击"开始",时间从暂停处继续累积。
  4. 点击"重置",时间归零、计次清空、序号复位,回到初始状态。
  5. 点击左上角"返回",页面销毁,定时器随之清理。

可以尝试的组合操作包括:计时一段时间后暂停再继续、连续计次若干条后重置、在暂停状态下点"计次"(会被忽略,因为 lap()running 守卫)等等,借此体会状态守卫的作用。也可以把秒表挂起一分钟再回来,观察时间是否准确,验证 Date.now() 差值计时的可靠性。

观察运行效果时还可以留意几个细节:时间大字的刷新粒度是厘秒,即小数点后两位,这意味着数字每秒会跳动约 100 次,视觉上非常顺滑;状态提示文案"计时中/已暂停"配合颜色变化,让用户一眼就能判断当前是否在跑;计次列表每条记录都是白色圆角卡片,与浅灰背景形成层次,滚动到底部会自动触底,无需额外处理。整体交互符合直觉:主操作(开始/暂停)放在最显眼的位置,计次是次要动作,重置放在最下方避免误触,这种按钮排列本身就是对交互优先级的一种取舍。

6. 可扩展方向

在这个基础上,可以朝多个方向扩展:

  • 分段计时(Split/Lap 差异):目前计次记录的是"到当前为止的总时间",可以改为记录每个计次之间的时间差,形成"单圈用时",更接近专业秒表的含义。
  • 结果统计:统计最快、最慢、平均计次用时,并用柱状图或列表高亮展示,加深列表和排序逻辑的练习。
  • 持久化 :把计次结果通过 @ohos.data.preferences 或应用沙箱文件保存,下次打开仍可查看,涉及数据读写和状态恢复。
  • 多段动画 :时间数字变化时加一点位移动画或缩放动画,配合 animateTo 提升体验。
  • 触觉与声音反馈:计次时播放短提示音或触发震动,需要调用系统能力接口。
  • 更多计时模式 :如正向秒表、倒计时、世界时钟切换,复用 formatstopTimer 等基础逻辑,把公共部分抽成工具类。

上述扩展大多不涉及架构上的大改,更多的是把已有逻辑拆开、重组、复用。比如把 formatstopTimerpadNum 这类与 UI 无关的纯函数抽到一个 utils/ 目录下的工具文件里,多个页面共同引用;把秒表与倒计时的公共逻辑抽象成可复用的组合式逻辑,则能进一步减少重复代码。做这些重构时,记得保持"创建与清理成对""状态单一来源"这两条原则不变,无论怎么抽,它们都是时间型应用的底线。

7. 常见问题与调试技巧

  • 计时不准 / 越走越慢 :检查是否把累计计数当时间用。正确做法是始终用 Date.now() 差值,示例中的 elapsed + (Date.now() - this.startTime) 就是标准范式。
  • 返回页面后仍在计时 :多半是 aboutToDisappear 没写或没调 stopTimer。可以在 stopTimer 里加一行日志 console.info('timer cleared'),配合 DevEco Studio 的日志面板确认清理时机。
  • 计次列表不刷新 :多半是直接 push 修改了 @State 数组。改为"拷贝---修改---重新赋值"(slice() 后整体赋值)即可。
  • 按钮文案与实际状态不一致 :确认所有文案都由 running 派生,而不是手工切换,避免多处维护导致漂移。
  • 时间停在 00:00 不动 :确认 start()this.running = true 已执行、定时器已创建;可以在回调里临时打印 this.time 观察是否进入回调。
  • 厘秒跳得不对(出现 0.9、1.0 之类) :检查格式化是否用 Math.floor(ms / 10) 取厘秒,以及是否漏掉 % 100 的取余步骤。
  • 重置后还残留旧数据 :检查 reset() 是否漏掉某个状态。对比源码,elapsedlapCountlapstimerunning 五个字段必须全部复位,少一个就会出现"时间归零但列表还在"的怪现象。
  • 列表项复用导致样式错乱ListItem 内部如果套用了复杂的自定义子组件,注意给每一项提供稳定的键;秒表示例直接用下标 idx.toString(),简单安全,不要随意改成按内容生成键。
  • 真机上数字跳动有拖影:多为刷新率与渲染管线叠加所致,不要用更密的定时器去"解决",那只会加剧耗电;适当放宽刷新间隔(如 50 毫秒)往往反而更稳。

调试这类定时器应用时,建议优先在真机观察,模拟器上定时器精度与真实设备略有差异;同时善用 DevEco Studio 的断点,在 setInterval 回调第一行暂停,检查 Date.now() - this.startTime 的差值是否符合预期。另外,如果页面里还叠加了动画或长列表滚动,主线程压力会变大,30 毫秒的刷新间隔可能出现轻微掉帧,此时可以适当放宽到 50 毫秒,对厘秒显示影响不大。此外,开发时可以把 formatpadNum 这类纯逻辑函数单独拿出来验证,例如传入已知毫秒数断言输出的字符串是否符合预期,这部分不依赖 UI 环境,随时可以跑通;而定时器相关的时序逻辑,则必须在真机上观察才能得出可靠的结论。

8. 总结

秒表示例用不到两百行代码,就把 ArkTS 状态管理、定时器生命周期、时间计算、列表渲染这几大主题串成了一台能真实运行的小应用。它的核心思想可以概括为三点:第一,用 @State 让数据驱动 UI,界面永远跟随状态;第二,用 Date.now() 差值而非计数累加来计时,从机制上消灭累积误差;第三,把定时器的创建与清理成对管理,任何离开运行态的路径都保证资源释放。把这三点吃透,再遇到"打字速度测试""在线时长统计""番茄钟""刷新倒计时"一类需求,都能直接复用这套思路。

总的来说,这个示例非常适合作为 ArkTS 入门后的第二个练手项目------第一个可以是纯静态页面,第二个就选这种带状态、带定时器、带列表的动态页面。它不依赖任何第三方库,也不涉及网络与存储,所有知识点都能在一个文件里看完,适合精读。建议读者在读完本文后,尝试关掉源码自己重写一遍:从定义状态开始,写出 formatstartpauselapreset,再组装 build 布局,最后补上 aboutToDisappear 的清理。如果能独立完成,说明对 ArkTS 的状态管理、定时器和布局已经形成了基本手感,接下来就可以向更复杂的应用迈进了。

相关推荐
zdj_develop2 小时前
鸿蒙应用开发实战(一):应用程序包概述
华为·harmonyos
geats人山人海2 小时前
数据结构第六章c语言 树的存储下二叉树的概念和存储
c语言·数据结构·算法
白狐_7983 小时前
408 数据结构算法题 01:线性表暴力求解保分指南
java·数据结构·算法
智塑未来3 小时前
鸿蒙更智能:从“人找App”到“意图即服务”
华为·harmonyos
冻柠檬飞冰走茶3 小时前
PTA基础编程题目集 7-31 字符串循环左移(C语言实现)
c语言·开发语言·数据结构·算法
来一碗刘肉面4 小时前
树的存储结构
数据结构
GrowthDiary0075 小时前
算法题:寻找二维数组top k问题
数据结构·python·算法
SHARK_pssm6 小时前
【数据结构——栈】
数据结构