最近看到这个代码,感觉非常有灵感

Array[2]={"even","odd"}printf(Array[x%2])


问题里这个例子体现了计算的一个本质问题,就是用数据结构的复杂度来换取代码的复杂度。

这个例子本身是 table-driven 的例子。如果 if-else 体现的是一张表,那就 explicitly 构建这张表。OOP 里的虚函数也体现了这个概念。

更复杂的例子是 compiler。不构建 AST 的 compiler 代码生成部分就比较复杂。构建 AST 的代码生成部分就相对简单。

既然很多人提到了复杂度,这里讲一下,本质复杂度(fundamental complexity)只能转移不能降低,但 accidental 复杂度可以降低。而 accidental 复杂度可以从下面的几个方面分析:

  1. 知识更适合用 declarative 的方式表达。从这点来说,数据结构作为 declarative 形式,有可能降低 accidental 复杂度。
  2. 数据结构有可能会降低知识表达的 self-contain 程度。因为数据结构是由另外的代码来解释的。而解释一个 declarative 结构的 generic code 有可能会比简单的 if-else 更复杂。
  3. 所以,数据结构最好有一些除了代码之外,比较明显的自解释表述。这也是软体工程里更强调给数据结构加注释的原因。


想要减少if else的话:

首先明确问题并不在if else上,而是在if else带来的「复杂度增加」上。把if else换成别的写法并不能减少复杂度。相反,少见的写法反而增加「认知负荷」进一步增加了代码的复杂度。

复杂度公式:代码的总复杂度等于代码每一个模块的复杂度之和。

代码总复杂度=sum(代码每一个模块的复杂度)

Array[2]={"even","odd"}

printf(Array[x%2])

if x%2==0:

print(「even」)

else:

print(「odd」)

复杂度是一样的。(原题c,此例python)

其他的改法比如换成问号表达式来写,可以把代码缩减到一行,而且很短。看上去更优雅。此外python等语言也可以把if条件后置,同样把代码缩减。

但是复杂度还是一样的。

单个的if else可以改成问题中例子这样写。但其实单个的if else增加的复杂度并不高。而多层嵌套的if else、与其他流程式控制制语句(各种循环和跳出循环的语句)混用的if else,无法用问号表达式来减少或改写。也无法用如java中的switch等另一些语法来改写。

此外常用的减少if else的方法,比如「及早退出」也有其局限性。先举个及早退出的例子:

if xxxx:

print( 「even」)

if xxxx:

print(「odd」)

这种写法就只有if 没有else。如果用在函数定义里,那么用return取代print,当符合及早退出的条件时,直接退出这个函数。那么后面不管还有多少if都不用看了。

正因为后面的代码不用看了,「及早退出」写法降低了复杂if else所带来的「认知负荷」。因为编写某种条件下的代码的开发者只要阅读符合这个条件的代码就可以了。或者从头阅读到符合他要的条件的代码为止。基于复杂度公式,代码对他来说复杂度下降了,下降的复杂度值=符合他的if条件之后的代码的复杂度。

那这个局限性在哪里呢,局限性在于开发者要的条件如果是最后一个if后面的xxx,那他还是要读全部条件代码。也就是说复杂度可能没有降低。所以及早退出法的效果取决于开发者要改的代码在第几个条件。如果按平均值算,那就是降低了一半的复杂度。当然实际情况可能不一定是一半。

如果要进一步降低复杂度,那么考虑把复杂度高的代码封装成函数。这种方法可以进一步降低复杂度的原因是:

复杂度公式2:如果一段代码在调用他前不需要知道他的内部实现,那么这段代码的复杂度对调用者来说接近于零。

所以问题就可以延伸到怎样设计好编程介面(广义上的)以降低代码复杂度。建议另外开问题讨论了。

最后补充一点:有时候定义函数或改写法带来的复杂度反而高于原来的if else写法。具体问题要具体分析。


1、switch-case

初学者最容易想到的if-else代替方式,其实差不多

switch {
case score &>= 0 score &< 60: fmt.Println("E") case score &>= 60 score &< 70: fmt.Println("D") case score &>= 70 score &< 80: fmt.Println("C") case score &>= 80 score &< 90: fmt.Println("B") case score &>= 90 score &< 100: fmt.Println("A") default: fmt.Println("Error") }

2、Map / Array / Table / Table-Driven

本质都是将不同条件下相应的处理代码存放到数据结构中,然后根据条件取出相应的处理代码来运算

存放代码的数据结构可以根据需要解决的问题和编程语言选择适合的数据结构,通常为Map和Array(Table在大多数语言中都是Array实现的),想玩的花一点,也可以选链表、树、图啊什么的

// map example

relationMap := map[string]string {
"Yang Mi": "da mei ren",
"Liu Yifei": "wo lao po",
"Huang xiaoming": "shuai shuai shuai!!!",
"Wu Yanzu": "wo lao gong",
}

fmt.Println(relationMap["Liu Yifei"])

// array example

levels := [5]func() {
func() { fmt.Println("E") },
func() { fmt.Println("D") },
func() { fmt.Println("C") },
func() { fmt.Println("B") },
func() { fmt.Println("A") },
}

limits := [5]int { 60.0, 70.0, 80.0, 90.0, 100.0 }

