最近看到這個代碼,感覺非常有靈感

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圖標


推薦閱讀:
相关文章