最初以 totw/94 发布于 2015-04-27
作者:Geoff Romer (gromer@google.com)
2017-10-25 修订
"在多种形式的虚假文化中,过早地与抽象概念进行交流或许最有可能对......智力活力的增长造成致命的影响"。- 乔治-布尔
假设你遇到这样的代码
int main(int argc, char* argv[]) {
ParseCommandLineFlags(&argc, &argv, false);
}
你能说出这段代码的作用,尤其是最后一个参数的含义吗?现在假设你以前遇到过这个函数,并且知道最后一个参数与调用后 argv 中是否留有命令行标志有关。你能分辨出哪个为真,哪个为假吗?
你当然不知道,因为这是假设,但即使是在真实代码中,我们也有比记住每个函数参数的含义更有用的事情要做,也有比去查找我们遇到的每个函数调用的文档更有用的事情要做。我们必须能够仅仅通过查看调用点,就能很好地猜测函数调用的含义。
精心选择的函数名是使函数调用具有可读性的关键,但这往往还不够。我们通常需要参数本身为我们提供一些关于参数含义的线索。例如,如果你以前从未见过 string_view,你可能不知道 absl::string_view s(x, y); 是什么意思,但 absl::string_view s(my_str.data(), my_str.size()); 和 absl::string_view s("foo"); 就清楚多了。bool 参数的问题在于,调用点的参数通常是字面意义上的 "true "或 "false",读者无法从上下文中了解参数的含义,正如我们在 ParseCommandLineFlags() 示例中看到的那样。如果有一个以上的 bool 参数,这个问题就会变得更加复杂,因为现在还需要弄清楚哪个参数是什么。
那么,我们该如何解决类似上述示例的代码问题呢?一种(糟糕的)可能性是像下面这样做:
int main(int argc, char* argv[]) {
ParseCommandLineFlags(&argc, &argv, false /* preserve flags */);
}
这种方法的缺点显而易见:不清楚该注释是在描述参数的含义还是参数的作用。换句话说,它是在说我们在保留标记,还是在说我们保留标记是假的?即使注释能够明确这一点,注释仍然有可能与代码脱节。
更好的办法是在注释中指明参数的名称:
int main(int argc, char* argv[]) {
ParseCommandLineFlags(&argc, &argv, /*remove_flags=*/false);
}
这样就清楚多了,也不容易与代码脱节。Clang-tidy 甚至会检查注释中的参数名称是否正确。另一个不那么含糊但更长的变体是使用一个解释变量:
int main(int argc, char* argv[]) {
const bool remove_flags = false;
ParseCommandLineFlags(&argc, &argv, remove_flags);
}
但是,编译器不会检查解释性变量名,因此它们可能是错误的。如果有多个 bool 参数,调用者可能会调换参数,这就是一个特别的问题。
所有这些方法都依赖于程序员始终记得添加这些注释或变量,并且正确地添加(尽管 clang-tidy 会检查参数名注释的正确性)。
在许多情况下,最好的解决方案是首先避免使用 bool 参数,而是使用枚举参数。例如,ParseCommandLineFlags() 可以这样声明:
enum ShouldRemoveFlags { kDontRemoveFlags, kRemoveFlags };
void ParseCommandLineFlags(int* argc, char*** argv, ShouldRemoveFlags remove_flags);
这样调用看起来就像这样
int main(int argc, char* argv[]) {
ParseCommandLineFlags(&argc, &argv, kDontRemoveFlags);
}
您也可以使用枚举类,如 TotW 86 中所述,但在这种情况下,您需要使用稍有不同的命名约定,例如
enum class ShouldRemoveFlags { kNo, kYes };
…
int main(int argc, char* argv[]) {
ParseCommandLineFlags(&argc, &argv, ShouldRemoveFlags::kNo);
}
显然,这种方法必须在定义函数时实施,而不能在调用时选择使用(可以伪造,但好处不大)。因此,在定义函数时,尤其是当函数将被广泛使用时,您有责任仔细考虑调用点的外观,尤其是对 bool 参数持怀疑态度。
以下一些总结更容易理解
在编写代码时,调用点的可读性非常重要,它直接影响到代码的理解和维护。如果函数的参数不明确,尤其是 bool
类型的参数,往往会使代码变得难以理解和错误多发。为了提高代码的可读性,可以考虑以下几点:
- 避免使用
bool
参数:bool
参数通常会使函数的意图模糊不清,因为函数的行为取决于布尔值的真假,但布尔值的含义可能不明确。例如,SetFlag(true)
和SetFlag(false)
可能在语义上相似,但具体意义却不明确。 -
使用枚举代替
bool
参数:通过使用枚举类型,你可以显著提高代码的可读性。枚举能够明确表示函数参数的具体选项,从而使函数调用的意图更加清晰。例如:enum class Mode { kRead, kWrite }; void OpenFile(Mode mode);
这样,函数调用时会明确传递kRead
或kWrite
,而不是布尔值true
或false
,从而使函数的行为更加明确。 -
使用命名参数:在函数签名中为布尔参数提供明确的命名,可以帮助提高代码的自解释性。例如:
void SetFlag(bool enableLogging);
通过这种方式,调用函数时
enableLogging
的含义比单纯的true
或false
更为明确。 -
分解函数:如果一个函数接受多个布尔参数,这可能是一个信号,表明函数的职责过于复杂。考虑将函数分解为多个具有更清晰职责的小函数。例如:
void Configure(bool enableLogging, bool enableDebug);
可以被重构为:
void EnableLogging(); void EnableDebug();
-
文档化函数:确保函数的文档明确说明了每个参数的作用和预期值,尤其是布尔类型的参数。这有助于其他开发者更好地理解函数的使用方法和目的。