关于代码中复数化的故事(2 Items vs 2 Boxes)
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
结果大多是正确的(boxes、cities、classes、apples),但 mouse → mouses 显得格外突出。异常名词无法仅靠简单规则解决,每出现一个新的边缘情况,代码就会变得更长且更脆弱。
“偶数很难” —— 这句梗在此情境下显得尤为贴切。
此时我意识到,我的“做到完美”好奇心正碰到语言本身的局限。
发现 ICU4J
我搜索了解决方案,发现了 ICU4J,这是一款在生产环境中用于国际化和复数化的 Java 库。然而,在更仔细地阅读文档后,我意识到 ICU4J 实际上并不会改变单词的拼写。它决定 何时 使用单数形式或复数形式,例如:
You have 1 item
You have 2 items
它 不会 自动将 box 变为 boxes,或将 child 变为 children。它解决的是 类别 问题,而不是 单词屈折(词形变化)问题。
诱人的想法
失望之余,我考虑了另一种方法:
如果我直接让 AI 来做这件事呢?
现代 AI 模型在语言方面表现出色。它们知道:
box→boxesclass→classesmouse→micechild→children
理论上,我可以进行一次 API 调用,传入名词,然后得到正确的复数形式。无需语法规则,也不需要不规则名词列表——只需让模型处理复杂性。
为什么 AI 思路实际上是有道理的(起初)
这种做法并不愚蠢;它有真实的优势,因为 AI 模型:
- 能理解不规则名词
- 能处理外来词和边缘情况
- 能自然地适应语言
- 我只需要写极少的代码
对于一个小型个人项目来说,这个想法确实很有吸引力。我根本不需要维护复数化逻辑——只要把问题交给 AI 的 API 即可。
但随后我停下来,开始像系统设计师而不是单纯的程序员那样思考。
第一个警示信号:成本
复数化是低价值、高频率的操作。如果每次用户向购物车添加商品都要向 AI 服务发起网络请求,累计的成本(无论是金钱还是延迟)很快就会超过便利性。
购物车需要做的事:
- 发起网络请求
- 为 token 付费
- 等待响应
这些成本会迅速累加。为决定是输出 boxes 还是 items 而为 AI 推理付费,难以令人信服,尤其是同样的操作本地完全可以零成本完成。
对于小脚本或实验来说,这可能还算可以。但在规模化时,成本会非常快地飙升。
第二个警示信号:延迟
复数化位于用户反馈的关键路径上。它发生在:
- 渲染 UI 时
- 打印输出时
- 快速交互过程中
一次 AI 调用会带来:
- 网络延迟
- 超时风险
- 重试逻辑
- 你无法控制的失败模式(例如 500 错误)
一个简单的打印句子瞬间就依赖于外部服务的可用性,这显然不对。
第三个警示信号:非确定性
这是最大的问题。语言本身是灵活的,AI 也会反映这种灵活性。例如:
cactus→cacticactus→cactuses
两者都是正确的,但如果应用先输出:
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 有帮助,应该用于支持复数的消息,而不是用于把每个名词拼写得完全正确。
- 采用受控词汇是制胜之道:它安全、可预测且易于维护。
思考
从 apple → apples 的过程到理解为何在应用中很少出现 2 boxes,这不仅是复数化的教训,更是关于设计可靠软件系统的教训,即使人类语言不配合。
有时,最简单的解决方案——比如显示 2 items——也是最优雅的。