每周技巧 #181:访问 `StatusOr<T>` 的值

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #181: Accessing the value of a StatusOr

原文最初作为 TotW #181 发布于 2020 年 7 月 9 日。

作者:Michael Sheely

更新于 2020 年 9 月 2 日。

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

StatusOr<Readability>:你不必二选一!

当需要访问 absl::StatusOr<T> 对象内部的值时,我们应该努力让这种访问既安全清晰,又高效

注意:本技巧试图强调“光线充足的道路”,为典型用例提供指导。它并不打算穷尽所有情况。如果遇到边界场景,请结合这里的建议和理由自行判断。

建议

访问 StatusOr 所持有的值时,应先调用 ok() 验证值存在,然后通过 operator*operator-> 访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 处理 unique_ptr 时使用的同一模式...
std::unique_ptr<Foo> foo = TryAllocateFoo();
if (foo != nullptr) {
  foo->DoBar();  // 使用值对象
}

// ...或处理 optional 值时...
std::optional<Foo> foo = MaybeFindFoo();
if (foo.has_value()) {
  foo->DoBar();
}

// ...同样也是处理 StatusOr 的理想方式。
absl::StatusOr<Foo> foo = TryCreateFoo();
if (foo.ok()) {
  foo->DoBar();
}

你可以在 if 语句的初始化器中声明 StatusOr,并在条件中检查 ok(),从而限制它的作用域。如果会立即使用一个 StatusOr,通常应该这样限制作用域(见技巧 #165):

1
2
3
if (absl::StatusOr<Foo> foo = TryCreateFoo(); foo.ok()) {
  foo->DoBar();
}

StatusOr 背景

absl::StatusOr<T> 类是一个带值语义带标签联合,表示以下情况之一且仅有一种:

  • 一个类型为 T 的对象可用;
  • 一个 absl::Status 错误(!ok()),说明为什么值不存在。

关于 absl::Statusabsl::StatusOr,可阅读技巧 #76

安全、清晰和高效

StatusOr 对象当作智能指针来处理,有助于代码在保持安全和高效的同时获得清晰性。下面我们会看看你可能见过的其他访问 StatusOr 的方式,以及为什么我们偏好使用间接运算符。

其他值访问器的安全问题

absl::StatusOr<T>::value() 呢?

1
2
absl::StatusOr<Foo> foo = TryCreateFoo();
foo.value().DoBar();  // 行为取决于构建模式。

这里的行为取决于构建模式,尤其取决于代码是否在启用异常的情况下编译。1 因此,读者不清楚错误状态是否会终止程序。

value() 方法组合了两个动作:先测试有效性,再访问值。因此只有在这两个动作都是你想要的情况下才应使用它(即便如此,也请三思,并注意它的行为取决于构建模式)。如果状态已经已知为 OK,那么理想访问器的语义就是单纯访问值,而这正是 operator*operator-> 所提供的。除了让代码更准确表达意图,这种访问也至少与 value() 先测试有效性再访问值的契约一样高效。

避免同一对象有多个名字

absl::StatusOr 对象像智能指针或 optional 值一样处理,也能避免让两个变量指向同一个值的概念尴尬局面。它还避免了由此而来的命名困境和对 auto 的过度使用。

1
2
3
4
5
6
7
// 不翻出 TryCreateFoo() 的声明,读者无法立刻理解这里的类型
// (optional?pointer?StatusOr?)。
auto maybe_foo = TryCreateFoo();
// ...又因使用隐式 bool 而不是 `.ok()` 更加不清楚。
if (!maybe_foo) { /* 处理 foo 不存在 */ }
// 现在两个变量(maybe_foo、foo)代表同一个值。
Foo& foo = maybe_foo.value();

避免 _or 后缀

使用 StatusOr 变量在检查有效性后直接访问其内在值类型(而不是为同一个值创建多个变量)的另一个好处是,我们可以给 StatusOr 使用最好的名字,而不需要(也不会被诱惑)添加前缀或后缀。

这里可以类比命名指针变量时避免使用 _ptr 后缀

1
2
3
4
5
6
7
8
// 类型已经说明这是 unique_ptr;`foo` 就很好。
std::unique_ptr<Foo> foo_ptr;

absl::StatusOr<Foo> foo_or = MaybeFoo();
if (foo_or.ok()) {
  const Foo& foo = foo_or.value();
  foo.DoBar();
}

如果只有一个变量(这个 StatusOr,并避免为解包后的值创建第二个具名变量),我们就可以去掉后缀,直接按底层值命名变量(就像给指针命名一样)。

1
2
3
4
5
const absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
  MakeUseOf(*foo);
  foo->DoBar();
}

absl::StatusOr 的值中移动

我们可能会写代码从 absl::StatusOr<T>T 中移动:

1
2
3
4
absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
  ConsumeFoo(std::move(*foo));
}

不过,更好一点的方式是从 StatusOr 本身移动,这向读者(包括人和机器)表明整个 StatusOr 对象在其值被移走后都不应再使用:

1
2
3
4
absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
  ConsumeFoo(*std::move(foo));
}

解决方案

像处理智能指针或 optional 一样测试 absl::StatusOr 对象是否有效,并使用 operator*operator-> 访问它,是可读、高效且安全的。

这能帮助你避免上面提到的命名歧义陷阱,而且不需要使用任何宏。

通过 operator*operator-> 访问值的代码(无论是指针、StatusOroptional 还是其他类型)都必须先验证值存在。这个验证应该靠近访问值的位置,这样读者可以轻松确认该值存在且正确。

每周技巧 #180:避免悬垂引用

上一节

每周技巧 #182:初始化你的整数!

下一节

  1. 根据 value() 函数的文档,如果启用异常,它会抛出 absl::BadStatusOrAccess(可能被捕获,因此程序不一定终止)。如果在禁用异常的情况下编译,代码会通过 LOG(FATAL) 崩溃。 ↩︎