代碼之髓讀後感——名字&做用域&類型

名字和做用域

爲何要取名

看着代碼中遍地都是的變量,函數,或多或少的咱們都應該想過,爲何會有這些名字呢?java

咱們知道,計算機將數據存儲到對應的物理內存中去。咱們的操做就是基於數據的。咱們須要使用這些數據,因此一個問題就是如何尋找到這些數據。一個較爲直接的方式就是爲它起個名字。python

聯繫現實生活中的,最典型的就是圖書館。一本本書,一塊塊數據。爲了查找,咱們使用的是對各個數據地址進行編碼。一一映射到一組惟一的數據上,以此便於查找的惟一替代彼不便於查找的惟一程序員

仔細想來,這種替代的方法彷佛和哈希函數的思想有思想卻是有些接近。或許其中就是使用了呢?ruby

實際上,每一個名字對應的都是實際內存中的地址。所以也必需要有一個表來存儲映射關係。ide

因此在語言的發展過程當中,對照表的設計也是一個關鍵之處。函數

做用域的演變

在早期的程序設計語言中對照表是整個程序共有的。這樣的設計,雖然必定程度上解決了映射的管理的問題,可是,這就至關於如今全局的做用域的概念,一處變,到處變,一次變,次次變。爲了防止變量名的重複使用,防止名字的衝突,一種方法就是使用更長的變量名。在這種過程當中,各類命名法就出現了。固然,對於如今來講,合理的命名對於程序的可讀性,易擴展性也是有極大的幫助的。再有就是使用做用域的限定。工具

隨着程序規模的擴大,爲了更方便管理變量,將不一樣層級的變量各自劃分範圍,將各個變量的有效範圍進行約束,因而出現了做用域的概念。ui

正是因爲原先的相似於全局做用域的設定,致使當前出現了做用範圍太大,出現衝突的可能性太大,進而由此想出瞭解決辦法——縮小做用域。編碼

做用域隨着後來的發展,出現了兩種類型——動態與靜態。spa

所謂的做用域就是指某段程序文本代碼。一個聲明起做用的那一段程序文本區域,則稱爲這個聲明的做用域。靜態做用域是指聲明的做用域是根據程序正文在編譯時就肯定的,有時也稱爲詞法做用域。而在採用動態做用域的語言中,程序中某個變量所引用的對象是在程序運行時刻根據程序的控制流信息來肯定的。

從對照表的角度來分析一下靜態做用域和動態做用域。

動態

動態做用域中的對照表能被所有代碼讀取

  1. 最初全局對照表中記錄了x與global的映射。

  2. 進入函數a,準備新的全局可見的對照表。

  3. 函數a寫入變量x的值記錄在新的對照表中。

  4. 進入函數b,因爲還未退出a,故其對照表仍有效。因此讀取其中的x的值。(參照變量的時候,按照由近及遠的順序讀取。如果訪問該對照表中沒有記錄的變量時,就翻轉到外層的對照表查找,這裏是全局對照表)

  5. 退出b。

  6. 退出a時,做廢新的對照表。

以下例子:

$x = "global";

sub a {
    local $x = "local";
    &b();
}

sub b {
    print "$x\n";
    # 輸出「local」
}

&a();

把變量原來的值實現保存在函數入口處,在出口處寫回變量中。這樣一來,在程序中間的改寫在退出函數的時候不會繼續影響。可是這樣就要要求,凡當函數退出時,全部地方就要毫無遺漏的加上返回值的代碼。因而處於懶惰的心理,咱們總但願讓計算機去完成這樣的工做。

1991年發佈的Perl4開始,Perl語言就增長了這樣的功能。它經過把變量聲明爲local,就能夠長程序處理器去承擔「把原來的值另存起來隨後返回」的任務。
這樣的做用域稱爲動態做用域。動態做用域中被改寫的值會影響到被調用函數,所以在引用變量時是什麼樣的值,不看函數調用方是無從得知的。這就使得在代碼規模龐大時,不便於把握。

靜態(又叫字面做用域,詞法域等等)

靜態做用域按照函數區分對照表

  1. 最初全局對照表中記錄了x與global的映射。

  2. 進入函數a,準備函數a專用的對照表。

  3. 函數a寫入變量x的值記錄在專用的的對照表中。

  4. 進入函數b,準備函數b專用的對照表。

  5. 讀取變量值,會先讀取b專用表,沒有結果則去讀取全局變量的表。

  6. 退出b,做廢該張對照表。

  7. 退出a,做廢該張對照表。

以下例子:

$x = "global";

sub a {
    my $x = "my";
    &b();
}

