把包裹重量从九百九十九克改成一千克,页面仍然显示“轻件档”。规则表里明明有一千这个键,容器也没有丢失数据。最容易被怀疑的是输入转换,其次是页面刷新;真正需要检查的,却是查询函数中的“比它小”究竟包不包含“等于它”。这个小问题足以让一张看似正确的分档表,在每个边界上都错一次。

阈值查询与有序区间封面

一次错档,把查询条件暴露出来

本文做一个重量分档工具,工程名为 FareBands,界面名称是“重量分档”。规则使用 0、1000、3000 克三个起始点,分别表示轻件、标准件和加固件。这里的数值是示例业务规则,不是运输服务商的收费标准。工程采用 Stage 模型、API 12 和 ArkTS,不访问网络,也不读取设备信息,因此不需要额外权限。

先把业务语言写完整:重量落在某个起始点上时,应该进入从该点开始的新档;落在两个起始点之间时,使用较小的那个起始点。因此查询目标是“小于或等于重量的最大键”。如果直接调用取得前驱键的方法,查询的却是“严格小于重量的最大键”。两个条件在大部分输入上表现相同,只在等号出现时分开。

TreeMap 官方接口说明,getLowerKey 返回小于比较键的最大键,无结果时返回未定义。示例里比较键为三时,前驱是二。项目不能把方法名中的英文习惯理解当成业务定义;确认等号归属,才是选择接口前的第一步。

这里不用排列顺序来猜档位。无序映射适合按完整键直接取值,数组适合小规模静态规则,但有序映射能把相邻键查询表达得更直接。选择它不是宣布其他容器不适用,而是让核心操作与当前问题一致。三条规则本身不会构成性能压力,本文也不以这个规模比较运行速度。

最初的错误还隐藏着另一个信号:若查询零克,前驱不存在。把这个返回值强制转换成数字,再拿它访问容器,只会把一次明确的规则缺口变成后续的空文本。代码必须让“没有起始档”成为能被解释的异常,而不能靠类型断言让编译器停止追问。

先命中等值键,再寻找前驱

在空 Ability 工程中保留模板的 EntryAbility,让它加载 pages/Index。新增 common/WeightBands.ets,所有规则都由这个模块维护。键使用整数克,避免把字符串的排列关系带入数值分档;比较器显式返回前者是否小于后者,这符合该容器的布尔比较器约定,不使用数组排序里的差值返回写法。

// entry/src/main/ets/common/WeightBands.ets
import { TreeMap } from '@kit.ArkTS';

export class WeightBands {
  private bands: TreeMap<number, string> =
    new TreeMap<number, string>((a: number, b: number): boolean => a < b);

  constructor() {
    this.bands.set(0, '轻件档');
    this.bands.set(1000, '标准件档');
    this.bands.set(3000, '加固件档');
  }

  classify(weight: number): string {
    if (!Number.isInteger(weight) || weight < 0 || weight > 20000) {
      throw new Error('重量须为 0—20000 的整数克');
    }
    const key: number | undefined = this.bands.hasKey(weight)
      ? weight : this.bands.getLowerKey(weight);
    if (key === undefined) {
      throw new Error('规则缺少起始档');
    }
    const name: string | undefined = this.bands.get(key);
    if (name === undefined) {
      throw new Error('分档规则不完整');
    }
    return `${key} 克起 · ${name}`;
  }

  describe(): string[] {
    const rows: string[] = [];
    for (const key of this.bands.keys()) {
      rows.push(`${key} 克起 · ${this.bands.get(key)}`);
    }
    return rows;
  }
}

等值判断放在前面,可以清晰地看到区间左端被包含。不存在完全相同的键时才查询前驱,这两步组合成需要的向下取档。它没有改写容器,也没有用加一或减一的技巧绕过边界;如果以后单位从克变成其他离散单位,等值判断仍然表达同一个规则。

