


修改大型桌面媒体应用,不卡住界面也不破坏撤销
用于处理文件和媒体的大型桌面应用:把耗时工作移出 UI 线程,处理取消和边界情况,保证撤销能准确恢复,并只报告真正运行过的测试。
要使用这个AI规则
## 追踪完整调用路径,为每个状态只定一个所有者
- 在修改代码之前,先理解该功能所属的模块和现有的抽象设计,并梳理清楚从界面、自动化、撤销、持久化到后台工作的完整调用路径。
- 对于非平凡的改动,在编写代码之前先确定数据的权威来源、状态所有者、隔离域、不变量,以及失败和取消时的行为。
- 先设计架构再考虑实现细节,优先做一次连贯完整的改动,而不是把补丁零散地打在互不相关的多个层次上。
- 每一份可变状态都应只有一个权威所有者;派生状态则通过带有明确失效规则的缓存来计算或维护。
- 复用已有的领域操作;不要仅仅因为调用方是预览、视图、控制器、自动化工具或测试,就另外新写一套实现。
- 代码应放在其所属功能的模块中;共享工具仅用于多个独立功能都会用到的基础设施;类型和文件的命名要能清楚体现归属关系。
## 注释保持简短,并清理不再使用的代码
- 注释要精简:只有在原因、不变量、安全约束或变通方案不明显时才写注释,并且控制在简短的一行以内。
- 不要用注释复述代码在做什么、重复变量或函数名、描述当前这次改动、留下删除痕迹的说明,或保留被注释掉的旧代码。
- 优先用精确的命名、小而清晰的类型、以及抽取出来的独立操作来表达意图,而不是依赖解释性注释。
- 每一条计算逻辑或业务规则只能有一处权威定义;绝不能把它复制到另一个文件或界面中。
- 完成改动前,清除死代码、未使用的状态、过时的兼容性分支,以及临时的调试诊断代码。
- 不要为项目本身不支持的平台、版本或配置添加兼容性代码。
## 把耗时工作移出 UI 线程,并在每次 await 后重新确认状态
- 把界面线程当作稀缺资源来对待:它可以承载界面状态和轻量的协调逻辑,但绝不能执行文件操作、解码、模型推理、图像处理、索引构建、导出、阻塞调用或大规模数据转换。
- 隔离不等于性能优化:把某段工作标记为异步或包进一个任务,并不会自动让它离开当前线程执行。要以实际的执行器为准。
- 让线程切换保持显式可见;在离开某个线程之前,先对最小必要的不可变输入做一次快照,返回一个值,并在确认该值仍然有效之后再应用它。
- 每经过一个挂起点,都要假设共享状态已经发生变化:在提交结果之前,重新校验身份、代次、选中状态和前置条件。
- 优先使用结构化并发;如果确实需要非结构化的任务,就必须有明确的所有者,在取消操作有意义时保存好任务句柄,并定义清楚清理流程。
- 取消操作是协作式的:长时间运行的循环要在有意义的节点检查取消状态,并且绝不能提交部分完成或已过期的结果。
- 并行工作的规模要以真实的稀缺资源为上限;对完全相同的在途请求做去重;获取和释放并发闸门时要保持作用域一一对应;每个续延或回调都必须且只能被恢复一次。
- 绝不能用同步等待、信号量、锁,或者对异步工作的轮询来阻塞界面线程。
## 在 UI 线程之外处理文件,并以原子方式替换文件
- 所有文件系统操作都必须放在界面线程之外执行:读取、写入、存在性和元数据检查、遍历枚举、复制、移动、替换、删除以及目录创建。
- 要假设任何存储卷都可能很慢、可移除、被外部修改,或者依赖网络;文件小或者之前访问成功过,都不能说明同步访问是安全的。
- 在执行文件操作之前,先对所拥有的模型数据做快照,并且不要在文件操作的执行器中直接修改可观察的状态。
- 先在正式目标之外把完整的输出准备好,在目的位置备好替换内容,然后再以原子方式完成替换;对正式内容的写入要统一通过一个协调者来完成,而不是直接写入。
- 针对同一个文件或目的地的操作要串行化,确保保存、导入、生成、缩略图、删除、导出和关闭这些操作之间不会产生竞态。
- 使用唯一的临时路径,并且无论成功、失败还是取消都要清理干净;在完整的替换内容真正准备好之前,绝不能删除或替换目的地内容。
- 用户主动发起的文件操作一旦失败,必须让用户看到;不要用被忽略的错误、空结果或伪装成成功的响应把失败掩盖起来。
## 声称变快之前先测量,并让高频路径保持轻量
- 识别出对性能敏感的路径,比如渲染、播放、输入处理、视图更新、导入、索引构建、保存、恢复和导出,并把它们当作热路径对待。
- 不要在逐项处理、逐帧处理或反复触发的交互路径中执行文件系统访问、日志记录、序列化、模型或解码器初始化等阻塞性的准备工作。
- 在宣称有性能提升之前先实测:对改动前后的同一工作负载做性能分析或基准测试。
- 先解决算法复杂度和不必要的重复工作,再考虑底层调优;要警惕嵌套扫描、重复排序、循环中的复制式修改、中间对象分配,以及反复的线程切换。
- 对批量修改进行合并与批处理;在做快照、撤销、导出、关闭,以及必须返回最新数据的读取操作之前,先把待处理的状态刷新落地。
- 数据要采用惰性加载和填充;只有在明确定义了缓存键、容量上限、失效规则和过期结果处理策略的前提下,才可以对开销较大且可复用的工作结果进行缓存。
- 在生命周期允许的范围内复用开销较大的对象,并在相关配置发生变化时使其失效;避免让进度、仪表等高频更新的状态触发整个视图的失效重绘;对保留的内存占用设置上限。
## 实现之前先列出边界情况和失败路径
- 只处理理想路径是不够的:在动手实现之前,先列举出适用的边界情况、生命周期情况、并发情况和失败情况,并确定每一种情况分别由哪一层来负责处理。
- 视情况对空值、nil、零值、负数、最大值、溢出值、非有限数、格式错误、重复、缺失、过期以及不受支持的输入进行校验。
- 在进行算术运算、索引访问或数值转换之前先做校验;不要指望靠后面的异常捕获来兜住算术层面的陷阱;四舍五入和数值截断的规则要明确定义,而不是悄悄地做限幅处理。
- 要覆盖空操作和重复操作的情况:它们必须准确地报告结果,并且不能产生实际的状态改变、历史记录、重复的工作,或者具有误导性的"成功"提示。
- 要考虑在开始之前、每个阶段进行中、以及工作完成但尚未提交这几个时机的取消操作;同时要考虑在选中项、目标或配置发生变化之后,出现过期完成结果的情况。
- 要考虑各种收尾路径:关闭、退出、休眠、唤醒、失活、设备变更,以及在工作进行中执行"另存为"的情况。
- 在每一条失败路径上都要维护好不变量:部分成功的结果要么是可以安全恢复且被明确报告为如此,要么就应当被回滚。
## 让用户的一次操作对应一次撤销
- 用户操作和自动化操作都应当经过同一套领域层的修改操作,并共用同一份历史记录。
- 一个连贯完整的用户意图应当对应生成一个可撤销的操作;不要把内部的子步骤各自暴露成独立的历史记录条目,除非每个子步骤本身具有独立的意义。
- 在开启一个历史记录分组之前先校验参数和前置条件;失败、被取消、被拒绝以及未产生实际变化的操作,都不应该生成空的历史条目。
- 嵌套的工作应当合并进外层的操作中,且不能关闭由平台或其他子系统所拥有的分组。
- 撤销操作应当精确还原到原来的状态,不能出现累积的舍入误差、派生状态的偏移、孤立的关系数据,或者过期的选中状态。
- 当某项改动涉及历史记录时,要测试用户编辑、自动化编辑、平台事件分组和并发请求之间相互交错的情况。
## 根据用户想完成的事来设计自动化工具
- 工具的设计应当从用户意图出发,而不是从内部API、存储操作或服务边界出发;在设计参数结构之前,先明确定义预期结果、成功标准、警告、失败、取消、幂等性和撤销语义。
- 一个工具只完成一个连贯的动作;一次调用通常应当完成一个原子的、易于理解的、可撤销的完整工作流。不要构建一个功能杂糅、模式互不相关的大而全工具,也不要强迫调用方为了完成系统本可以安全自行完成的工作而串联一堆底层小工具。
- 参数要用面向用户的领域概念来表达,隐藏存储结构和实现细节;自动化调用要使用稳定的实体标识符,位置索引和标签只能作为上下文参考,不能当作持久的身份标识。
- 把每一个参数都当作不可信的输入来对待:要求精确的类型、有限的数值、明确的边界和有效的标识符;在执行修改之前,先完整地解析并校验整个请求,涉及多个实体的改动要以原子方式应用。
- 复用与界面相同的领域操作,不要重复实现放置、关联或校验逻辑。
- 返回结构化的执行回执,说明实际发生了什么改变、稳定的标识符、明确的空操作状态、警告信息和可操作的错误提示;当实际结果被调整过或未能达成时,绝不能返回伪装成成功的响应;除非接口约定明确承诺并会报告,否则不能悄悄地做限幅、改变目标或降级回退处理。
- 长时间运行的工具要提供一个调用方可以查询的持久化任务或最终结果;面向机器的接口约定要保持稳定,并与本地化文案分离;说明文档要解释一个工具在何时、为何以及如何与其他工具协同工作。
## UI 状态留在 UI 线程,并保留平台原有的行为
- 可观察的界面状态要保留在界面线程上,后台产生的结果应当以不可变值的形式跨越线程边界传递过来。
- 状态观察的范围应限定在真正需要该值的最小视图内;进度、悬停、播放状态这类高频更新的数据不应导致无关的视图树发生失效重绘。
- 不要在渲染函数体内启动持久性的副作用;应当使用感知生命周期、带有明确取消和清理机制的任务或控制器来完成,同时保持渲染函数体的执行快速且无副作用。
- 焦点、键盘操作、Esc取消、菜单、窗口恢复、撤销、拖拽状态和关闭确认这些行为,都应当保持平台原生的表现。
- 代理和完成回调这类接口约定,无论是成功、失败、取消,还是目标缺失的路径,都必须且只能被完成一次。
- 除非API明确保证,否则不要假定框架回调或完成回调一定会在界面线程上触发。
## 处理媒体时不阻塞,并保持时间和帧数值精确
- 对外部资源使用异步属性加载,不要调用可能阻塞调用线程的、已被弃用的同步访问方法。
- 尽可能长时间地让数值保持在其精确的原生领域内,比如精确的时间、帧数或整数单位,只有在明确的界面边界或外部格式边界处才转换为浮点数。
- 明确定义并保留素材原有的语义,比如时序、缩放、变换、色彩、透明度、通道布局和元数据,除非功能本身明确要对它们进行改变。
- 在生命周期允许的范围内复用读取器、渲染上下文和处理流水线;在连续交互过程中,除非确实需要因失效而重建,否则不要重新构建它们。
- 对并发的解码器、读取器、导出、推理和提取任务数量设置上限;取消信号要贯穿每一个解码、渲染和导出循环;当底层某一步骤本身无法被取消时,要在分块之间检查取消状态。
- 要在轨道缺失、非常规容器格式、时长为零或不确定、旋转或带透明通道的输入、非标准帧率,以及取消操作等情况下进行测试。
## 用设计令牌代替写死的样式数值
- 所有样式都要通过共享的设计令牌系统来实现;绝不能硬编码间距、尺寸、字重、圆角、边框宽度、不透明度、颜色、阴影或动画时长。
- 如果所需的令牌尚不存在,应当先把它添加到令牌系统中,而不是直接硬编码一个数值。
- 资源查找机制必须在打包后的应用、本地运行和测试环境中都能正常工作;要使用共享的资源查找抽象层,而不是各功能自行摸索实现;每一种资源包布局都应当被当作一种独立的配置去验证。
- 不要用仅限调试用的开关来切换到本质上不同的资源路径或行为,除非这种差异是刻意设计并经过测试的。
- 新增打包资源时,要同步更新包清单、构建脚本和测试,验证最终打包后的布局,并确保带功能开关的可选代码在开关开启和关闭两种情况下都能正常构建。
## 让每个操作都有可见的结果,并留下有用的日志
- 每一个由用户或自动化发起的操作,最终都应当达到一个可观察的状态:成功、失败、拒绝或取消。
- 如果某个失败会改变最终结果,就不要在交互边界上忽略这个错误;应当把它转化为界面状态、工具错误、任务失败或者可操作的日志。
- 只有在明确属于"尽力而为"性质的情况下,才允许使用"发出即忘"式的操作;这类操作不能承担必须完成的持久化职责,其失败必须是可以安全忽略的,或者会被记录下来。
- 记录失败日志时要包含具体的操作、稳定的标识符和所处的生命周期阶段;绝不能把密钥、凭据或用户的原始输入写入日志。
- 要保留足够的诊断信息,以便区分取消操作、校验失败、资源不可用、框架层面的失败,以及内部不变量被破坏这几种不同情况。
- 断言只应当用于程序员层面的不变量检查,不能用于可恢复的用户输入、外部文件、生命周期竞态或自动化请求。
## 编写小巧、结果确定、可并行运行的测试
- 单元测试要简洁并聚焦于行为:一个测试只验证一个不变量或一个回归问题,只保留能把这一点讲清楚所必需的准备代码,使用具有描述性的命名,不写逐步叙述过程的注释。
- 针对边界情况的组合,使用参数化测试和共享的测试夹具构建器,而不是在各个测试套件中重复编写准备代码。
- 通过能够证明该行为的、最小且稳定的接口来进行测试,避免与私有实现细节或偶然的调用次数产生耦合。
- 每一次修复缺陷,只要可行,都应当配一个针对原始缺陷会失败的回归测试,并且要覆盖各种反向路径:校验失败、空操作、部分失败、取消、过期完成结果,以及清理逻辑。
- 并发相关的测试要通过闸门、续延或注入的钩子来实现确定性的协调,不能依赖睡眠等待、时序上的运气,或者调度顺序。
- 测试要能够并行运行:不能共享可变的全局状态、固定的临时名称、端口或目录;应使用唯一的临时目录并在结束后清理;单元测试中不能出现真实的凭据、外部服务或真实的用户文件。
## 先审查正确性,并如实报告运行了哪些测试
- 评审时先关注正确性,再关注风格:把改动带来的行为变化沿着调用方、共享状态、后台工作、持久化、撤销、界面和自动化接口这些环节逐一追踪清楚。
- 优先处理会导致数据丢失、数据损坏、崩溃、卡死、竞态、安全暴露、结果错误、撤销功能失效或误导性"成功"提示的问题,其次才是性能、可维护性和细节打磨。
- 要留意界面线程阻塞、并发方面的错误(比如挂起之后未经重新校验就提交状态)、无人负责的任务、缺失的取消处理、无限制的任务扇出、不成对的并发闸门、被重复恢复两次的回调,以及重复的领域逻辑、不安全的缓存和未经校验的边界算术运算;评审的范围应限定在这次改动本身及其直接影响的部分。
- 对结论进行验证:运行与该改动相应的构建和测试,对涉及并发的改动要实际触发取消和生命周期路径,并在具有代表性的工作负载下测量性能变化。
- 准确报告哪些验证做了、哪些没做;如果只完成了部分验证,就不要声称已经完成了完整的构建、测试或运行时验证。
- 提交要保持聚焦,使用范围明确、准确的类别前缀,不夹带无关的清理工作;描述一次改动时,应说明它的概要、思路、关键设计决策和测试证据,而不是罗列具体做了哪些编辑操作。
