博客 / 詳情

返回

懂得編程語言的通用結構,隨便哪個語言都是手拿把掐

編程語言核心結構體系:從相似性到本質理解

前言

在接觸過多個編程語言的學習之後,觀察到一些通用的範式結構,編程語言雖然表面差異巨大,但底層存在一套不可簡化的最小完備集——這是所有語言都必須包含的基本元素,否則無法表達任意算法。

而把握住這一點之後,對任意編程語言的學習都有一種脈絡極其明晰的感覺,一旦瞭解到這種通用範式的結構,那麼對於入門編程語言就會有一個系統性的學習認知框架,知道該學什麼,從哪裏開始學。

這種通用的範式結構就是所有編程語言共有的基礎元素,這種相似性源於計算機科學的基本原理——所有編程語言本質上都是人與計算機溝通的抽象工具,需要遵循計算機底層執行邏輯的約束

正是這樣的約束,導致了編程語言在設計時所共同遵守的某種規則,也就是那些隱藏在各種語法表面之下的共性規律。也因為是講解共性的內容,所以只會涉及到有哪些共性,不會描述這些共性在具體語言中是怎麼表示的內容。

基礎要素

任何編程語言都像一座建築,需要最基礎的材料和結構。這些基礎元素是表達程序邏輯的基本單元,它們共同構成了編程語言的基礎框架,包括如下五個部分:

  1. 數據表示
  2. 表達式與運算
  3. 控制結構
  4. 抽象機制
  5. 輸入輸出機制

下面按順序進行介紹。

數據表示

任何計算都涉及數據,必須有表示數據的方式

就像是在草稿本上求解數學題一樣,特別是代數內容,有字母、計算符號以及數值,這些寫在本子上的字符是表達這些內容的具體形式,並且可以保證每個學習過代數的人都可以看懂和理解,因為是一套相同的機制。

變量

首先要介紹的是變量,那麼為什麼要有變量呢?想象一下,在代數中,如果沒有變量,只有常量,也就是具體的數值,那麼所有的問題都只是數值計算問題,而且是必須一次性完成的計算,不可能分步驟,迭代式的計算。

同時在實際情況中,就是有求解未知量的需求,也有某些量在動態變化的情況,所以單純的常量無法建構一個複雜且動態的數學世界,對編程而言也同樣如此。

在程序中,變量的作用如下所示:

  1. 臨時保存數據,用於分步計算,避免一次性大量計算
  2. 避免直接使用常量,因為在一個表達式中,常量是無法修改的
    • 假設定義一個穿了增高鞋的人的身高函數為f(x)=x+2,其中變量x表示這個人的實際身高,而整數常量2就表示增高鞋的高度,是固定的數值
    • 如果它只穿同一個增高鞋的話,這個函數沒有問題,但是哪天他換了其他高度的鞋子,這個2就不適用了
    • 難道要為每個鞋子定義一個專屬的函數嗎,這顯然不可能。但是又不知道鞋子具體能給他提供多少身高
    • 所以這時候就換用變量a來描述,f(x)=x+a,此時這個變量a就代指了增高鞋的高度,根據實際鞋子的增高功能同步變化,靈活度就更高了。
  3. 記錄程序運行狀態,不同於臨時保存數據只是某一計算的中間過程,此處的運行狀態可以調控程序的流程和效果
  4. 根據輸入變化行為,提供了與外界交互的可能,因為輸入是不確定的,只有變量才能描述這種不確定性
  5. 隱藏內存細節,因為變量本質上是內存中存儲數據的位置代稱,否則需要直接操作內存地址,可讀性非常差

下面給一個關於代數中的變量與程序中的變量的對比:

代數 編程 相同點
變量 表示未知數或可變的量 本質上是內存中存儲數據的位置代稱,或者説一個容器的名字 1. 兩者都是"符號代表值"
2. 都可以被重新賦值
3. 都遵循"先定義後使用"的原則

有一個常見的混淆點,就是符號=,在數學中,=表示的是一種等價關係,只是一種邏輯關係、比如x=5,表示的是x等於5這樣一個關係或事實表述;而在程序中,表示的是一個動作,即賦值,也可以形象的表述為把一個值放入進一個容器中,具體來説就是把數值5放入到名為x的容器中,這個x也稱為變量。

作為對比,現在有表達式x=x+1,如果從數學的角度來看,這個等價關係是不成立的,但是從程序的角度來看,就是取出容器x的值,加一後再放回去的意思。