返回键显式声明为可能未定义,是为了把运行时边界留在代码里。接口文档的返回类型写成键类型,并不意味着业务可以忽略没有结果的分支。对于当前含零起始档且重量非负的规则,这条异常路径不应正常触发;保留它能在维护者删除零档后尽早暴露配置破坏。

第二次判断处理“键存在但没有有效名称”的契约破坏。当前值类型限定为字符串,构造函数又给出了完整名称,这个分支通常也不会进入。它的价值是把容器查询与界面展示的边界固定下来,避免未来把值改成可选结构后,页面默默输出一个空白结果。

WeightBands 核心查询与重量分档页面

规则公开的是查询和只读描述,没有公开内部容器。页面拿到描述数组以后可以展示,但不能顺手改写下一次查询的事实来源。小工程也值得保留这个边界,因为“配置显示正常而查询结果异常”往往来自两份规则,而不是来自树的排序算法。

页面输入负责语法,规则模块负责范围

页面只接受十进制整数文本。先校验文本形态,再转换成数字,最后交给规则模块检查取值范围。三个层次分别回答“是不是所需写法”“对应什么数值”“业务是否接受这个数值”。将它们合并成一次宽松转换,会让空字符串、空格、小数或科学记数法悄悄得到一个数字。

// entry/src/main/ets/pages/Index.ets
import { WeightBands } from '../common/WeightBands';

@Entry
@Component
struct Index {
  private model: WeightBands = new WeightBands();
  @State weightText: string = '1000';
  @State result: string = '等待查询';
  @State rows: string[] = this.model.describe();

  private check(): void {
    const value = this.weightText.trim();
    if (!/^\d{1,5}$/.test(value)) {
      this.result = '请输入整数克,不含小数或单位';
      return;
    }
    try {
      this.result = this.model.classify(Number(value));
    } catch (error) {
      this.result = (error as Error).message;
    }
  }

  build() {
    Column({ space: 16 }) {
      Text('重量分档').fontSize(28).fontWeight(FontWeight.Bold)
      Text('左闭右开,边界进入新档').fontSize(14).fontColor('#64748B')
      TextInput({ text: this.weightText, placeholder: '输入整数克' })
        .type(InputType.Number).height(48)
        .onChange((value: string) => { this.weightText = value; })
      ForEach(this.rows, (row: string) => {
        Text(row).fontSize(17).width('100%').padding(16)
          .backgroundColor('#FFFFFF').borderRadius(14)
      }, (row: string) => row)
      Button('查询所属档').width('100%').height(44)
        .backgroundColor('#0F766E').onClick(() => { this.check(); })
      Text(this.result).fontSize(18).fontColor('#0F766E')
    }.width('100%').height('100%').padding(24).backgroundColor('#F1FAF7')
  }
}

输入类型控制键盘体验,不能替代校验。粘贴、辅助输入或未来从其他页面传入参数,都可能越过键盘暗示。页面的正则限制一到五位数字,模块进一步限制最大重量,因此两万零一虽然符合文本形态,仍然会得到范围错误。保留这类区别能让提示与实际拒绝原因一致。

对前导零也要做决定。当前把五位输入“01000”按十进制数值处理,最终等同 1000 克;如果业务要求输入必须是规范写法,应单独添加格式约束,不应借容器排序来实现。规则模块只接收数字,也就无需知道用户最初输入了几个零。

界面初始显示三条规则和“等待查询”,一千克只是输入框里的待查值,不代表查询已经发生。点击后才出现标准件档。图片中的按钮、输入及结果必须对应同一时刻,否则读者会以为默认值已经触发计算,进而在初始化阶段加入本来不需要的副作用。

把验证样本放在每一道边上

验证这类问题,随意输入几个大数的收益很低。应该围绕每个起始点准备前一单位、当前值和后一单位,再单独覆盖最小值、最大值与非法输入。由于单位是整数克,边界两侧的距离明确,不需要讨论浮点误差;若改成千克小数,应先重新设计归一化方式。

