每周技巧 #171:避免哨兵值

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #171: Avoid Sentinel Values

原文最初作为 TotW #171 发布于 2019 年 11 月 8 日。

作者:Hyrum Wright

更新于 2020 年 4 月 6 日。

快捷链接:abseil.io/tips/171

哨兵值是在特定上下文中具有特殊含义的值。比如考虑下面这个 API:

1
2
// 返回账户余额;如果账户已关闭,则返回 -5。
int AccountBalance();

-5 之外,int 的每个值都被文档说明为 AccountBalance 的有效返回值。直觉上这有点奇怪:调用者应该只检查 -5,还是任何负值都可以可靠地表示“账户已关闭”?如果系统之后支持负余额,这个 API 又该如何调整才能返回负值?

使用哨兵值会增加调用代码的复杂度。如果调用者很严谨,它会显式检查哨兵值:

1
2
3
4
5
6
int balance = AccountBalance();
if (balance == -5) {
  LOG(ERROR) << "account closed";
  return;
}
// 在这里使用 `balance`

一些调用者可能会检查比规范更宽的取值范围:

1
2
3
4
5
6
int balance = AccountBalance();
if (balance <= 0) {
  LOG(ERROR) << "where is my account?";
  return;
}
// 在这里使用 `balance`

还有一些调用者可能会完全忽略哨兵值,假设它在实践中并不会出现:

1
2
int balance = AccountBalance();
// 在这里使用 `balance`

哨兵值的问题

上面的例子展示了使用哨兵值时的一些常见问题。其他问题还包括:

  • 不同系统可能使用不同的哨兵值,比如单个负值、所有负值、无穷值,或者任意某个值。传达特殊值含义的唯一方式是文档。
  • 哨兵值仍然属于该类型的有效取值域,因此类型系统不会强制调用方或被调用方承认某个值可能无效。当代码和注释不一致时,通常两者都是错的。
  • 哨兵值限制接口演进,因为某个具体哨兵值将来可能变成系统中的有效值。
  • 一个系统的哨兵值可能是另一个系统的有效值。在多个系统交互时,这会增加认知负担和代码复杂度。

忘记检查约定的哨兵值是常见 bug。最好的情况下,未经检查的哨兵值会在运行时立刻让系统崩溃。更常见的是,它会继续在系统中传播,并一路产生错误结果。

改用 std::optional

不要用特殊值表示信息不可用或无效,而应使用 std::optional

1
2
// 返回账户余额;如果账户已关闭,则返回 std::nullopt。
std::optional<int> AccountBalance();

新版 AccountBalance() 的调用者现在必须显式查看返回值内部是否真的有余额,这个动作本身就表达了结果可能无效。除非有额外文档说明,调用者可以假设任何有效的 int 值都可能从该函数返回,而不需要排除某些具体哨兵值。这种简化能让调用代码的意图更清晰。

1
2
3
4
5
6
7
std::optional<int> balance = AccountBalance();

if (!balance.has_value()) {
  LOG(ERROR) << "Account doesn't exist";
  return;
}
// 在这里使用 `*balance`

下次你想在系统中使用哨兵值时,请认真考虑改用合适的 std::optional

相关阅读

  • 关于使用 std::optional 传递参数的更多信息,见 TotW #163
  • 如果你在判断何时使用 std::optional、何时使用 std::unique_ptr,见 TotW #123

每周技巧 #168:`inline` 变量

上一节

每周技巧 #172:指定初始化器

下一节