小結一下,可以認為變量就是一種容器(當然也有其它類型的容器),既然是容器那麼就是可重複利用的,同時所有語言都必須提供將數據存儲在內存中並可通過名稱引用的機制,這是計算的前提,後面的內容表述中,容器就是變量的意思。

標識符

上一小節介紹了變量,也提到變量就是容器,但這些都是抽象的概念,也就是説,給你一些看起來一模一樣的容器,然後拿一個小球隨機放進一個容器中,並打亂容器的擺放順序,你還能找到小球在哪個容器中嗎?

很難對吧,但是如果給每個容器標識一個唯一的名字,那麼只要記住小球放入哪個名字標識的容器就可以了,因為此時容器是可識別的。

實際上只要標識符能唯一確定某個容器,並不會關心標識符由什麼組成,但現實是程序的標識符需要遵循一些規範,比如不能以數字開頭、不能包含特殊字符等。

此外還有一類編程語言獨有的預定義標識符,也稱為關鍵字,這些標識符是不可使用的,比如python中的inputprint內置函數名。

數據/值

既然有了容器(變量),那麼總要往容器裏面放入一些東西,對於程序而言,就是數據,也可以稱為,而把數據放入變量這種容器的動作就是賦值操作

數據有很多種類型,比如日常在excel中有文本類型的數據、有數值類型的數據,還有一些複合類型的數據,這是因為數據來源於多種形式的活動中。

其中文本類型的數據可能是公司的員工姓名、數值類型的數據可能是員工的薪資、複合類型的數據可能是員工其他信息的組合。

在數學中,數字有整數、小數,複數等類型,同樣在程序中的數據類型也有多種,比如數值類型(整數,浮點數)、布爾類型(真/假)和字符串類型等。

之所以有這些數據類型,就是要定義數據的性質以及不同類型的處理方式,既可以是同類型之間的運算,比如3+2是兩個整數之間的運算;也可以是不同類型之間的運算,比如3+2.5中一個是整數,另一個是小數,定義它們之間的運算方式為:把整數轉換為小數之後再與另一個小數進行計算。

表達式與運算

沒有運算就無法計算

所有語言都支持將值通過運算符組合成新值,而這種由變量常量運算符組合的形式就是表達式,比如x + 5 * 3,和數學中的形式很像,並且一般情況下運算符的語義也是相通的。

這樣的表達式稱為算術表達式,可以包含變量和常量,也可以通過小括號改變運算順序,但是不同於數學中這樣的表達式只表示關係,在程序中,這樣的表達式會實際計算值,也就是有一個算術結果,並且乘號不能省略。

接下來是比較與邏輯運算,比如數學中x > 5,表示變量與數值的關係,是這樣一個事實陳述:x大於5;在程序中,這樣的表達式稱為布爾表達式,會產生一個布爾值(真/假)。

要記住只要是值就可以賦值給變量,比如is_greater = x > 5,那麼如果x>5,則變量is_greater保存的結果就為一個邏輯真值,在python中就是true,否則為一個邏輯假值false,一般布爾表達式用於條件判斷,比如if條件判斷。

控制結構

控制流就是邏輯的表達

基本控制結構有下表所示:

元素 本質作用 説明
順序執行 默認執行方式 語句按順序依次執行
條件分支 根據條件選擇路徑 必須支持if或等價機制
循環/迭代 重複執行代碼塊 必須支持while或等價機制
跳轉/返回 改變執行位置,體現思維的跳躍 returnbreak

其中順序執行圖示如下:

graph TD A[開始] --> B[步驟1] B --> C[步驟2] C --> D[結束]

按照規定好的工序一步步順序執行,不完成步驟1,就不會執行到步驟2,就比如洗完澡穿衣服,正常的順序應該是先穿內衣再穿外衣,也就是步驟1是穿內衣,步驟2是穿外衣,排除不穿內衣的情況,那麼應該沒人會先執行步驟2穿外衣,再執行步驟1穿內衣吧(不會吧不會吧😲😲)

條件分支圖示如下:

graph TD Start([開始]) --> Condition{條件判斷} Condition -->|True| ProcessA[執行操作A] Condition -->|False| ProcessB[執行操作B] ProcessA --> End([結束]) ProcessB --> End

