每周技巧 #94:调用点可读性和 bool 参数

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #94: Callsite Readability and bool Parameters

原文最初作为 totw/94 发布于 2015 年 4 月 27 日。

作者:Geoff Romer

修订于 2017-10-25。

“在诸多虚假文化形式中,过早与抽象打交道,也许最容易致命地损害……智力活力的成长。”– George Boole

假设你遇到这样的代码:

1
2
3
int main(int argc, char* argv[]) {
  ParseCommandLineFlags(&argc, &argv, false);
}

你能看出这段代码做什么吗?尤其是,最后一个参数是什么意思?现在再假设你以前见过这个函数,知道最后一个参数和调用后命令行 flag 是否留在 argv 中有关。你能看出 true 和 false 分别表示什么吗?

当然你不知道,因为这是个假设场景。但即使在真实代码中,我们的大脑也有比记住每个函数参数含义更值得做的事,我们的时间也有比查阅遇到的每个函数调用文档更值得做的事。我们必须能仅仅通过看调用点,就对函数调用的含义做出相当不错的猜测。

选得好的函数名是让函数调用可读的关键,但它们往往还不够。我们通常需要实参本身给出一些含义线索。例如,如果你以前从没见过 string_view,可能不知道如何理解 absl::string_view s(x, y);,但 absl::string_view s(my_str.data(), my_str.size());absl::string_view s("foo"); 就清楚得多。bool 参数的问题在于,调用点处的实参经常只是字面量 truefalse,而这不会给读者任何关于参数含义的上下文线索,就像我们在 ParseCommandLineFlags() 例子中看到的那样。如果有多个 bool 参数,问题会加重,因为你还得额外弄清哪个参数对应哪个含义。

那么如何修复示例中的代码?一种(糟糕的)可能做法是这样:

1
2
3
int main(int argc, char* argv[]) {
  ParseCommandLineFlags(&argc, &argv, false /* preserve flags */);
}

这种方法的缺点很明显:不清楚这个注释是在描述参数的含义,还是描述实参的效果。换句话说,它是在说我们正在保留 flag,还是在说“我们正在保留 flag”这件事为 false?即使注释设法说清楚了,仍然存在注释和代码不同步的风险。

更好的方法是在注释中指定参数名:

1
2
3
int main(int argc, char* argv[]) {
  ParseCommandLineFlags(&argc, &argv, /*remove_flags=*/false);
}

这清楚得多,也更不容易与代码不同步。Clang-tidy 甚至会检查注释是否使用了正确的参数名。另一种歧义更少但更长的变体,是使用解释性变量:

1
2
3
4
int main(int argc, char* argv[]) {
  const bool remove_flags = false;
  ParseCommandLineFlags(&argc, &argv, remove_flags);
}

不过,解释性变量名不会由编译器检查,因此它们可能是错的。当你有多个 bool 参数,而调用方可能把它们调换顺序时,这尤其成问题。

所有这些方法还依赖程序员持续记得添加这些注释或变量,并且正确添加(虽然 clang-tidy 会检查参数名注释的正确性)。

在许多情况下,最好的解决方案是一开始就避免使用 bool 参数,改用 enum。例如,ParseCommandLineFlags() 可以这样声明:

1
2
3
enum ShouldRemoveFlags { kDontRemoveFlags, kRemoveFlags };

void ParseCommandLineFlags(int* argc, char*** argv, ShouldRemoveFlags remove_flags);

这样调用可以写成:

1
2
3
int main(int argc, char* argv[]) {
  ParseCommandLineFlags(&argc, &argv, kDontRemoveFlags);
}

你也可以使用 TotW 86 中描述的 enum class,但这种情况下可能会想使用稍微不同的命名约定,例如:

1
2
3
4
5
enum class ShouldRemoveFlags { kNo, kYes };

int main(int argc, char* argv[]) {
  ParseCommandLineFlags(&argc, &argv, ShouldRemoveFlags::kNo);
}

显然,这种方法必须在定义函数时实现;你并不能真正在调用点选择加入它(可以伪装一下,但收益很小)。所以,当你定义函数时,尤其是这个函数会被广泛使用时,你有责任认真思考调用点会长什么样,特别是要非常怀疑 bool 参数。

每周技巧 #93:使用 `absl::Span`

上一节

每周技巧 #99:非成员接口礼仪

下一节


本节目录