感覺看見代碼頭大怎么辦?
正好最近公司一個同事想學編程。就把這個貢獻給題主了~~~
每一門學科都有它自己的思維方式,學習的過程就是通過它的知識描述、實例、習題來去感知這門學科究竟是用什么樣的思維去運作的。在初學的時候,去感知這門學科的思維方式是非常重要的,非常重要的,非常重要的。
舉個例子,比如音樂,別人給你唱一段,只要你不是五音不全,或者音太高或太低,那么你當場就模仿一遍也不是什么難事。但是,真正懂音樂的人,會知道他唱的每一個音的音高是什么,是什么風格的、甚至腦中都已經響起了和弦。
又比如在繪畫領域,很多學過素描的都能直接照著描這棵樹:
《與西斯圖合作的樹的習作》 -- 達芬奇但是,如果你去搜一下達芬奇的《論繪畫》,你就會明白,這是一顆不存在這個世界的樹,但是卻是一顆真實的樹。
回歸正題
其實學編程也一樣,逐行抄代碼的作用,當然會有,但是并不大。你應該明確自己逐行抄代碼的意義;那就是熟悉這門語言的語法的拼寫,而已,也就是強行記語法&單詞,你需要盡早擺脫抄代碼,學習一周以內,你就不應該一句一句地抄了。你真正想學的編程,是一種思維活動,這個涉及到你是否真的明白教材里面的知識。
從我之前教人編程總結的一些經驗給你說說如何入門編程。
PS:比較長。
0、初學編程的核心問題據我的經驗,初學編程所遇到的問題無非就是以下3種:
思路混亂(問題領域)不清楚每行代碼的作用(靜態思考)不知道每行代碼當前所處的狀態(動態思考)思路混亂,這個問題在很多的程序員中也會出現,它屬于領域問題,在于對需求理解不夠,對問題的分析不到位所導致的。而對于初學者,則會容易出現另一個問題:把問題領域等同于編程問題。也就是從一開始就以 “我要寫代碼” 的方式去思考,結果越想越混亂。為什么呢?因為初學編程首先對語法就不熟悉,更別提語言里面的各種概念了。所以,以 “寫代碼” 的方式直接思考問題領域中的問題,本身就會讓你的思維更加混亂。
例如,我現在叫你寫一個判斷9102年是不是閏年的程序,如果你的腦子里面飛出來一堆 “應該怎么寫?”、“是用if嗎?”、“還是要用到for?”、“if語句怎么寫來著?”、“糟糕,我忘了main函數的寫法了”;那么你應該先去買杯奶茶冷靜一下(順便給我帶一杯)。
接下來,你應該準備紙筆,邊喝奶茶邊想 “閏年是什么意思?”、“我應該先判斷它能不能被4整除(在紙上寫下 判斷9102/4是整數)”、”噢,這樣還不行,我還應該判斷它是否同時滿足不能被100整除(在紙上寫下 判斷9102/100不是整數)“、“哎呀,還有還有,如果剛好是400的倍數,那么雖然可以被100整除,但是也是閏年(在紙上寫下 判斷9102/400是整數)”。你看,這就是所謂的問題領域,跟編程其實一毛錢關系都沒有,大家都是這樣思考問題的,而編程,只是把問題轉化為語句罷了(其實你在紙上寫下這些的時候,也是把問題轉化為中文而已,只是因為你對中文的表達形式太熟悉了,所以能直接寫出來)。
不清楚每行代碼的作用,你已經知道什么是閏年了,也知道靠你自己的腦子里的數學運算知識,就可以知道9102年是不是閏年了。但是,程序應該如何表達你的思維過程呢?這個就是你需要學習的所謂的“編程語言”,也就是它的語法以及語句所表達的含義。
不知道每行代碼當前所處的狀態,這點對于初學者來說是個非常頭疼的問題,因為它需要你去感知代碼在CPU上跑的時候,究竟是怎么樣的,也就是你的大腦在模擬CPU,這也是程序員的標準日常。而對于初學者來說,最難越過的就是這道坎。
這相當于什么呢?比如在學音樂的時候,會不斷地聽音、模唱、腦補這個音在五線譜上的位置,以及在你所學樂器中如何表現出來,直到你能夠看到五線譜上的音符的時候,腦中就自然地響起那個音,這在音樂中被稱為內心聽覺(敲黑板)。
或者說你對繪畫比較熟悉?那么在學習繪畫的時候,學習勾線,然后看看別人的畫是怎么布局的、怎么勾線的,鍛煉自己在畫的時候就能在腦中清晰地腦補出想要畫的事物。
你看,其實每個學科都是同樣的,需要你培養對這個學科的感知能力。編程也不例外,它要求你在看別人的代碼或者自己寫代碼的時候,你的腦中應該有清晰的“圖像”,并且這個圖像是動態的,隨著你關注問題的不同部分,這個圖像就會不同。
OK。既然知道了初學會面臨什么樣的問題,那么就等于有了目標。不過,在談如何解決這些困難之前,我們先來學點最簡單的“哲學”,當然,不是專業課上的哲學,而是經過簡化的編程哲學。
PS:另外提幾句,如果題主是想以編程為主業或者對編程特別感興趣,那么建議你從C、Java/Kotlin、Golang等(半)編譯型語言,理由很簡單,這些語言會迫使你去認知你寫的代碼到CPU執行會經過什么步驟,讓你對計算機基礎概念的理解有極大的幫助。我的建議當然是直接學C,雖然入門之后的指針、垃圾回收等概念有些困難,但是你會了解到非常多計算機原理的知識,對你未來學習其他語言,以及精進的幫助是非常大的。如果你只是想學個編程來輔助你的工作,比如,你是搞金融的、搞地質勘測的、社交/交易數據分析的,那么學個Python、JavaScript就非常合適,沒必要去碰真·程序員的那些“燒腦”的東西。(最近不知道哪來的風氣,說C、Java不適合入門,要學Python之類的腳本語言,并且一律全是學爬蟲,看得我一臉懵逼。我只能說,如果你先學了這些語言,倒也問題不大,只是你可能很難再去認真學計算機原理了;為什么?因為你會選這些所謂的“簡單”的語言的時候,就已經表明你并沒有想要去接觸計算機原理了,而當你學會之后,你就會有一種“我都已經學會編程了”,我還學那些干嘛?當然,一切還是看你自己。)
1、抽象&認知題主千萬不要被抽象和認知這兩個詞嚇到。大家都說程序員需要抽象思維,并且抽象思維聽起來好像很難的樣子。其實抽象比你想象的要簡單的多,也普遍得多。比如,當你在街上看到一只狗,你的腦中就會浮現“狗”這個詞,以及“狗很可愛”、“狗有很多毛”、“狗有尾巴”等特征,還有“狗會跑”、“狗會尿尿”、“狗會吠”、“狗會咬人”等行為。其實這個時候,你就是在利用抽象能力,只是多數情況下你會忽略這種思緒,因為它實在是太平常了。
而你現在,需要做的,就是如何刻意去練習抽象,從最基礎的認知開始:抽象就是從具體事物中提取出它們的共同特征。你看,剛剛說的對狗的認知,就包含了大部分的狗都符合的特征和行為。
所以,當我們要去學習“大象”的本質是什么之前,首先要看到整頭大象大概是長什么樣的。
認知,就是我要講的第一個重要的點。很多初學者在學習的時候,并不是真正的對編程有了認知,而是只是記住了編程語言的語法,而不是它的核心概念。什么叫做核心概念呢?下面就開始詳細的說明:
在大多數邏輯型語言中(例如比較主流的C/C++、Java、Python;而所謂非邏輯語言,是指像Json、HTML、CSS等不包含邏輯的描述語言),都會存在一些核心的基本概念,這些概念其實和語法沒有任何關系,語法僅僅是為了表達這些概念而存在的。對于初學者來說,并不要求立刻就理解這些抽象的概念。但是,這是我們的學習目標,我們的學習目標并不是學會X語言的語法,而是語法背后的核心概念(敲敲~敲黑板)。
編程其實就是處理各種各樣的數據。說得很抽象對吧?別怕,跟著我來。
大多數編程語言一上來都會給個“Hello World!”程序,這就是我上面所說的那頭“大象”,它的意義就是讓你先對這個語言長成什么樣有個感性的認識。它告訴你,在我這個語言里面,你這樣寫,就能在終端/屏幕上輸出“Hello World!”,讓你先體驗一下即時反饋的樂趣。這時,你可以做的最簡單的事情就是去修改輸出的內容,比如改成“Hello, XNan!”,你就能感受到你的修改是有效的。不過,接下來,就沒那么簡單的,因為當你看到這頭“大象”之后,就要開始“解剖”這頭“大象”了(動物保護協會的不要來找我啊~~~)。
比如說C語言,你會接觸到的是變量和數據類型,然后是各種運算符,接著是if語句,再接著是while語句,再再接著就是for語句,說不定還有switch語句,一直到結構體、聯合體、指針、函數 balabalabala~。眼花繚亂,一臉懵逼,生無可戀······
但是,你先拿起筆,在本子里記下下面的列表:
數據存儲(定義變量&數據類型&自定義類型)數據運算(運算符)條件判斷(if語句&switch語句)重復(while、do...while、for循環)拆解問題(函數)是的,這就是我說的核心概念,幾乎任何一種邏輯型語言,都會存在上面這個列表中的概念,唯一不同的,就是這個語言究竟是如何去表達它的。
再舉個 :“我”,其實是一個概念,還記得你小時候,你媽媽或者幼兒園老師教你 “我” 這個概念的時候的情景嗎?一般都會指著自己,然后說 “我”,來去傳達這個概念給你,而這個“我”字,只不過是表達 “我自己” 這個概念的形式而已,在英語中,也有 “我” 的概念,并且表現形式很不一樣,使用“I”來表達“我”這個意思的。
那么現在你應該大概能理解了,定義變量這種具體的行為,其實只是數據存儲的概念,它的本質是你有了一個可以存儲數據的空間,你可以把一些數據放進去。那么編程思維就構建起來了。其他幾個也是同理,你在學習的時候,你要去理解語法背后的概念。那么現在也就有了學習的方法。
好了,有了目標,也有了學習方法,就可以開始學習了。
2、分析問題上面已經說到,初學者第一個面臨的問題是思路混亂,也就是沒有對問題領域有清晰的認識,沒有在邏輯層面解決所提出的問題,而上面所使用的閏年的例子已經說得很清楚了。在你還沒有非常熟悉語言的語法的時候,你需要現在紙上把問題先解決了。為什么呢?因為你對所使用的工具還不熟悉,所以你根本沒辦法一邊思考問題,一邊思考代碼怎么寫,所以你需要先分開思考。想要越過這個坎,還是很簡單的,看到習題的時候,首先在腦子里去解決問題,然后再把問題分步驟寫在紙上,最后再去找每一步,在你所學習的語言里面是如何表達的。很容易,對吧?不就是一杯奶茶的事情嘛,如果不夠,那就兩杯。
3、理解每行代碼的作用理解一行代碼究竟是什么作用的,是一種靜態思考,這是必須的,這就是上面所舉的音樂和繪畫的例子中的一個概念;在寫代碼的時候,腦子里面應該想些什么。你需要非常清楚地知道你寫的這一行代碼的作用究竟是什么。
比如
這行代碼,應該在腦子里想什么呢?下面給出不同層次的思緒。
初等:我定義了一個整數類型的變量,名字叫num,這個變量等于42(這個思緒嚴格來說是錯的)進階:我定義了一個可以存儲int類型數據的變量,命名為num,并且我給它賦值為42(這個是正確的,但是認知太淺了)原理:我在棧中有一個int型大小的空間,這個空間保存著42這個值的二進制碼,這個空間···唔···就命名為num吧(如果寫這句代碼時,潛意識是這樣想的,那么你已經擁有真正的程序員思維了)底層:我在這個函數棧寄存器指針(例如ESP)所在的第N個位置存儲著42這個值,我可以通過num這個名字來取到這個值,唔···可能有些CPU會有不同的行為(初學者就不用考慮這一層了,如果對匯編沒什么概念的話。而且正常來說也沒必要)物理:CPU寄存器的某些位置會聚集電荷,表示存儲了數據······(額······還是去搞集成電路吧)神話:And God said, "Let there be light" and there was light.(行了,已經不用學了)OK,扯淡完畢。對于初學者,應該可能目前處于“初等”的思維,有些可能可以達到“進階”思維。我的建議是,努力達到“原理”思維。至于“底層”思維,如果有興趣可以去學匯編,了解CPU的構造,如果沒興趣,也沒必要。
只有一個例子,可能還無法理解。那么就針對if語句來舉例子吧。
這里就不列每個思緒了,直接說你腦子應該想什么:我想要判斷num變量是不是大于或等于42,num的值保存在棧的某個空間里面,這樣我就能取到它的值,并且使用>=運算符來進行判斷,if語句就是這樣的,使用單詞if,后面括號里面的是條件,條件表達式會返回這個判斷是不是對的,如果是對的,那么CPU就會跳到執行“對的”情況下的代碼,也就是那句語句。很清晰,對吧?那就按照這個方式,去真正理解程序里面每一句代碼的作用是什么。
以上,看起來很繁瑣,但是,初學的時候,你必須這樣去思考,一開始可能會很慢,看代碼可能一行要看幾秒甚至幾十秒才能反應出它的含義,但是隨著你這樣的思考越來越頻繁,不出半年,你就可以瞬間反應出來,并且都是潛意識的活動,你不注意都可能沒發現自己已經想的那么深入了。
4、跟蹤每行代碼運行時所處的狀態這是初學者面對的最難解決的問題,因為初學者的思維比較單一,并且在對語法都還不熟悉的情況下,更加難保持清晰的認知狀態,在這些debuff疊加的狀態下,讓初學者苦不堪言。不過沒關系,下面我就來談談如何解決這個問題。
首先,這里的“運行時”,并不是說程序真的在運行的時候,而是我們在寫代碼的時候,就要在腦中模擬運行這個程序。那么什么叫做狀態呢?也就是當程序運行到某行代碼的時候,究竟有多少數據?這些數據的含義是什么?這些數據的值我能確定嗎?這些值在現在這行代碼執行的時候,值域范圍是什么?我應該如何處理它才能達到我的目的?
上面的所有問題,都是要你在寫代碼的時候,能瞬間回答的問題。下面我就拿判斷閏年的例子,來說明如何動態思考:
吶,通過這個簡單的例子。應該毫無壓力的就能理解所謂的 “跟蹤每行代碼運行時所處的狀態”了。
5、實例下面就用一個稍微復雜一點的實例來結束這個回答吧。
我個人在教初學者的時候,習慣用判斷一個數是否是素數來作為最基礎的實例。原因是它簡單,不管是問題的描述還是用代碼去實現,都很簡單,但是卻用到了語言的所有基礎概念和語法(變量定義、條件判斷、循環)。這里我并不打算講函數,只作為對那3個問題的一個講解實例而已。
首先,我們先在問題領域把這個問題解決了。
素數的定義:一個數,如果只能被1和它本身整除,那么它就是一個素數。
那么在邏輯上,我們是怎么去判斷的呢?比如說,一個數,如果比較小,那么你的大腦很容易就能判斷出來,比如5,是一個素數,它不能被234整除,而大于5的數就肯定不可能整除5了。再比如,17,它不能2~16的數整除,但是···你確定嗎?你有拿每個數去除過嗎?沒有,那么你現在就去試試,拿2 3 4 ~ 16每個數都去除一下,然后告訴我,17是不是素數?
PS:當然其實你只需要除到8就可以了,因為一個數被任何一個數整除的最小值是2,所以能整除17的整數絕對不會超過8,不過我們這里不考慮
是的,17確實是素數,但是如果你沒有一個一個去除過,你就不能保證它的確是素數,而計算機,判斷一個數是否是素數的思路,也是一樣。你需要從2開始,拿每一個數去除17,這樣才能證明17是素數。那么,我們就在問題領域里面,解決了這個問題了。但是,如果按照這個思路,你可能會寫出每個除數一行代碼來判斷的程序,這樣一點都不符合程序員“偷懶”的思維,還不如我在紙上直接寫來得快呢,是吧?那么應該如何寫呢?
OK。看代碼(我這里使用C語言,如果你學的是其他語言,那么你可以作為參考,然后自己用目標語言去寫):
下面跟著我來一步一步地在大腦中執行代碼。注意格式,適當的空行有助于分隔代碼,也有助于你的思路清晰。
首先,main函數是C語言的入口,對于初學者,你只要知道,把代碼寫到main下面的大括號里面就可以了。第5行,定義int型的變量num,這個變量的含義是我們要判斷是否為素數的數字。記住你定義了一個這樣的變量。第7行,定義int型的變量i,這個變量表示num需要除的數,它是動態的,你會在每次除完以后,將它+1,比如先讓num除以2,然后再除以3,再除以4······,所以你必須很清晰地知道i的作用。它是用來充當每次驗證num的除數。因為我們判斷素數是從2開始的,所以它的初始值是2。第8行(開始判斷是否進入循環),現在想想那幾個問題;究竟有多少數據?這些數據的含義是什么?這些數據的值我能確定嗎?這些值在現在這行代碼執行的時候,值域范圍是什么?在運行到這行代碼的時候,num和i的作用你必須要很清楚。那么就可以這樣思考,當前的i為2,num的值是什么,我們并不需要知道,但我們依然可以先假設為17。總之我們的目的就是看i是不是比num小,如果是,我們就應該拿i去除num。所以我們進入循環的條件就是 i < num。而按照目前的數據來看,2 顯然小于 17。所以會進入循環。第10行(第1次進入循環),上面說了,i當前的值是2,我們拿它去除num,并且看看余數是不是0,如果余數是0,就表示被整除了,那么說明就不是素數;如果不能被整除,那么就繼續檢查下一個數字。而在這行代碼中,我們拿2 去除 17,并不能整除,所以就會跳過if的大括號里面的代碼。第15行,這就是將i的值+1,注意了,當執行到這行代碼的時候,i的值,改變了,根據當前狀態,i的值會變成3,而num依然是17。接下來,就是很多初學者會腦抽的地方,那就是代碼執行的順序會改變,它不會16行、17行、18行這樣執行。因為while語句是循環語句,并且while的整個大括號都屬于循環體,所以當執行到16行的時候,它會重新回到第8行,并且重新執行判斷。接下來就是這樣的,我們繼續按照當前變量的狀態來執行。第8行(第二次判斷是否進入循環),然后還是那幾個問題,你要清楚i和num的狀態。剛剛已經知道了,i的值被+1了,所以就等于3了。3 < 17 依然成立,所以繼續進入循環。第10行(第2次進入循環),這里最后一次強調你要清楚i和num的狀態。i 等于 3,那么17 / 3并不能整除,所以依然跳過if的大括號里面的代碼。第15行,再次將i的值+1,所以就變成4了。接下來,就會一直執行循環,不斷拿新的i值去除num,只要每次都不會除盡,那么循環就會繼續,i不斷+1,一直到i的值等于num的值為止,在這里,i最終會等于17。那么我們這時就會再回到第8行,判斷是否進入循環的時候。來看看這個臨界時刻。第8行(經過15次的循環之后···),可以發現,17 < 17 顯然不成立了,所以這個時候,就不會再進入循環了,而是會直接跳過循環,繼續執行。第18行,運行到達這里,就說明我們已經有明確的可以判斷num是否為素數的條件了。那么條件是什么?我們來看看,如果一個數不是素數,那么在i 小于 num并且被除的時候就會被除盡,例如,如果我們的num是9,那么在i等于3的時候,第10行就會判斷num能夠被i整除,所以就會進入if語句的大括號里面,也就是第12行的語句,這條語句表示直接跳出循環,所以當i被整除的時候,就會直接跳出循環,也就會直接跳到現在這行代碼。現在就簡單了,如果i的值能夠被整除,那么它當然就還沒被加到num的時候,就已經跳出來了,所以i一定不會等于num。但是如果i一直沒有被整除,那么最后必然會等于num(這也是我為什么說先不管可以除到num的一半就可以),所以當i等于num的時候,就說明num是素數;不相等的時候,就表示num不是素數。這個程序,就已經分析完了。那么簡單的程序啰嗦了那么多,目的是讓初學者去感知寫代碼的時候,大腦就行是如何運作的。
6、總結在編程里面,最難的是思維的轉變,很多說學不會編程的,其實是因為并沒有把思維轉到程序中來。初學的時候,千萬不要以為抄個代碼,運行成功,結果也對就萬事大吉了。而是要認真看書、看文檔,去搞清楚每一個概念,只有理解了概念,才是真的會編程。也不要覺得按照上面的實例分析來去跟蹤思考每一步程序很麻煩,這和其他學科一樣的,你一開始會覺得阻力如山,但是一旦你每一個實例都去用正確的思維方式去思考,很快就可以變成潛意識的活動了。
感覺挺多人對編程的理解有障礙的。弄得我都想搞個培訓了~~~哈哈哈哈!!!