作为TotW#181最初发表于2020年7月9日
由Michael Sheely创作
当需要访问 absl::StatusOr 对象中的值时,我们应该努力使该访问安全、清晰和高效。
注意:本提示试图强调提供典型用例的指导,但并非详尽无遗。如果遇到边缘情况,请考虑本文中的建议和推理,并行使判断力。
通过 operator* 或 operator-> 访问 StatusOr 持有的值,在调用 ok() 确认值存在后执行。
// 处理 unique_ptr 时使用的相同模式…
std::unique_ptr<Foo> foo = TryAllocateFoo();
if (foo != nullptr) {
foo->DoBar(); // 使用值对象
}
// ...或可选值...
std::optional<Foo> foo = MaybeFindFoo();
if (foo.has_value()) {
foo->DoBar();
}
// ...同样适用于处理 StatusOr。
absl::StatusOr<Foo> foo = TryCreateFoo();
if (foo.ok()) {
foo->DoBar();
}
您可以通过在 if 语句的初始化器中声明并在条件中检查 ok() 限制 StatusOr 的作用域。如果立即使用 StatusOr,则通常应以此方式限制作用域(参见 Tip #165):
if (absl::StatusOr<Foo> foo = TryCreateFoo(); foo.ok()) {
foo->DoBar();
}
absl::StatusOr 类是一个带有值语义的标记联合,表示以下情况之一:
类型为 T 的对象可用,
absl::Status 错误(!ok()),表示为什么值不存在。
您可以在 Tip #76 中了解有关 absl::Status 和 absl::StatusOr 的更多信息。
将 StatusOr 对象视为智能指针一样处理,可以帮助代码实现清晰而安全的效果,同时保持高效。在下面,我们将考虑您可能已经看到的一些其他访问 StatusOr 的方式,以及为什么我们更喜欢使用间接操作符的方法。
那么 absl::StatusOr::value() 怎么办?
absl::StatusOr<Foo> foo = TryCreateFoo();
foo.value(); // 行为取决于构建模式。
在这里,行为取决于构建模式-特别是是否启用异常编译。1 因此,读者不清楚错误状态是否会终止程序。
value() 方法结合了两个操作:有效性测试后跟值的访问。因此,仅在两个操作都打算使用时才应使用它(即使是这样,请再三考虑并考虑到它的行为取决于构建模式)。如果状态已知为 OK,则您的理想访问者具有简单访问值的语义,这正是 operator* 和 operator-> 提供的。除了使代码更准确地指示您的意图外,访问将至少与 value() 的合同一样有效地测试有效性,然后访问值。
将 absl::StatusOr 对象视为智能指针或可选值一样处理,还可以避免概念上尴尬的情况,即有两个变量引用同一个值。它还避免了命名困境和自动使用问题。
//如果不查看TryCreateFoo()的声明,读者将无法立即理解这里的类型(可选?指针?StatusOr?)。
auto maybe_foo = TryCreateFoo();
// ...加上了使用隐式布尔而不是`.ok()`的复杂性。
if (!maybe_foo) { /* 处理foo不存在 */ }
// 现在两个变量(maybe_foo,foo)表示相同的值。
Foo&foo = maybe_foo.value();
使用 StatusOr 变量的内在值类型(而不是为相同值创建多个变量)并在检查有效性后,另一个好处是我们可以使用最佳的 StatusOr 名称,而无需添加前缀或后缀(或者说不会有这种诱惑!)。
这里有一个类比,就像在命名指针变量时避免使用“_ptr”后缀一样。
// 类型已经描述了这是一个 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),避免使用第二个命名变量来获取未包装的值,我们可以删除后缀,仅根据其基础值命名变量(就像我们在处理指针时一样)。
const absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
MakeUseOf(*foo);
foo->DoBar();
}
我们可能编写代码从 absl::StatusOr 的 T 中移动:
absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
ConsumeFoo(std::move(*foo));
}
不过,更好的方法是从 StatusOr 本身移动,向读者(无论是人还是机器)表明在其值被移动后,整个 StatusOr 对象都不应再被使用:
absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
ConsumeFoo(*std::move(foo));
}
测试 absl::StatusOr 对象的有效性(就像智能指针或 optional 一样),并使用 operator* 或 operator-> 访问它是可读性、效率和安全的。
它可以避免上述命名模糊的潜在问题,而且完全不需要使用任何宏。
通过 operator* 或 operator-> 访问值的代码(无论是使用指针、StatusOr、optional 还是其他方式)必须先验证该值是否存在。这个验证应该接近访问该值的位置,以便读者可以轻松地验证它是否存在且正确。
根据 value() 函数的文档,如果启用了异常,则会抛出 absl::BadStatusOrAccess 异常(该异常可以被捕获,这意味着程序可能不会终止)。如果没有启用异常,则代码将崩溃并输出 LOG(FATAL)。 ↩