本周提示 #94: 调用点的可读性和 bool 参数

最初以 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 类型的参数,往往会使代码变得难以理解和错误多发。为了提高代码的可读性,可以考虑以下几点:

  1. 避免使用 bool 参数bool 参数通常会使函数的意图模糊不清,因为函数的行为取决于布尔值的真假,但布尔值的含义可能不明确。例如,SetFlag(true)SetFlag(false) 可能在语义上相似,但具体意义却不明确。
  2. 使用枚举代替 bool 参数:通过使用枚举类型,你可以显著提高代码的可读性。枚举能够明确表示函数参数的具体选项,从而使函数调用的意图更加清晰。例如:

    ​enum class Mode { kRead, kWrite };
    
    void OpenFile(Mode mode);


    这样,函数调用时会明确传递 kReadkWrite,而不是布尔值 truefalse,从而使函数的行为更加明确。            

  3. 使用命名参数:在函数签名中为布尔参数提供明确的命名,可以帮助提高代码的自解释性。例如:

    void SetFlag(bool enableLogging);

    通过这种方式,调用函数时 enableLogging 的含义比单纯的 truefalse 更为明确。

  4. 分解函数:如果一个函数接受多个布尔参数,这可能是一个信号,表明函数的职责过于复杂。考虑将函数分解为多个具有更清晰职责的小函数。例如:

    void Configure(bool enableLogging, bool enableDebug);

    可以被重构为:

    void EnableLogging();
    void EnableDebug();
  5. 文档化函数:确保函数的文档明确说明了每个参数的作用和预期值,尤其是布尔类型的参数。这有助于其他开发者更好地理解函数的使用方法和目的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值