(原文链接:https://abseil.io/tips/161 译者:clangpp@gmail.com)
每周贴士 #161: 局部变量有好有坏
- 最初发布于:2019-04-16
- 作者:James Dennett
- 更新于:2020-04-06
- 短链接:abseil.io/tips/161
We may freak out globally, but we suffer locally. – Jonathan Franzen
概要
局部变量挺好的,但是容易被滥用。通过将其限制在它能提供确切好处的地方,我们通常可以让代码更简化。
建议
只有如下一条或多条适用的时候,才使用局部变量:
- 该变量名增加了有用的文档。
- 该变量简化了过于复杂的表达式。
- 该变量提炼出了重复的表达式,让人(以及优化能力稍弱的编译器)一眼就能看出它的值每次都一样。
- 一个对象的生存期需要跨越多个语句(例如,因为对象的引用被保留到单个语句之后,或者因为变量保存了一个在生存期内被更新的值)。
在其他情况下,请考虑去除该局部变量,把表达式直接写在调用点,以减少一层间接性。
考量
为值命名给理解代码增加了一层间接性,除非变量名完全捕捉住了其含义的相关方面。在C++中,给一个值命名,也同时将其暴露给了当前作用域的剩余部分。它还会影响“值类别(value category)”,因为每个命名变量都是一个左值,即使它被声明为右值引用且以右值初始化。这样会需要额外的std::move
来保证代码审查中的关注,以避免“移动后使用(use-after-move)”的错误。基于这些缺点,局部变量的使用最好还是保留在那些它能提供确切好处的地方。
示例:局部变量坏的用法
去除立即返回的局部变量
一个去除无用的局部变量的简单例子是,相比于
MyType value = SomeExpression(args);
return value;
更好的是
return SomeExpression(args);
把待测试的表达式内联进GoogleTest的EXPECT_THAT
auto actual = SortedAges(args);
EXPECT_THAT(actual, ElementsAre(21, 42, 63));
这里变量名actual
没有增加任何有用的信息(EXPECT_THAT
的第一个参数总是实际值),它并没有简化一个复杂的表达式,并且它的值只被使用了一次。将表达式内联如下
EXPECT_THAT(SortedAges(args), ElementsAre(21, 42, 63));
让人一眼就能看出被测试的东西,而且不为actual
命名确保了它不会被意外地重用。这样也给测试框架提供了在报错信息中打印原始表达式的机会。
用匹配器(Matchers)来消除测试代码中的变量
匹配器允许EXPECT_THAT
直接表达我们对一个值的所有期望,因此可以用来避免测试代码中对局部变量命名的需要。比起把代码写成这样
absl::optional<std::vector<int>> maybe_ages = GetAges(args);
ASSERT_NE(maybe_ages, absl::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*
,以避免程序崩溃),我们可以在代码中直接表达意图:
EXPECT_THAT(GetAges(args),
Optional(ElementsAre(21, 42, 63)));
示例:局部变量好的用法
提炼出重复的表达式
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的三个字段。用一个局部变量来给相关的message取个小名,可以让意图更清楚:
auto& subsubmessage = *myproto.mutable_submessage()->mutable_subsubmessage();
subsubmessage.set_foo(21);
subsubmessage.set_bar(42);
subsubmessage.set_baz(63);
注:如果可以提高可读性,那就请显式地指明引用的类型。只有在“避免繁杂的、显而易见的、不重要的类型名——类型并不能帮助读者更清晰地理解代码”的时候才使用auto
。
有时候这也能帮助编译器生成更好的代码,因为编译器不用去证明这段重复的表达式每次都返回相同的值。不过还是要小心过早优化:如果去除一个公共的子表达式不能帮助人类读者,那请在试图帮助编译器之前跑对比实验(profile)(译者注:以证明其真的有效果)。
为Pair和Tuple的元素取有意义的名字
虽然一般情况下使用含有有意义成员名的struct
要比pair
或tuple
更好,但是我们可以通过给pair
或tuple
的成员绑定有意义的别名来缓解这个问题。例如,相比于
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中我们可以写成
for (const auto& name_and_age : ages_by_name) {
const auto& name = name_and_age.first;
const auto& age = name_and_age.second;
if (IsDisallowedName(name)) continue;
if (age < 18) children.insert(name);
}
而在C++17中,我们可以简单地使用“结构化绑定”来达到取有意义名字的效果:
for (const auto& [name, age] : ages_by_name) {
if (IsDisallowedName(name)) continue;
if (age < 18) children.insert(name);
}