sub b {
    print "$x\n";
    # 輸出「global」
}

&a();

如今說全局變量很差或者全局污染緣由即是在此。實際操做中儘可能減少做用範圍,是一種明智的選擇。

靜態做用域的不足

以一開始就採用了靜態做用域的python語言爲例:

2000年發佈的python2.0中,對照表有三個層次(做用域)。從大到小分別是內置的,全局的,局部的。簡單而言,每一個程序都有一張總體對照表(內置),一張文件級別的對照表(全局),一張函數級別的對照表(局部)。

內置對照表,能夠在程序的任何地方使用參照,全局對照表是針對每一個文件的,由於有的語言也稱之爲文件做用域,局部做用域則是針對每一個函數的對照表。

在實際使用中,主要出現如下問題:

嵌套函數的問題

python支持函數嵌套定義。

x = "global"
def a():
    x = "a"
    def b():
        print x
    b()
a()

在python2.0最初的設計中,如上函數嵌套時,當b中的局部做用域中找不到x,接下來去找的是全局做用域。

這樣的設計引來不少誤解,人們經常會覺得,從表面看,由於a中包含b,因此a的做用域也包含b的做用域,當b中找不到x時,參照相鄰的外部a的做用域。

後來在2011年發佈的python2.1中設計修改成了逐層往外尋找的策略。

外部做用域的再綁定問題

指的是採用靜態做用域時,沒法變動嵌套做用域外部的變量。當嵌套的內層函數變量賦值時,如果當前做用域沒有該值時,就會在當前的做用域定義一個新的局部變量。對這個名字進行了一次再次綁定,爲他關聯了另外的值。可是,這並不會影響外部的做用域。即沒法變動外部的變量。

爲了解決這個小問題,2006年的python3.0提供了關鍵字nolocal,在函數開始時,聲明變量爲nolocal性質。即主動地聲明是非本地的。

這個關鍵字的選取,主要考慮了對於過往代碼的兼容性,他在過去的代碼中,出現的頻度是最低的。

這樣的nolocalglobal關鍵字有區別麼?

對於和python很相似的ruby而言,則是使用了方法與代碼段的區分。

函數發生嵌套,形式上有兩種類型。方法套方法,方法套代碼段。

以下兩例:

# 方法在進行嵌套時做用於不嵌套
def foo()
    x = "outside"
    def bar() # 方法嵌套
        p x   # 會出錯,由於沒法訪問外部的x
    end


# 方法中有代碼段時,方法的局部做用域中有的名字,在代碼段中視爲方法的局部變量,除此之外被視爲代碼段的局部變量。
# 相同名字則爲方法的,不一樣的,則是本身的。
def foo()
    x = "old"
    lambda {x = "new"; y = "new"}.call
    # x -> foo(), y -> lambda的本地變量
    p x # new
    p y # lambda的本地變量,外部沒法訪問
end

做用域總結

雖然如今不多會使用動態做用域,但這一律念並非徹底沒有用處。與靜態做用域中做用域是源代碼級別上的一塊完整獨立的範圍不一樣,在動態做用域中,做用域則是進入該做用域開始直至離開這一時間軸上的完整獨立的範圍。與此相同的特徵也體如今其餘好多地方。好比,在某處理進行期間,一時改變某變量的值隨後將原值返回的代碼編寫方式就至關於建立了本身專屬的動態做用域。又如,異常處理與動態做用域也很類似,函數拋出異常時的處理方式受到調用函數的 try/catch 語句的影響
面向對象中像 private 聲明這樣的訪問修飾符,在限制可訪問範圍的做用上和做用域是很是類似的。private 將可訪問範圍限制在類以內,而 protected 將此範圍擴大到其繼承類。這和函數調用處的變動會影響到調用裏面的操做這一動態做用域表現是類似的,二者都具備這麼一個缺點,這就是影響範圍沒有能限制在代碼的某一個地方
好比 Java 語言,它是靜態做用域語言,它的類能夠在源代碼的任意處被訪問。這意味着類是具備全局做用域的。可是類的名字具備層次而且只有導入後才能被使用,這避免了全局變量帶來的無心的名字衝突。可是無論是全局變量仍是類的靜態成員均可以在源代碼的任意地方被變動。這提醒咱們,在享受使用上的便利的同時,要謹防濫用致使的代碼難以理解的狀況發生。
做用域是編寫易於理解的代碼的有力工具,不少地方都應用了這一律念。


類型

C,java,C++等語言中的int,void,double,float等等,這些是怎麼出現的?如今又有了怎樣的變化與發展?

類型是什麼

