关于代码中复数化的故事(2 Items vs 2 Boxes)

发布: (2026年1月5日 GMT+8 21:20)
9 min read
原文: Dev.to

Source: Dev.to

介绍

我在编写一个简单的控制台应用程序——一个小型购物车。它并不复杂,只是用来尝试输入、计算和格式化输出。下面是我写的第一个版本:

import java.util.Scanner;

public class Cart {
    static void cart() {
        Scanner scanner = new Scanner(System.in);

        System.out.print("Enter item name: ");
        String item = scanner.nextLine();

        System.out.print("Enter price for each: ");
        double price = scanner.nextDouble();

        if (price  1 
        && !"aeiou".contains("" + word.charAt(word.length() - 2))) {
        return word.substring(0, word.length() - 1) + "ies";
    }

    return word + "s";
}

运行几个测试

System.out.println(pluralize("box", 2));    // boxes
System.out.println(pluralize("city", 2));   // cities
System.out.println(pluralize("class", 2));  // classes
System.out.println(pluralize("apple", 2)); // apples
System.out.println(pluralize("mouse", 2));  // mouses

结果大多是正确的(boxescitiesclassesapples),但 mouse → mouses 显得格外突出。异常名词无法仅靠简单规则解决,每出现一个新的边缘情况,代码就会变得更长且更脆弱。

“偶数很难” —— 这句梗在此情境下显得尤为贴切。

此时我意识到,我的“做到完美”好奇心正碰到语言本身的局限。

发现 ICU4J

我搜索了解决方案,发现了 ICU4J,这是一款在生产环境中用于国际化和复数化的 Java 库。然而,在更仔细地阅读文档后,我意识到 ICU4J 实际上并不会改变单词的拼写。它决定 何时 使用单数形式或复数形式,例如:

You have 1 item
You have 2 items

不会 自动将 box 变为 boxes,或将 child 变为 children。它解决的是 类别 问题,而不是 单词屈折(词形变化)问题。

诱人的想法

失望之余,我考虑了另一种方法:

如果我直接让 AI 来做这件事呢?

现代 AI 模型在语言方面表现出色。它们知道:

  • boxboxes
  • classclasses
  • mousemice
  • childchildren

理论上,我可以进行一次 API 调用,传入名词,然后得到正确的复数形式。无需语法规则,也不需要不规则名词列表——只需让模型处理复杂性。

为什么 AI 思路实际上是有道理的(起初)

这种做法并不愚蠢;它有真实的优势,因为 AI 模型:

  • 能理解不规则名词
  • 能处理外来词和边缘情况
  • 能自然地适应语言
  • 我只需要写极少的代码

对于一个小型个人项目来说,这个想法确实很有吸引力。我根本不需要维护复数化逻辑——只要把问题交给 AI 的 API 即可。

但随后我停下来,开始像系统设计师而不是单纯的程序员那样思考。

第一个警示信号:成本

复数化是低价值、高频率的操作。如果每次用户向购物车添加商品都要向 AI 服务发起网络请求,累计的成本(无论是金钱还是延迟)很快就会超过便利性。

购物车需要做的事:

  • 发起网络请求
  • 为 token 付费
  • 等待响应

这些成本会迅速累加。为决定是输出 boxes 还是 items 而为 AI 推理付费,难以令人信服,尤其是同样的操作本地完全可以零成本完成。

对于小脚本或实验来说,这可能还算可以。但在规模化时,成本会非常快地飙升。

第二个警示信号:延迟

复数化位于用户反馈的关键路径上。它发生在:

  • 渲染 UI 时
  • 打印输出时
  • 快速交互过程中

一次 AI 调用会带来:

  • 网络延迟
  • 超时风险
  • 重试逻辑
  • 你无法控制的失败模式(例如 500 错误)

一个简单的打印句子瞬间就依赖于外部服务的可用性,这显然不对。

第三个警示信号:非确定性

这是最大的问题。语言本身是灵活的,AI 也会反映这种灵活性。例如:

  • cactuscacti
  • cactuscactuses

两者都是正确的,但如果应用先输出:

You bought 2 cacti

随后又输出:

You bought 2 cactuses

就会出现不一致。一个“有时正确”的答案往往比始终保持一致的简单答案更糟糕。

第四个警示信号:控制与安全

要使用 AI 对单词进行复数化,我必须把用户输入发送到外部服务。这会引发以下问题:

  • 如果商品名称包含敏感信息怎么办?
  • 如果这些数据必须保留在设备本地怎么办?
  • 如果 API 行为发生变化会怎样?

于是,一个简单的控制台程序瞬间牵涉到:

  • 网络访问
  • API 密钥
  • 隐私考虑

此时情况变得清晰:使用 AI 进行复数化虽然解决了一个语言难题,却引入了成本、延迟、不一致、依赖性,并且仍然无法保证完美的结果。

实际的取舍

我必须退后一步,问自己程序的真正目标是什么:

  • 显示购买的商品数量
  • 显示总费用

产品名称的复数形式拼写并不关键。

这时我恍然大悟——几乎我使用的所有应用都这么做。购物网站、通知系统和仪表盘都不会尝试对任意名词进行复数化。它们只使用一个安全、受控的名词:

You bought 2 items
You have 5 notifications
Your cart contains 3 products

这种方式可预测、可靠,并且易于扩展,因为没有需要处理的边缘情况,而且如果以后将应用翻译成其他语言,也能完美工作。

最终代码片段

以下是我在反思后原始购物车代码的演变过程:

double total = price * quantity;

String message = quantity == 1 ? "item" : "items";

System.out.printf(
    "You bought %d %s. Your grand total is KES %.2f.%n",
    quantity, message, total
);

输出

You bought 1 item. Your grand total is KES 100.00.
You bought 2 items. Your grand total is KES 200.00.

教训

  • 好奇心很重要。尝试改进代码会让你学到很多关于编程、语言和权衡的知识。
  • 语言是混乱的;即使是简单的复数形式也可能有数十种边缘情况。
  • 生产代码重视简洁、正确性和一致性。
  • 在合适的场景下使用 ICU4J 有帮助,应该用于支持复数的消息,而不是用于把每个名词拼写得完全正确。
  • 采用受控词汇是制胜之道:它安全、可预测且易于维护。

思考

appleapples 的过程到理解为何在应用中很少出现 2 boxes,这不仅是复数化的教训,更是关于设计可靠软件系统的教训,即使人类语言不配合。

有时,最简单的解决方案——比如显示 2 items——也是最优雅的。

Back to Blog

相关文章

阅读更多 »

我的 TicketDesk 系统

简介:在我的编程入门模块中,我用 Java 开发了一个 TicketDesk 系统,能够:- 跟踪工单 - 跟踪登录信息 - 提供基于角色的身份验证……