输入重量预期起始键预期名称
00轻件档
9990轻件档
10001000标准件档
29991000标准件档
30003000加固件档
200003000加固件档

等值命中与严格前驱的边界对照

表中的结果可以由规则直接推导。调试时先观察输入转换后的数值,再观察等值检查是否成立,最后观察前驱键;不要一上来打印整个页面状态。若九百九十九正确而一千错误,重点查等号;若三千正常而三万还被接受,重点查业务范围;若所有数值顺序都异常,才检查键类型与比较器。

再做一次配置变更练习:把标准件起始点从一千移动到一千二百。需要修改构造函数的规则和对应样本,页面查询逻辑不应变化。若页面也藏着一套区间判断,这次变更就会出现双点维护。这个练习比增加更多界面按钮更能检验模块职责是否干净。

重复键也值得单独讨论。同一个起始点再次写入时,容器更新对应值,不会自动保留两条不同规则。因此动态规则导入时,应在构建映射前检查重复阈值,不能用最终长度正常来推断来源没有冲突。本文规则写在构造函数中,暂不增加外部配置入口,避免把边界查询扩大成另一个数据导入系统。

容器的有序性给出的是可重复的比较关系,业务仍然负责解释这个顺序。切换成降序比较器以后,“前面”与“更小”不能再凭习惯混用。本例把自然升序作为明确约定,所有示意图和测试值都依照这个约定,维护时应把比较器与分档规则一起评审。

还有一种不容易发现的输入错误,是把称重设备传来的数值当成已经统一过单位。接口返回一点五,可能表示千克,也可能表示业务编码。接入外部数据时,应先在入口明确单位并完成换算,再进入分档模块;否则容器会忠实执行错误的数值规则,调试日志也可能看起来十分合理。

是否接受零重量属于产品决定,不属于容器约束。本文保留零作为下界,用来让区间示例完整;如果真实业务要求正重量,可以在校验中拒绝零,同时保留规则的零起始点。输入是否允许与规则从哪里开始是两个问题,不必因为禁用一个输入就删除整个首档。

比较器还需要保持一致性。对于任意两个不同重量,应有明确先后;不能一会儿按数字大小,一会儿按显示名称排序,也不要让比较结果依赖会变化的页面开关。如果规则需要另一种排序展示,可以从描述数据生成单独的展示视图,不能在同一个容器中途修改比较原则。

名称变化与阈值变化的风险不同。把“轻件档”改成更容易理解的文字,改变的是展示;把起始点向右移动,改变的是区间归属。测试应针对后者重新覆盖相邻两侧,而前者关注文案和数据展示。将两类变更分开审查,能够避免每次改字都重复所有操作,却漏掉真正需要检查的边界。

如果未来允许用户新增规则,插入之前必须确认新起始点在合法范围内、名称非空,并决定覆盖同键是否需要确认。批量构建时可以先写入临时映射,完整校验后再替换当前规则对象,避免界面读取到只装了一半的档位。这里提出的是扩展时的提交方式,不把尚未实现的设置页面画进当前效果图。

有序树也不自动解决跨线程共享。当前模型由页面同步调用,计算非常短,不需要为了使用容器再引入任务池。若后续把规则交给后台任务,应传递明确的规则快照或按所用并发接口要求处理数据,不能因为查询函数没有修改内容,就推断任何对象都能任意跨线程使用。

最后,把失败样本留在评审说明里会很有帮助。“一千克必须进入标准件档”比“修好了边界问题”更容易被后续维护者复核。它既说明等号的方向,也说明规则的业务含义。代码审查时只要沿着这个样本走一遍,就能发现直接使用前驱接口的简化是否再次丢掉了等值分支。

这次错误的修复只有一处等值分支,但其成立依赖四个条件:单位统一、键按自然升序、区间起点包含自身、规则存在最小起始点。把条件写进模块和样本,比让页面多显示一句成功提示更可靠。遇到相似问题时,可以先把业务区间翻译成大小关系,再去寻找容器接口,而不必从控件刷新开始排查。

参考资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