類型是人們給數據附加的一種追加數據。計算機中保存的數據是由 on 和 off 或 0 和 1 的組合來表達的。至於 on 和 off 的組合(比特列)是如何表達各類數值的,哪一種比特列表示哪一種值,這些只不過是人們簡單的約定事項而已。一樣的比特列,當其被解釋爲的數據的類型不一樣時,獲得的數值是不一樣的。爲了不這一狀況的發生,人們追加了關於數據的類型信息,這就是類型的起源。

計算機中的數值是整數、浮點數仍是其餘類型的數,爲了在計算機中管理這一信息,因而催生了類型。起初,類型中只加入了數值的種類信息,最後又有多種多樣的信息加入進來。好比,能在這個數值上施加的操做、此函數可能拋出的異常等信息都被加入到類型中來了。如今,像靜態類型和動態類型那樣連內存地址和使用時間都不同的事物也被稱爲類型,這使得類型這種東西變得愈來愈難以捉摸。什麼樣的信息放在什麼地方,在什麼樣的時間被使用,從這個視角來看反而更容易理解。

表達數字的思考

如何在電子計算機中表達數值呢?如前所述,在計算機中全部的數值都用 on 和 off 或 0 和 1 的組合來表達。爲了更形象地說明,咱們換個角度來思考,該如何用燈泡的點亮與熄滅來表達數值呢?爲了充分利用資源,只能是用最少的技術標量來表示最多的數。

徹底按個數(n個計數標量) 
        -> 數位(阿拉伯數字)(10個計數標量) 
                    -> 七段數碼管(7個計數標量) 
                                    -> 算盤(最少的5個,上一下四)
                                                -> 從十進制到二進制(9也只須要4個,1001)

把二進制中某幾個字符組合在一塊兒用一個字符來表示,使之變得更容易讀,這種表達方式就是八進制或十六進制。

如何表達實數

實數的複雜之處在於在正整數的基礎上添加了小數和負數。因此針對這兩種狀況進行設計。

定點數——小數點位置肯定

一種方法是肯定小數點的的位置。好比,約定好把整數的小數點向左移動四位,最低四位就是小數部分。這樣一來,1 變成 0.0001,100 變成 0.0100 即 0.01.這種方法有個問題,它沒法表達比 0.0001 小的數,好比沒法表達 0.00001。固然只要把約定改成把整數的小數點向左移動五位獲得小數部分就能夠,但這樣針對每個新的小數都要記一句新的約定很困難,並且還容易出錯。那該怎麼辦呢?

浮點數(floating point number)——數值自己包含小數部分何處開始的信息

之前關於浮點數有各類不一樣的約定,如今都標準化爲 IEEE 75415。15官方名稱爲「IEEE Standard for Floating-Point Arithmetic (ANSI/IEEE Std 754-2008)"。IEEE 754 最先制定於 1985 年,後於 2008 年進行了修訂。另外,在此標準規定有 5 種標準類型,這裏僅僅說明了其中的單精度二進制浮點數。

img

左邊那盞燈(最高比特位),也能夠稱之爲 MSB(most significant bit),最高有效位 表明了數的符號。該位爲 0 時表示正數,爲 1 時表示負數,在標準中,零區分爲正的零和負的零。

接下來的 8 盞燈是表示位數的指數部分。指數部分做爲整數理解的話能夠表達 0~255 之間的數,減去 127 獲得範圍-127~128。-127 和 128 分別表明了零和無限大,剩下的-126~127 表明了小數點的位置。-126 是指小數點向左移動 126 位,127 是指小數點向右移動 127 位。

其他的 23 盞燈是尾數部分,表示了小數點如下的部分。尾數部最左邊的燈泡表示 1/2(二進制中的 0.1),接下來是 1/4(二進制中的 0.01)。請看圖中的 1.75 這個數,它等於 1+1/2+1/4,用二進制來表示就是 1.11。因此,1/2 位的燈泡和 1/4 位的燈泡都點亮。指數部分爲 127(要減去 127 就是範圍中的 0),這表示小數點的位置移動 0 位。這兩點組合起來就是 1.75。

準確來說,尾數是在二進制表達中爲使得整數部分變成 1 而移動小數點獲得的小數部分。

接下來的數 3.5,用二進制來表示是 11.1。小數點向左移動一位就獲得 1.11。因此它的尾數部分和 1.75 同樣,1/2 位和 1/4 位點亮。指數部分變成 128(減去 127 就是範圍中的 1)。3.5(二進制中的 11.1)其實就是 1.75(二進制中的 1.11)的小數點向右移動一位獲得的數 19。而 7.0 則是由指數部分繼續加 1 獲得。