一個邏輯判斷只會產生兩個結果,不是真就是假,比如你今天下班買菜了嗎這個邏輯判斷,要麼買了,要麼沒買,所以對應到圖上,只會產生兩條支路,如果買菜了,那麼就執行操作A,可以是自己做晚飯,如果沒買菜,那麼執行操作B,可以是點外賣

循環結構圖示如下:

graph TD Start([開始]) --> Init[初始化計數器] Init --> Condition{判斷循環條件} subgraph 循環體 Process[執行循環操作] Update[更新計數器] end Condition -->|滿足條件| Process Process --> Update Update --> Condition Condition -->|不滿足條件| End([結束])

循環,可以理解為就是重複,比如你計劃一個長達1年的早起習慣養成目標,那麼對應到圖上,循環條件就是不滿365天,也就是還在習慣養成過程中。

然後循環體中就是要執行早起這個動作並且累計早起天數,從開始早起的第一天算起,後面每天早起都增加天數,直到超過365天,這時候就恭喜完成1年的目標啦

對於跳轉和返回這類非順序的控制流,關鍵在於打破默認的從上至下的執行順序,但一般用在循環的結束條件和函數上下文切換中,其他地方不推薦使用,比如goto語句,因為屬於邏輯跳躍,不利於理解。

其次是所有圖靈完備語言都必須支持條件判斷和循環(或等價的遞歸),這是表達任意算法的必要條件

抽象機制

沒有抽象就無法管理複雜度

從本質上講,抽象就是信息隱藏——將複雜的內部實現封裝起來,只對外提供必要的操作接口。這就像駕駛汽車:你只需要知道油門、剎車、方向盤,而不需要了解發動機如何工作、變速箱如何換擋。在程序中,基礎的抽象的形式主要有:函數/過程類與對象

函數/過程​

什麼是函數呢,本質上是將一段完成特定任務的代碼封裝成一個獨立的、可複用的單元,通過定義輸入(參數)和輸出(返回值)來隱藏內部實現細節。

它是最基礎、最核心的編程抽象機制。通過這種方式我們可以將複雜系統分解為易於理解和管理的小模塊,所有實用語言都提供將代碼組織成可複用單元的機制,否則無法編寫大型程序。

形象的理解函數,可以認為函數就是一台機器,排除額外的改造之外,每台機器都有各自的功能,這是在機器誕生之日起就固定下來了,比如吸塵器,顧名思義,就是吸入灰塵的。

也就是説吸塵器的輸入是灰塵,灰塵經過吸塵器之後會有一個輸出,一團聚集的灰塵,這是機器的理想工作狀態,那如果輸入紙巾呢,好像也勉強能接收吧,可能有的機器處理的不是很好吧,但如果輸入磚頭呢,應該沒有哪個吸塵器能幹這個事情吧。

所以一台機器的輸入是有規定的,雖然它的輸入是不受機器自身控制,而來源於外部,但是如果想要正常使用機器的功能,那麼就不能由着自己的性子,想輸入什麼就輸入什麼

同樣的對於輸出而言,這是機器本身固定的部分,吸塵器不能把灰塵變成黃金,同時對於輸入是磚頭時,吸塵器也不知道該輸出什麼,它直接罷工不幹了。

總結下來,函數的核心作用如下所示:

  1. 代碼複用,一次定義,多次使用
  2. 可維護性,修改一處即可
  3. 可讀性,函數名錶達意圖
  4. 錯誤排查,錯誤集中在函數內

那麼該怎麼使用函數呢,首先就是定義的問題,也就是確定函數的作用函數的輸入以及函數的輸出,即函數名參數以及返回值。作為對比,可以和數學中的函數定義進行比較,

數學思維 編程思維
函數是一種映射關係
從定義域映射到值域
函數是一個可執行過程
會實際計算出結果
函數的輸入來自於定義域 函數的參數有類型約束
函數的輸出來自於值域 函數的返回值也有類型約束

需要注意的是,需要區分函數的定義與調用,也就是先有定義才可以調用,比如先有函數f(x)=2x,才會有f(2)=2*x=4這個計算過程。

輸入輸出機制

沒有I/O的程序無法與外界交互

日常中,我們能感知到的輸入輸出機制就是在使用手機的過程中,比如刷手機時,手指滑動屏幕,會切換到下一個視頻。

此時輸入就是手指滑動這個動作,輸出就是手機屏幕做切換視頻的動作,這就是一種與用户交互的方式,可以很直觀的被用户感知到。

所有實用語言都提供與外部世界交互的途徑,否則程序的作用就被限制在計算機內部。