for index, limit := range limits {
if score &< limit { levels[index]() break } if index == 4 { fmt.Println("Error Score") } }

3、多态

简单来说,就是定义介面,不同条件下的处理代码由不同子类实现

在Java巨巨巨巨巨巨巨巨巨多的设计模式中还有个名字——策略模式(Strategy Pattern)

type Animal interface {
Speak()
}

type Dog struct {}
func (d Dog) Speak() {
fmt.Println("I may not be human, but you are a real dog.")
}

type Cat struct {}
func (c Cat) Speak() {
fmt.Println("What bad thoughts can cat have?")
}

type CodeMonkey struct {}
func (cm CodeMonkey) Speak() {
fmt.Println("ahhhhhhhhhhh, spaghetti code!!!")
}

var animal Animal
animal = CodeMonkey()

animal.Speak()


鲁迅说过「代码复杂度只会被转移,不会被消灭」

不管是if-else、switch-case,还是map、array、table、表驱动,还是多态、策略模式,其实复杂度都没变,变的只是不同条件的处理代码的位置

在实践中,不必太过纠结长长的if-else长蛇阵,因为不管哪种写法,复杂度一点都没有变,只是可能看起来比较简洁,过后去看,还是一头雾水

在某些演算法或业务中,由于实际情况的需要,长长的if-else必不可少,这是不可避免的,不必太过纠结。但如果演算法或业务中,长if-else过多,就要考虑演算法或业务是否合理,是不是需要换种方法或换个工作了。


看了下回答,带入了下题主的心情,感觉比较少有直接讨论问题本身的(虽然都概况的很好,都说到了点子上),忍不住来说两句。

实际上不论是 if 还是你发的那个数组访问的,本质上都是在根据某个条件做跳转,只是 if 是最简单的条件跳转单位,只有两个分支(不要跟我说 else if,那只是嵌套的 if);按下标访问数组是一种能 encode 更复杂的逻辑的条件跳转,因为她可以有任意自然数个分支(内存允许的情况下)。也就是说数组(或者说列表)是一种更复杂的处理逻辑的基本单位。

我们再来考虑 if 和列表访问的嵌套:if 的嵌套有多种,比如 if (a) if (b),比如 else if, 比如 if (a) { if (b) else }。我们可以一一讨论。

第一个等价于 if (a b)——对于列表访问而言我们可以把两个『只有 true false 两种可能』的条件给 encode 成一个『有四种可能的值』的条件,然后用一个列表去表示。这种 if 的嵌套是列表访问不需要嵌套就可以做到的事情。

第二个也是列表不需要嵌套就能做等价事情的。

第三个稍微复杂一点,它等价的列表操作也需要嵌套,但是我们分析一下:

if (a) {
if (b) {
f1();
} else {
f2();
}
} else {
if (b) {
f3();
} else {
f4();
}
}

以上代码是根据两个条件一共四种可能走出 4 个可能的分支,一共用了三个 if,其中 b 条件重复了两次。但是列表操作中等价的代码, 只需要二维的列表嵌套(我假设存在 [] 这样的语法来表示一个列表,然后假设『条件』是一种值为 0 或者 1 的整数类型,这个语法在很多语言比如 js, hs 里面是合法的,相信大家是能看懂的):

array = [ [f1, f2] , [f3, f4] ]
array[a][b]();

这样的嵌套只用到了两层的列表(虽然一共出现的列表还是有三个),b 条件在代码里面只出现了一次!

所以我们可以看出(本段有点类似重复其他回答,但是还是写一下比较好),使用更复杂的『基本逻辑单元』组合出的复杂逻辑,呈献出的代码就会更加简洁。这实际上是把一些逻辑的复杂性给藏到了你使用的基本逻辑单元里面,但是如果这些基本逻辑单元是众所周知的,那么我们可以说是我们对逻辑进行了更大压缩率的压缩,使得人在阅读的时候需要处理的信息被减少,最终实现题主所谓的『优雅』。

而彻底贯彻这种『组合更好的基本逻辑单元来压缩逻辑』的思想的编程风格,叫做『函数式编程』。


table lookup,很基本的技巧。

可以看看《代码大全》一书,里面应该介绍过(具体细节我都记不清了,好多年前看的)。

再难一点的就是在table里放function object。这个在函数式编程中算是比较常用也比较高端的技巧了。Emacs Lisp代码中常见。比如company-mode (见 company-mode/company-mode 中company-backends) 。

我认为有进取心的javascript程序员应该学习一下Lisp。毕竟Lisp是函数式编程的先驱。网上一些javascript函数式编程技巧就是Lisp技巧的简化版而已。

一但学会Lisp,在其他语言应用函数式编程技术就和呼吸一样自然。

当初Brendan Eich本来是想让Netscape用Scheme(一种Lisp方言),以下是他的原话,

「The idea was 『Come and do Scheme in Netscape. Put this programming language into the browser.」 He later calls Scheme 「that beautiful research language I was tempted with.」

后来由于上头不给时间,于是大神就在10天内弄出了个新语言,加了一点scheme特性。起了个名字叫javascript。

最后推荐我的教程:

从零学习快速编程,精通所有IDE和编辑器,学会Emacs Lisp,

如何提高编程速度 - Emacs高手教授轻松学习所有编辑器和IDE的秘诀?

edu.51cto.com图标


推荐阅读:
相关文章