每周技巧 #161:好的局部变量和坏的局部变量

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #161: Good Locals and Bad Locals

原文最初作为 TotW #161 发布于 2019 年 4 月 16 日。

作者:James Dennett

更新于 2020 年 4 月 6 日。

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

我们也许会在全局范围内惊慌失措,但承受痛苦是在局部。—— Jonathan Franzen

概要

局部变量很好,但也可能被过度使用。我们常常可以通过把局部变量限制在能提供特定收益的场景中,来简化代码。

建议

只有当下面一项或多项成立时,才使用局部变量:

  • 它们的名字增加了有用文档。
  • 它们简化了过度复杂的表达式。
  • 它们把重复表达式抽取出来,让人类(以及较小程度上让编译器)清楚知道每次都是同一个值。
  • 对象生命周期需要跨越多个语句(例如,对该对象的引用会保留到单个语句结束之后,或者变量保存一个会在其生命周期内更新的值)。

其他情况下,可以考虑移除一层间接性:消除局部变量,并在使用处直接写表达式。

理由

给值命名会给代码理解增加一层间接性,除非变量名完全捕捉了其含义中相关的方面。在 C++ 中给值一个名字,也会把它暴露给作用域的其余部分。它还会影响“值类别”,因为每个具名变量都是左值,即使它声明为右值引用并由右值初始化。这可能需要额外使用 std::move,而代码评审时需要小心这些用法,避免 use-after-move bug。考虑到这些缺点,局部变量最好保留给能提供特定收益的场景。

示例:局部变量的糟糕用法

消除立即返回的局部变量

作为消除无帮助局部变量的简单例子,与其写:

1
2
MyType value = SomeExpression(args);
return value;

不如写:

1
return SomeExpression(args);

把被测表达式内联进 GoogleTest 的 EXPECT_THAT

1
2
std::vector<string> actual = SortedAges(args);
EXPECT_THAT(actual, ElementsAre(21, 42, 63));

这里变量名 actual 没有增加任何有用信息(EXPECT_THAT 总是把实际值作为第一个参数),它没有简化复杂表达式,而且它的值只使用一次。像下面这样内联表达式:

1
EXPECT_THAT(SortedAges(args), ElementsAre(21, 42, 63));

可以一眼看出正在测试什么;并且通过不给 actual 命名,确保它不会被无意复用。它还允许测试框架在错误输出中显示失败调用。

注意:较短版本隐藏了 SortedAges 的期望类型。如果验证类型很重要,可以考虑声明变量以显示其类型。

使用 matcher 消除测试中的变量

Matcher 可以让 EXPECT_THAT 直接表达我们对某个值的全部期望,从而帮助避免在测试中命名局部变量。不要写这样的代码:

1
2
3
4
5
6
7
std::optional<std::vector<int>> maybe_ages = GetAges(args);
ASSERT_NE(maybe_ages, std::nullopt);
std::vector<int> ages = maybe_ages.value();
ASSERT_EQ(ages.size(), 3);
EXPECT_EQ(ages[0], 21);
EXPECT_EQ(ages[1], 42);
EXPECT_EQ(ages[2], 63);

这里我们必须小心写 ASSERT* 而不是 EXPECT* 以避免崩溃。我们可以直接在代码中表达意图:

1
2
EXPECT_THAT(GetAges(args),
            Optional(ElementsAre(21, 42, 63)));

示例:局部变量的良好用法

抽取重复表达式

1
2
3
myproto.mutable_submessage()->mutable_subsubmessage()->set_foo(21);
myproto.mutable_submessage()->mutable_subsubmessage()->set_bar(42);
myproto.mutable_submessage()->mutable_subsubmessage()->set_baz(63);

这里的重复让代码冗长(有时还需要不幸的换行),也可能让读者需要更多努力才能看出这正在设置同一个 proto 消息的三个字段。使用局部变量为相关消息创建别名可以清理它:

1
2
3
4
5
SubSubMessage& subsubmessage =
    *myproto.mutable_submessage()->mutable_subsubmessage();
subsubmessage.set_foo(21);
subsubmessage.set_bar(42);
subsubmessage.set_baz(63);

在某些情况下,这也有助于编译器生成更好代码,因为它不需要证明重复表达式每次返回同一个值。不过要警惕过早优化:如果消除公共子表达式不能帮助人类读者,请先性能分析,再尝试帮助编译器。

给 pair 和 tuple 元素有意义的名字

虽然通常用带有有意义字段名的 struct 比用 pairtuple 更好,但我们可以通过把有意义的别名绑定到它们的元素上,来缓解 pairtuple 的问题。例如,与其写:

1
2
3
4
for (const auto& name_and_age : ages_by_name) {
  if (IsDisallowedName(name_and_age.first)) continue;
  if (name_and_age.second < 18) children.insert(name_and_age.first);
}

在 C++11 中可以写:

1
2
3
4
5
6
7
for (const auto& name_and_age : ages_by_name) {
  const std::string& name = name_and_age.first;
  const int& age = name_and_age.second;

  if (IsDisallowedName(name)) continue;
  if (age < 18) children.insert(name);
}

而在 C++17 中,我们可以更简单地使用“结构化绑定”达到同样的命名效果:

1
2
3
4
for (const auto& [name, age] : ages_by_name) {
  if (IsDisallowedName(name)) continue;
  if (age < 18) children.insert(name);
}

每周技巧 #158:Abseil 关联容器和 `contains()`

上一节

每周技巧 #163:传递 `std::optional` 参数

下一节