除了這種顯式的交互方式,還有就是隱藏在系統內部發生的輸入輸出過程,這裏涉及到多層級的過程,對於用户而言,輸入和輸出都在最外層。

這個過程就像食品加工過程一樣,比如水果罐頭的加工過程,如下圖所示:

graph TD %% 輸入源 水果原料[🍎 水果原料] --> 原料準備 %% 第一層:原料準備 subgraph 第一層_原料準備 原料準備[原料預處理<br>輸入: 水果原料<br>輸出: 潔淨果塊] --> 裝罐[裝罐與糖水灌注<br>輸入: 潔淨果塊 + 糖水<br>輸出: 半成品罐頭] end %% 第二層:密封殺菌 subgraph 第二層_密封殺菌 裝罐 --> 密封殺菌[密封與殺菌<br>輸入: 半成品罐頭<br>輸出: 殺菌罐頭] end %% 第三層:成品處理 subgraph 第三層_成品處理 密封殺菌 --> 冷卻包裝[冷卻與包裝<br>輸入: 殺菌罐頭<br>輸出: 包裝成品] end %% 最終輸出 冷卻包裝 --> 成品[🍑 整果罐頭成品] %% 樣式設置 style 第一層_原料準備 fill:#fff3e0,stroke:#ef6c00,stroke-width:2px style 第二層_密封殺菌 fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px style 第三層_成品處理 fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px style 水果原料 fill:#ffccbc,stroke:#d84315,stroke-width:2px style 成品 fill:#c8e6c9,stroke:#388e3c,stroke-width:3px

輸入輸出關係標註規則如下所示:

  • 輸入:當前工序接收的物料/半成品
  • 輸出:當前工序處理後產生的物料/半成品
  • 每個工序框內都明確標註了輸入和輸出內容
  • 箭頭方向表示物料流向,即上一工序的輸出是下一工序的輸入

對比到app的登陸驗證過程中,用户輸入密碼->app內部做驗證->返回驗證結果,對於用户而言內部怎麼做驗證是不需要關心的,只要密碼正確就應該登錄進賬户,密碼錯誤就應該無法進入賬户。

而對於app而言,它不會直接接收到用户的輸入,用户的輸入直接給到了鍵盤,這中間還有操作系統的工作,app只是接收了某一過程的輸出作為輸入。

同樣對於驗證結果而言,用户能看到的只有屏幕,但是屏幕絕不會做驗證密碼的事情,實際上app輸出的驗證結果也不是直接給到屏幕作為輸入的,這個驗證結果也要經過操作系統的底層操作,輸出給屏幕,然後屏幕再輸出給用户顯示結果。

在學習編程時,都會想要看到反饋的結果,一個普遍的做法是把結果或其他信息輸出到屏幕上,比如pythonprint函數,就可以輸出信息到屏幕,比如print("讀取文件失敗")這一語句就表示程序想要讀取某個文件,但是讀取失敗了。

那麼接下來可以就為啥會讀取失敗這個問題,有針對性的調試代碼了,這種信息就是調試信息。而這個print函數其實並不能直接驅動屏幕,它只是觸發了驅動屏幕的開關,給了下一層級一個輸入,也就是哪個調試信息文本,整個流程就和上面介紹的水果罐頭加工過程一樣,涉及到底層I/O操作。

總結

經過以上內容的介紹,我們知道在學習新語言時,就應該先把握住如下重點:

  1. 它如何表示數據(變量、值),如何聲明變量和賦值,有哪些數據類型
  2. 它如何進行計算(運算符、表達式),有哪些基本運算符(算術、比較、邏輯)
  3. 它如何控制流程(條件、循環),如何寫條件語句(if或等價形式),如何寫循環(while/for或等價形式)
  4. 它如何封裝代碼(函數、作用域),如何定義和調用函數
  5. 它如何與外界交互(I/O),如何讀取輸入和輸出結果

一旦對編程語言的認知框架形成,基本就掌握了使用這個編程語言的能力,處理一些簡單問題是完全足夠的。

在理解語言共性時,應關注功能等價性而非語法相似性。例如,Python的縮進和C++的花括號都是表達代碼塊的方式,本質相同。

這種"透過語法看語義"的視角,才能真正把握編程語言的共性規律。

加油吧,每個初學編程的朋友。

微信公眾號:軟趴趴的工程師
image

原文鏈接

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.