永远不要使用 ELSE 语句,编写更好的代码并成为更好的程序员
过去几年我一直是一名专业程序员。在此期间,我的晋升非常迅速。我最初是一名实习生,但现在我已经是首席工程师,负责一套服务于140多个国家超过16万用户的产品。
最近,我回顾了这些年来我编写的所有代码(我仍然可以访问)。我用各种各样的语言编写过生产代码,包括 Haskell、Scala、Go、Python、Java 和 JavaScript。在所有这些代码中,我注意到一个明显的趋势:我几乎从不使用 ELSE 语句。
不过,我意识到我厌恶 else 语句背后有一个明确的理由。我认为它们不应该被使用,而应该被视为一种代码异味。我这样认为有两个原因:else 语句违反了视线规则,而且它们总是缺乏上下文。在展示如何避免使用 else 语句之前,我会详细解释这两点。
视线规则#
我坚信,代码应该优化以便未来人类阅读,而不是优化以便机器执行。在这一点上,我赞同 Donald Knuth 的观点:
“程序是供人类阅读的,只是偶尔供计算机执行。”——唐纳德·克努斯,《计算机编程艺术》。
问题在于,代码的可读性是主观的:很难准确定义什么才能使代码可读。不过,有一条规则可以澄清这一点,那就是视线规则。这是 Go 社区中一条流行的规则。Mat Ryer在他的演讲和文章中对其进行了简明的定义。简而言之,就是代码中的“快乐路径”应该尽可能少地缩进。
相反,任何错误处理或特殊情况的代码都应该进一步缩进。
任何遵循此规则的代码都具有一个独特的属性:只需扫描缩进最少的代码即可了解任何代码的功能。扫描缩进较多的代码则能显示所有可能发生的特殊情况和错误。这使得代码一目了然,非常容易理解。
那么 else 语句与此有何关系?
Else 语句存在问题,因为它们会强制代码缩进一层。这会让人突然搞不清楚哪些代码与“快乐路径”相关,以及哪些是特殊情况。
这种缺乏清晰度的情况使得代码更难扫描,并损害了可读性。
缺乏上下文
快速高效地扫描代码的能力至关重要。独立地理解小段代码是其中的关键。我们不想为了理解代码库的一小部分而总是阅读每一行代码。
Else 语句使这一点更加困难,因为它们将条件和受其影响的代码分隔开来if
。最好通过两个示例来解释。首先,你能说出运行这三行代码时会发生什么吗?
if myVariable == nil {
return “”
}
希望以上解释足够清晰。不过,我们来看一个对比的例子:
} else {
return “”
}
我们可以看到,如果没有这个if
语句,我们就无法确定它到底想做什么。为什么它会返回一个空字符串?这是错误,还是“正常”行为?这段代码依赖于我们记住并阅读之前的上下文。当语句很短时,这无关紧要,但如果代码块中逻辑复杂if { … }
,或者我们快速浏览,那么将上下文与代码分离会严重损害可读性。当 if/else 语句嵌套,或者一个函数中有多个 if/else 语句时,这种情况会更加严重(这个 else 语句是针对哪个 if 语句的?)。
如何删除 else 语句?#
现在我们已经同意 else 语句很垃圾了。但这本身并没有多大帮助。真正的技巧在于如何避免它们。值得庆幸的是,有两种简单的方法可以做到这一点:
- 反转
if
条件并提前返回,并且, - 创建辅助函数。
反转条件
这是我遇到的最常见的情况。它也有两种形式——一种else
是隐式的,一种是显式的。显式版本如下所示:
func doSomething() error {
if something.OK() {
err := something.Do()
if err != nil {
return err
}
} else {
return nil, errors.New("something isn't ok")
}
}
隐式函数类似,但本身不包含else
任何语句。相反,它else
只是通过简单地删除函数末尾来隐式地表示(这种隐式函数在 Python 或 JavaScript 中更常见,如果没有明确声明,则返回None
或)。undefined
function doSomething() {
if (something.OK()) {
return something.Do()
}
}
再次强调,这并不能完全清晰地展现该行为的全部内容。如果不阅读整个函数,返回值就无法清晰。
通过简单地反转if
条件,我们就可以解决所有这些问题。
function doSomething() {
if (!something.OK()) {
// return or throw error
}
return something.Do()
}
现在我们可以扫描这个函数,清楚地看到缩进的错误条件和正常流程,满足视线规则。行为完全明确,并且没有上下文分离。这好多了。
辅助函数
我们也遇到了一些不直接导致 的 else 语句return
。这通常是由于一些未正确隔离的特殊情况逻辑造成的。例如
let charities
if (country != "") {
if (tier != "") {
charities = getCharitiesByCampaignCountryAndTier(campaign, country, tier)
} else {
charities = getCharitiesByCampaignAndCountry(campaign, country)
}
} else {
charities = getCharitiesByCampaign(campaign)
}
// do something with charities
通过将慈善募捐逻辑合并到单独的函数中,可以提高代码的可读性。这样可以适当处理特殊情况,并提前返回。通过反转一些 if 语句,可以进一步改进代码的可读性。
例如:
function getCharities(campaign, country, tier) {
if (country == "") {
return getCharitiesByCampaign(campaign)
}
if (tier == "") {
return getCharitiesByCampaignAndCountry(campaign, country)
}
return getCharitiesByCampaignCountryAndTier(campaign, country, tier)
}
这个辅助函数巧妙地封装了我们需要的所有逻辑,消除了对任何 else 语句的需求,并且更好地将快乐路径代码保持在左侧。这使得代码更容易浏览,并且可读性也更高。
结论#
Else 语句是一种奇怪的代码异味。它们强制错误处理和快乐路径的缩进级别相同,从而损害了所有代码的可读性。它们还具有将代码与影响代码的逻辑分离的独特能力。通过提前返回和将逻辑拆分为辅助函数这两种技巧,可以轻松避免使用 Else 语句。因此,它们是不必要的。通过避免使用 Else 语句,您可以编写更好的代码,并成为一名更优秀的程序员。
一些警告(以阻止学究们)。
- 在 SQL 中,CASE WHEN ... ELSE ... 实际上是无法避免的。
- 在 Scala 中,隐式返回(避免使用返回语句来实现引用透明性)意味着您必须使用它们 - 您实际上没有“提前返回”的能力。
- 三元运算符很好。
- 在python中,三元运算符使用
else
。这也很好。