19在二進制中,小數點移動一位進位不是 10 倍而是 2 倍。指數部分加 1,變成 2 倍,減 1 變成 1/2。

浮點數的問題

現今你們接觸到的語言中,實數大多用浮點數 IEEE 754 表達。從實用角度來看,大部分狀況下這沒有任何問題。可是,這種方法要表達 3 除以 10 的答案時,十進制中能夠確切表達出來的 0.3 在二進制中卻變成了 0.0100110011001100110011……這樣的無限循環小數,不管怎麼寫都有偏差存在。正由於如此,會出現對0.3作十次加法並捨去某些位數後獲得2這樣的現象。銀行和外匯交易等涉及資金操做的場合尤爲不歡迎這種系統行爲,因此這些場合使用的是定點數或者加三碼(excess-3)這樣的十進制計算方式。

爲何出現類型

在內存中記錄的數值是整數仍是浮點數,單靠人的記憶很難避免錯誤。有沒有更爲簡易的方法呢?

  • 一種方法是用肯定的規則來表示變量名所表達的內容。好比,早期的 FORTRAN 語言使用了一系列規則,指定以 I~N 開頭的變量名錶示整數,除此之外的表示浮點數。

  • 另外一種更好的方法是告訴處理器某某變量是整數,讓計算機而不是人去記憶這一信息。這就是變量的類型的聲明產生的緣由。好比 C 語言中,聲明 int x; 表示名字爲 x 的變量指向的內存被解釋爲整數,聲明 float y; 表示名字爲 y 的變量指向的內存被解釋爲浮點數。這樣經過提供關於類型的信息,處理器在進行運算時,就能自動判斷該作整數相加運算仍是浮點數相加運算,而不須要人們逐個去指定。

    • 整數之間、浮點數之間的運算計算機參照數據的類型來決定怎樣執行。若是 x 和 y 同爲整數,就作整數之間的加法運算。若是 x 和 y 同爲浮點數,就作浮點數之間的加法運算。

    • 整數與浮點數之間的計算,則依據語言不一樣採用了不一樣的方法。有顯示使用轉換函數(如早期的 FORTRAN 語言),也有隱式自動轉換的(如C)。

      • C 語言中採用的設計方法是由計算對象的類型來決定是否捨去小數部分。這一方法在很長時間內被不少語言使用,以致於不少程序員都很是習慣,認爲理所固然。然而,這個不是恆久不變的物理法則,只不過是人們確立的設計方法而已。所以並非全部的語言都採用這種設計。

      • 一些語言使用特定的運算符來處理是否保留小數的狀況。

        • 1973 年問世的 ML 語言中,整數的除法運算就表達爲 x div y, 而浮點數的除法運算表達爲 x / y。

        • OCaml 中也用 x / y 和 x /. y 來區分整數的除法運算和浮點數的除法運算。

        • 1991 年問世的 Python 語言起初使用的是混雜着 C 語言風格的除法運算方式。

        • 2008 年發佈的 Python 3.0 中,把 x / y 做爲與 x 和 y 類型無關不作捨去的除法運算,帶捨去的除法運算用 x // y 來表示。

類型的發展

  1. 使用語言中自帶的基本數據類型經過組合定義新的類型的這一功能被髮明出來。這被稱爲用戶定義型

  • 如 C 語言中的結構體。

  • 在 C 語言以前的 COBOL 語言中,能夠用基本的類型組合起來定義一種帶有層次結構的記錄類型。

  • PL/I 語言也有能組合基本類型並建立新的類型的語句 DEFINE STRUCTURE。(結構體(structure)這個術語應該就是從那時候開始使用)的。

其實,不只限於整數這樣的數據,函數這樣決定數據如何被處理的對象也被糅合到類型中來了。C++ 語言的設計者本賈尼·斯特勞斯特盧普把用戶能自定義的類型看成構造程序的基本要素,把這種類型冠名爲類。

  1. 後來出現了類型既是功能的觀念。這種觀念認爲,構成結構體和類的類型不該該是所有公開而是最小限度地公開,類型是否一致這個交由編譯器來檢查,用類型來表達功能,與功能是否一致也是由編譯器來檢查。所以,只須要將與外部有交互的部分做爲類型公開,而實現的細節則隱藏起來。這樣類型就被區分爲公開部分和非公開部分了。

  2. 出現了不包含有具體的實現細節的類型(Java 語言中的接口等)。

  3. 另外把函數是否拋出異常這一信息也看成類型。

