引子
對於計算機從業者而言不論你的母語是什麼語言,中文,英語或是法語,西班牙語等,你的第一工作語言都是程式語言,你一定聽說過那句話 「talk is cheap show me the code"。所以,快速學習和掌握程式語言一直以來都是每一個工程師夢最想要擁有的超能力。
我從小學開始學習編程,在後來17年的職業生涯中也主動和被動的學習了一眾程式語言,如C/C++,Java,Python,Haskell,Groovy,Scala,Clojure,Go等等,在這期間付出了很多努力,取得了不少經驗,當然也走過了更多彎路。下面分享一下自己的學習心得,希望可以對大家的學習有所幫助和借鑑。
掌握編程範式優於牢記語法
各種程式語言裡的獨特語法簡直是五花八門,下面就隨便選取了其中幾種語言,看看你們知道他們都是什麼語言嗎?
1.
def biophony[T <: Animal](things: Seq[T]) = things map (_.sound)def biophony[T >: Animal](things: Seq[T]) = things map (_.sound())2.
quicksort [] = []quicksort (x:xs) = let smaller = [a|a<-xs,a<=x] larger = [a|a<-xs, a>x] in quicksort smaller ++[x]++ quicksort larger3.
my @sorted = sort {$a <=> $b} @input;my @results = map {$_ * 2 + 1} @input;很多工程師往往把學習語言的重點放在了學習不同語言的語法上,而忽略了語言背後的思想及適合的應用場景。
其實對於程式語言的學習,意義最大,收穫最大的就是對於編程思想的學習。正如著名的計算機學者,首位圖靈獎獲得者,Alan Perlis說的那樣如果一個程式語言不能夠影響你的編程思維,這個語言便不值得學習。
「A language that doesn’t effect the way you think about programming, is not worth knowing.」 - Alan Perlis
程序語言的編程思想主要受到編程範式的影響,如果了解這點你就會發現很多新語言其實是新瓶裝老酒。
編程範式(programming paradigm):
官方的定義是:A programming paradigm is a style, or 「way,」 of programming. (一種編程風格和方式)
以下是目前廣泛應用的一些編程範式:
結構化 (Structured)
函數式 (Functional)
面向對象 (Object Oriented)
關於這些典型編程範式相信你已經有所耳聞,也可以在網絡上找到很多詳細的相關資料,這裡就不再贅述,僅通過一些簡單的實例對比,來讓大家認識到不同編程範式對程序設計思想的影響。
結構化 vs. 函數式
我們通過快速排序的實現來看看這兩種編程範式的差別:
快速排序 結構化實現:Groovy語言實現
class QuickSort { private static void exch(int pos1,int pos2,List data){ int tmp=data[pos1]; data[pos1]=data[pos2]; data[pos2]=tmp; } private static void partition (int lo,int hi, List a){ if (lo<0||hi<0||lo>=hi-1){ return; } int midValue=a[lo]; int i=lo+1;int j=hi; while (true){ while(i<hi&&a[i]<midValue){ i++; } while(j>lo&&a[j]>midValue){ j--; } if (i>=j) break; exch(i,j,a); } exch(i,lo,a); partition(lo,i-1,a); partition(i+1,hi,a); } public static List sort(List a){ int lo=0; int hi=a.size()-1; partition(lo, hi, a); return a; }}快速排序 函數式實現:Groovy實現
def quickSort_fp(List list){ if (list.size()==0){ return [] } def x=list[0] def smaller=list.findAll{it<x} def mid=list.findAll{it==x} def larger=list.findAll{it>x} return quickSort_fp(smaller)+mid+quickSort_fp(larger) }通過以上比較你會發現:對於結構化編程,我們要通過程序來告訴機器怎麼做,而函數式編程則更像是通過程序告訴機器我們想要什麼。
函數式 vs 面向對象
這裡我們通過實現一個通用的計算過程計時功能來比較。大家可以細細體會其中的不同。
面向對象 Go語言實現
以下利用Decorator模式來為不同的Caculator實現的Caculate過程計時 (如果對於下面程序有些疑惑,建議先回顧一下設計模式裡的Decorator模式)
type Caculator interface { Caculate(op int) int}
type TimerDecorator struct { innerCal Caculator}
func NewTimerFn(c Caculator) Caculator { return &TimerDecorator{ innerCal: c, }}
func (td *TimerDecorator) Caculate(op int) int { start := time.Now() ret := td.innerCal.Caculate(op) fmt.Println("time spent:", time.Since(start).Seconds()) return ret}
type SlowCal struct {}
func (sc *SlowCal) Caculate(op int) int { time.Sleep(time.Second * 1) return op}
func TestOO(t *testing.T) { tf := NewTimerFn(&SlowCal{}) t.Log(tf.Caculate(10))}函數式 Go語言實現
以下也是通過類似於Decorator的思路來為不同的方法實現添加運行過程計時
func timeSpent(inner func(op int) int) func(op int) int { return func(n int) int { start := time.Now() ret := inner(n) fmt.Println("time spent:", time.Since(start).Seconds()) return ret }}
func slowFun(op int) int { time.Sleep(time.Second * 1) return op}
func TestFn(t *testing.T) { tsSF := timeSpent(slowFun) t.Log(tsSF(10))}通過上面的比較大家可以有個簡單認識,在函數式編程中函數是第一公民可以作為參數和返回值,而在面向對象編程中對象才是第一公民。
可以類比,但不要翻譯
在學習程式語言時,如果我們已經了掌握了一種語言,通過類比,尤其是比較不同點,可以有助於我們更快的掌握另一種新的語言。
要注意的是學習程式語言也和我們學習自然語言一樣,要掌握不同語言的特點,習慣用法,否則就會出現類似於中式英語這樣的問題。不管後來學習了什麼語言,都是先用熟悉的語言的方式實現,然後翻譯成另一種語言,我們常常可以看到C++語言描述的C程序,Go語言描述的Java程序等。
下面給大家舉個例子:
讓我們來生成一副撲克。
Java中的實現:
public static void main(String[] args) { List<String> cards = new ArrayList<>(); for (int i=2;i<12;i++){ cards.add(String.valueOf(i)); } cards.addAll(List.of("J","Q","K","A")); System.out.println(cards); }Java程式設計師,學習了Python以後,通常會實現成這樣
cards = ['J','Q','K','A']for n in range(2,11): cards.append(str(n))print cards上面的Python代碼雖然也可以準確的實現功能,但在一個Python程式設計師眼中上面的代碼總覺得不是那麼地道,下面是地道的python程序
cards = [str(n) for n in range(2,11)] + list('JQKA')print cards專注語言特性而不是語法糖
上面談到寫地道的程序,學好程式語言中的「語法糖」,通常可以讓我的代碼更簡化和「看上去」很地道,因此也有很多程式設計師非常熱衷於此,猶如對於IDE的快捷方式的熱衷,似乎這些是一個資深,高效程式設計師的標誌。
什麼是語法糖呢?
語言中某種特殊的語法
對語言的功能並沒有影響
對程式設計師有更好的易用性
增加程序的可讀性
一些語法糖的例子:
Python中union兩個list
Go交換兩個變量的值
上面由語法糖帶來的地道我之所以用了「看上去」這個詞來修飾,就是因為要想真正的寫出地道的程序,比掌握語法糖更重要的是掌握語言的特性。
什麼是語言的特性呢?
我們以Go和Java語言中並發機制特性為例:
1 基本並發單元
Go中採用獨特的協程(Goroutine)作為基本並發單元,這一定會讓Java程式設計師聯想起線程(Thread),並不免在編寫Go並發程序時引入很多編寫多線程程序的思維,實際由於兩者間存在著很多差異,直接以多線程的編程思想來編寫多協程的程序有時是不適合的。
先讓我們簡單看看兩者的差異:
雖然,我們沒有完全列出兩者的差異,但是你也可以發現像Java程序中常見的線程池,在Go程序中很多情況下並不會帶來像Java中那樣的性能提升。
2. 並發機制,
Java通常採用共享內存機制來進行並發控制,而Go中則支持了CSP(Communicating Sequential Processes)機制。
下面通過典型的生產者和消費者並發任務來比較兩種方式不同的特性:
共享內存方式(Java實現)
import java.util.LinkedList;import java.util.Queue;import org.junit.jupiter.api.Test;
class Producer implements Runnable { private Queue<Integer> sharedData; public Producer(Queue<Integer> sharedData) { this.sharedData = sharedData;
}
@Override public void run() { for (int i = 0; i < 100; i++) { synchronized (this.sharedData) { try {
while (this.sharedData.size() != 0) { this.sharedData.wait(); } this.sharedData.add(i); System.out.printf("Put data %d \n", i); this.sharedData.notify(); } catch (InterruptedException e) { e.printStackTrace(); }
} }
}}
class Consumer implements Runnable { private Queue<Integer> sharedData; public Consumer(Queue<Integer> sharedData) { this.sharedData = sharedData; }
@Override public void run() { while (true) { synchronized (this.sharedData) { try { while (this.sharedData.size() == 0) { this.sharedData.wait(); } System.out.println(this.sharedData.poll()); if (this.sharedData.size() == 0) { this.sharedData.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } }
} }}
class ProducerConsumer { @Test void test() throws InterruptedException { Queue<Integer> sharedData = new LinkedList<>(); new Thread(new Producer(sharedData)).start(); new Thread(new Consumer(sharedData)).start(); Thread.sleep(2 * 1000); }}利用共享的Queue來實現生產者和消費者之間的數據傳遞,為了保證數據在多線程間同步,我們使用了鎖。
CSP方式 (Go實現)
package Demoimport ( "fmt" "testing" "time")
func Producer(ch chan int) { for i := 0; i < 100; i++ { fmt.Printf("put %d \n", i) ch <- i } close(ch)}
func Consumer(ch chan int) { for { select { case i, ok := <-ch: if !ok { fmt.Println("done.") return } fmt.Println(i)
} }}
func TestFn(t *testing.T) { ch := make(chan int) go Producer(ch) go Consumer(ch) time.Sleep(time.Second * 3)}Go這是利用CSP機制中的channel在生產者和消費者之間傳遞數據。
好的代碼風格不能代替好的設計
代碼是軟體實現的最終形式。但是對於更為複雜的軟體而言,我們僅僅在代碼層面整潔,復用及高可讀性還是遠遠不夠。
人們總是通過更高層次的抽象來實現簡化。猶如,程式語言由機器語言,到彙編語言,再到高級語言不斷演進,抽象層次不斷提高。更高的抽象層次,能夠更加有助於人們去了理解和構建更複雜的軟體。
所以,在每個抽象層面,都要考慮簡潔,復用和易理解。而且軟體的設計過程及人們理解軟體設計的過程也通常是自頂向下的。這就產生了指導人們做好高層抽象的設計模式,甚至更高抽象層面的架構模式。
作為合格程式設計師光學習代碼的整潔之道是不夠的,還有學習更高層面的整潔和復用。
下面我們還是用一個例子來說明
這裡模擬我們要實現一個可以擴展使用不同支付方式的支付過程:
type PayChannel int
const ( AliPay PayChannel = 1 WechatPay PayChannel = 2)
func payWithAli(price float64) error { fmt.Printf("Pay with Alipay %f\n", price) return nil}
func payWithWechat(price float64) error { fmt.Printf("Pay with Wechat %f\n", price) return nil}
func PayWith(channel PayChannel, price float64) error { switch channel { case AliPay: return payWithAli(price) case WechatPay: return payWithWechat(price) default: return errors.New("not support the channel") }}上面的代碼在編碼風格上是整潔的,可讀性也不錯。但我們會發現每增加一種支付模式我們都要修改PayWith這個方法,在switch中加入一個對應的分支。
通過利用面向對象中的命令模式的設計思想,我們可以將代碼優化為如下(當然這裡是採用函數式編程來實現這個命令模式的思想的)
type PayFunc func(price float64) error
func payWithAli(price float64) error { fmt.Printf("Pay with Alipay %f\n", price) return nil}
func payWithWechat(price float64) error { fmt.Printf("Pay with Wechat %f\n", price) return nil}
func Pay(payMethod PayFunc, price float64) error { return payMethod(price)}現在可以看到,新增支付方式時,Pay方法完全不用做任何修改。
這裡給大家推薦兩本設計模式方面的經典書籍
不要害怕遺忘和混淆
「學了也用不上,很快就忘記了」常常會成為很多程式設計師拒絕學習新的程式語言的藉口。
遺忘和混淆都是正常的,人類記憶就是這樣。
這是遺忘曲線,在一開始的遺忘率是最高的。對於語言學習,我的經驗是我們會很快忘記我們學到的特殊語法,留存下來會是我們對語言編程範式,編程特性的理解。所以,正如前面我們已經提到過的《黑客與畫家》中觀點:學習了Lisp,即使你在工作中極少使用到它,你也會成為一個更優秀的程式設計師。
如果你是按照我們前面所說的方式充分掌握每種語言的特性並了解編程範式,你所遺忘和混淆的更多的是語法。通過寫上一,兩天,甚至幾小時的程序,你很快就會發現所有那些對於這種語言的技能就都回來了。這好比練過健身的人,一段時間不練,肌肉會有流失,但是與從來沒有練過的人不同,他們通過訓練,肌肉很快能夠恢復原有的狀態,就是所謂的肌肉記憶,我們的大腦記憶也是這樣的。
所以,不要因為害怕遺忘和混淆就不去學習新的語言,他們不僅可以拓寬你的編程思路,一旦需要你便可以經過較短時間從回巔峰!
不要讓遺忘成為你放棄學習的藉口,讓遺忘成為一種提煉。
「Stay hungry,stay foolish." -- Steve Jobs.