類型便是功能的方法獲得了愈來愈普遍地應用,但遺憾的是,用類型來實現全部功能的想法卻尚未成功。若是它能成功,就很理想了:只要類型一致就不用關心內部的實現細節,功能與類型的不一致交由編譯器來檢查,編譯經過意味着沒有 bug。然而,仍有很多類型沒法表達的信息,如輸入這個數據須要多少處理時間,這個處理過程須要多少內存,線程中是否能夠進行這種操做等。至今,這些問題也只能經過人爲地讀取文檔和源代碼來判斷。

總稱型、泛型和模板

經過將不一樣類型進行組合獲得複雜的類型後,使用中會出現想更改其中一部分卻又不想所有從新定義的再利用需求。

所以出現了構成要素部分可變的類型,即總稱型。想要表現不一樣的狀況時,出現了以類型爲參數建立類型的函數(C++ 語言中的模板、Java 語言中的泛型以及 Haskell 語言中的類型構造器能夠說就是這種建立類型的機制)。

動態靜態類型

到目前爲止,咱們介紹的類型的機制中,處理器把變量名、保存數值的內存地址、內存裏的內容的類型三者做爲一個總體來看待。把類型的信息和數值看做總體的方式叫動態類型。做爲其反義詞,到目前爲止介紹的類型機制都叫靜態類型。如今大多數的腳本語言都採用了動態類型。

動態類型如何實現的呢?

是由於在內存上使用了同等類型對待的設計方法。好比 Python 語言中,無論是整數仍是浮點數仍是字符串,所有都做爲 PyObject 對待,開始部分都是同樣的。另外在 PyObject 類型的結構中還預留了保存值的類型信息的地方。這一點在其它的腳本語言中也是一樣的狀況,好比在 Ruby 語言中,任何數值都是 VALUE 類型的。

img

※ 使用次數是指在內存管理中記錄這個數值有幾處被參照引用的數值(引用計數)。
※ 字符串的散列值是散列函數的計算結果(詳見第 9 章),狀態是表示該字符串是否記錄在 Internpool 裏(處理器是否把該字符串進行惟一處理的標誌)。

動態類型的優點與不足

使用這種數值類型處理方法,能實現從來靜態類型語言不能實現的靈活處理。運行時肯定類型和改變類型成爲可能。然而,它也有一些不足。靜態類型語言在編譯時肯定類型,同時編譯時也檢查了類型的一致性。有了這種類型檢查,在實際執行前,便能發現一部分bug。這一點動態類型語言是沒法作到的。

類型推斷

既不放棄編譯時的類型檢查,也想盡可能減小麻煩的類型聲明,要實現這一要求就要用到計算機自動推論肯定類型的方法。

一樣是使用類型推斷這一術語,在不一樣的語言中,如何作類型推斷以及類型推斷的能力如何,狀況是不同的。咱們來比較一下 Haskell 語言和 Scala 語言。

GHCi
> :type identity identity
identity identity :: t -> t
> identity identity 1
1

Scala 語言中類型推斷的行爲和 Haskell 語言是不同的。它會首先來定義 identify 函數。

Scala的對話終端
scala> def identity = x => x
<console>:7: error: missing parameter type
       def identity = x => x
                      ^

scala> def identity[T] = (x : T) => x
identity: [T]=> T => T

scala> identity(identity)
res0: Nothing => Nothing = <function1>
scala> identity(identity)(1)
<console>:9: error: type mismatch;
 found   : Int(1)
 required: Nothing
              identity(identity)(1)
                                 ^

因而可知,一樣是使用類型推斷的表達方法,不一樣語言指示的具體內容是不同的。剛剛展現了 Scala 語言推論失敗的一個例子,即便推論成功了,在實用價值上有沒有優點這個問題上,你們也是有意見分歧的。即便認可它的優點而對類型推斷的機制進行修改,在由此帶來的做業代價與推論失敗的代價之間作權衡以後,再決定否應該作改進和變動將是一個更加困難的問題。

強類型下是否能夠作到程序沒有bug

類型推斷與理論推論之間有對應關係。因而有些語言發出挑戰,試圖經過使用比 C 語言和 Java 語言更強力的類型系統來證實程序中沒有任何 bug。從此在改善類型系統的表現力和類型推斷規則方面應該會開展各類研究。

好比,在一個接受 X 型參數返回 Y 型返回值的函數中傳遞一個 X 型的數值,會獲得 Y 型的返回值。這一關於類型的描述與「X 爲真在若是 X 則 Y 的狀況下,Y 就爲真」這一邏輯的描述是相對應的,被稱爲 Curry-Howard 對應。
相關文章
相關標籤/搜索