變數範圍

變數的範圍是指變數可存取的程式碼區域。變數範圍有助於避免變數命名衝突。這個概念很直觀:兩個函式都可以有稱為 x 的引數,而這兩個 x 卻不會指涉同一個東西。類似地,還有許多其他情況,不同的程式碼區塊可以使用同一個名稱,而不會指涉同一個東西。同一個變數名稱何時指涉同一個東西,何時不指涉同一個東西的規則稱為範圍規則;本節將詳細說明這些規則。

語言中的某些構造會引入範圍區塊,這些區塊是符合某些變數範圍的程式碼區域。變數的範圍不能是任意的原始碼行;相反地,它總是會與這些區塊之一對齊。Julia 中有兩種主要的範圍類型,全域範圍區域範圍。後者可以巢狀。Julia 中還區分了引入「硬範圍」的構造和僅引入「軟範圍」的構造,這會影響是否允許同名的全域變數遮蔽

範圍構造

引入範圍區塊的構造為

構造範圍類型允許在內部
modulebaremodule全域全域
struct區域 (軟)全域
forwhiletry區域 (軟)全域、區域
巨集區域 (硬)全域
函式、do 區塊、let 區塊、理解、產生器區域 (硬)全域、區域

此表中明顯遺漏了不會引入新範圍的begin 區塊if 區塊。三種類型的範圍遵循略有不同的規則,如下所述。

Julia 使用 詞彙範圍,表示函數的範圍不會繼承其呼叫者的範圍,而是繼承函數定義所在的範圍。例如,在以下程式碼中,foo 內部的 x 指的是 Bar 模組的全局範圍中的 x

julia> module Bar
           x = 1
           foo() = x
       end;

而不是 foo 使用所在範圍中的 x

julia> import .Bar

julia> x = -1;

julia> Bar.foo()
1

因此,詞彙範圍 表示特定程式碼區段中的變數所指涉的內容可以從其單獨出現的程式碼中推論出來,而不依賴於程式碼的執行方式。嵌套在另一個範圍內的範圍可以「看到」它所包含的所有外部範圍中的變數。另一方面,外部範圍無法看到內部範圍中的變數。

全局範圍

每個模組都會引入一個新的全局範圍,與所有其他模組的全局範圍分開,沒有包羅萬象的全局範圍。模組可以透過 using 或 import 陳述式或使用點號表示法進行限定存取,將其他模組的變數引入其範圍中,亦即每個模組都是所謂的名稱空間,也是將名稱與值關聯在一起的一流資料結構。

如果頂層表達式包含使用關鍵字 local 的變數宣告,則該變數在該表達式外無法存取。表達式內的變數不會影響同名的全局變數。一個範例是在頂層的 beginif 區塊中宣告 local x

julia> x = 1
       begin
           local x = 0
           @show x
       end
       @show x;
x = 0
x = 1

請注意,互動式提示(又稱 REPL)位於 Main 模組的全局範圍中。

局部範圍

大多數程式區塊都引入了新的局部範圍(請參閱上方的表格以取得完整清單)。如果此類區塊在語法上嵌套在另一個局部範圍內,則它建立的範圍會嵌套在它出現的所有局部範圍內,而這些範圍最終都嵌套在評估程式碼的模組的全局範圍內。外層範圍中的變數在它們包含的任何範圍中都是可見的,這表示它們可以在內層範圍中讀取和寫入,除非有一個同名的局部變數「遮蔽」了同名的外層變數。即使外層局部變數在內層區塊之後(在文字上位於其下方)宣告,這也是正確的。當我們說一個變數在特定範圍中「存在」,這表示一個具有該名稱的變數存在於當前範圍嵌套在其中的任何範圍內,包括當前範圍。

有些程式語言需要在使用新變數之前明確宣告。明確宣告在 Julia 中也適用:在任何區域範圍內,撰寫 local x 會在該範圍內宣告一個新的區域變數,不論外層範圍內是否已經有一個名為 x 的變數。然而,像這樣宣告每個新變數有點冗長且乏味,因此 Julia 和許多其他語言一樣,將指派給不存在的變數名稱視為隱式宣告該變數。如果目前的範圍是全域的,則新變數是全域的;如果目前的範圍是區域的,則新變數是屬於最內層區域範圍的區域變數,且會在該範圍內可見,但在範圍外不可見。如果您指派給現有的區域變數,它總是會更新該現有的區域變數:您只能透過在巢狀範圍內使用 local 關鍵字明確宣告一個新的區域變數來覆蓋區域變數。特別是,這適用於在內部函式中指派的變數,這可能會讓來自 Python 的使用者感到驚訝,因為在 Python 中,內部函式中的指派會建立一個新的區域變數,除非明確宣告該變數為非區域變數。

大多數情況下,這相當直觀,但與許多直觀行為一樣,細節比人們天真地想像的還要微妙。

x = <value> 出現在區域範圍內時,Julia 會套用下列規則來決定表達式的意義,根據指派表達式出現的位置以及 x 在該位置已經指涉的內容

  1. 現有的區域變數:如果 x 已經是區域變數,則會指派現有的區域變數 x
  2. 硬範圍:x 尚未為區域變數且指派發生在任何硬範圍建構中(即在 let 區塊、函式或巨集主體、理解或產生器內),則會在指派範圍中建立一個名為 x 的新區域變數;
  3. 軟範圍:x 尚未為區域變數且包含指派的範圍建構全部為軟範圍(迴圈、try/catch 區塊或 struct 區塊),則行為取決於是否已定義全域變數 x
    • 若全域 x 未定義,則會在指派範圍中建立一個名為 x 的新區域變數;
    • 若全域 x 已定義,則指派會被視為不明確
      • 非互動式內容(檔案、eval)中,會印出不明確警告並建立新的區域變數;
      • 互動式內容(REPL、筆記本)中,會指派全域變數 x

您可能會注意到,在非互動式內容中,硬範圍和軟範圍的行為相同,但當隱含區域變數(即未透過 local x 宣告)遮蔽全域變數時,會印出警告。在互動式內容中,規則會遵循更複雜的啟發法,以求便利。這會在後續範例中深入探討。

現在您已了解規則,讓我們來看一些範例。假設每個範例都在新的 REPL 會話中評估,因此每個程式碼區塊中的唯一全域變數都是該區塊中指派的變數。

我們將從一個明確的狀況開始—在硬範圍內指派,此處為函式主體,且尚不存在同名的區域變數

julia> function greet()
           x = "hello" # new local
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # global
ERROR: UndefVarError: `x` not defined

greet 函數內部,指定 x = "hello" 會使 x 成為函數範圍內一個新的區域變數。有兩個相關事實:指定發生在區域範圍內,而且沒有現有的區域 x 變數。由於 x 是區域的,因此無論是否有名為 x 的全域變數都不重要。例如,在這裡我們在定義和呼叫 greet 之前定義 x = 123

julia> x = 123 # global
123

julia> function greet()
           x = "hello" # new local
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # global
123

由於 greet 中的 x 是區域的,因此呼叫 greet 也不會影響全域 x 的值(或其缺乏)。硬性範圍規則不關心是否存在名為 x 的全域變數:在硬性範圍內指定 x 是區域的(除非 x 已宣告為全域變數)。

我們將考慮的下一種明確情況是,當已經有一個名為 x 的區域變數時,這種情況下 x = <value> 始終指定給現有的區域 x。無論指定發生在同一個區域範圍、同一個函數主體中的內部區域範圍,還是發生在嵌套在另一個函數中的函數主體中(也稱為 封閉),這都是正確的。

我們將使用 sum_to 函數(用於計算從 1 到 n 的整數總和)作為範例

function sum_to(n)
    s = 0 # new local
    for i = 1:n
        s = s + i # assign existing local
    end
    return s # same local
end

與前一個範例一樣,在 sum_to 頂端的第一次指定 s 會使 s 成為函數主體中的新區域變數。for 迴圈在函數範圍內有自己的內部區域範圍。在 s = s + i 發生時,s 已經是一個區域變數,因此指定會更新現有的 s,而不是建立一個新的區域變數。我們可以在 REPL 中呼叫 sum_to 來測試這一點

julia> function sum_to(n)
           s = 0 # new local
           for i = 1:n
               s = s + i # assign existing local
           end
           return s # same local
       end
sum_to (generic function with 1 method)

julia> sum_to(10)
55

julia> s # global
ERROR: UndefVarError: `s` not defined

由於 ssum_to 函式的區域變數,因此呼叫函式不會對全域變數 s 造成影響。我們也可以看到 for 迴圈中的更新 s = s + i 一定更新了由初始化 s = 0 建立的相同 s,因為我們取得了 1 到 10 的整數正確總和 55。

讓我們深入探討 for 迴圈主體有自己的範圍這項事實,我們將撰寫一個稍微更詳細的變體,我們將稱之為 sum_to_def,其中我們在更新 s 之前將總和 s + i 儲存在變數 t

julia> function sum_to_def(n)
           s = 0 # new local
           for i = 1:n
               t = s + i # new local `t`
               s = t # assign existing local `s`
           end
           return s, @isdefined(t)
       end
sum_to_def (generic function with 1 method)

julia> sum_to_def(10)
(55, false)

這個版本會像之前一樣傳回 s,但它也會使用 @isdefined 巨集傳回一個布林值,表示函式的最外層區域範圍中是否有定義名為 t 的區域變數。正如您所見,在 for 迴圈主體之外沒有定義 t。這是因為嚴格範圍規則:由於對 t 的指定發生在函式內部,這會引入一個嚴格範圍,因此指定會導致 t 變成它出現的區域範圍中的新區域變數,也就是在迴圈主體內部。即使有一個名為 t 的全域變數,也不會造成任何差異—嚴格範圍規則不受全域範圍中的任何事物影響。

請注意,for 迴圈主體的區域範圍與內部函式的區域範圍沒有不同。這表示我們可以重新撰寫此範例,以便將迴圈主體實作為對內部輔助函式的呼叫,並且它會以相同的方式運作

julia> function sum_to_def_closure(n)
           function loop_body(i)
               t = s + i # new local `t`
               s = t # assign same local `s` as below
           end
           s = 0 # new local
           for i = 1:n
               loop_body(i)
           end
           return s, @isdefined(t)
       end
sum_to_def_closure (generic function with 1 method)

julia> sum_to_def_closure(10)
(55, false)

此範例說明了幾個重點

  1. 內部函式範圍就像任何其他巢狀區域範圍。特別是,如果一個變數已經是內部函式外部的區域變數,而您在內部函式中指定它,則會更新外部區域變數。

  2. 外部區域變數的定義是否發生在它被更新的下方並不重要,規則仍然相同。在解析完整個封閉的區域範圍並確定其區域變數之前,會先解析內部區域變數的意義。

此設計表示您通常可以在不改變其含義的情況下將程式碼移入或移出內部函數,這有助於使用封閉函式在語言中使用許多常見慣用語(請參閱do 區塊)。

讓我們探討軟範圍規則涵蓋的更多含糊情況。我們將透過將 greetsum_to_def 函式的本體萃取到軟範圍內容中來探索這一點。首先,讓我們將 greet 的本體放入 for 迴圈中(這是軟的,而不是硬的),並在 REPL 中評估它

julia> for i = 1:3
           x = "hello" # new local
           println(x)
       end
hello
hello
hello

julia> x
ERROR: UndefVarError: `x` not defined

由於在評估 for 迴圈時未定義全域 x,因此軟範圍規則的第一個子句適用,並且 x 被建立為 for 迴圈的區域變數,因此在迴圈執行後,全域 x 仍然未定義。接下來,讓我們考慮將 sum_to_def 的本體萃取到全域範圍,並將其參數修正為 n = 10

s = 0
for i = 1:10
    t = s + i
    s = t
end
s
@isdefined(t)

這段程式碼做了什麼?提示:這是個陷阱題。答案是「取決於情況」。如果這段程式碼是互動輸入的,則其行為方式與在函式本體中相同。但是,如果程式碼出現在檔案中,它會印出含糊警告並擲出未定義變數錯誤。讓我們先在 REPL 中看到它的運作方式

julia> s = 0 # global
0

julia> for i = 1:10
           t = s + i # new local `t`
           s = t # assign global `s`
       end

julia> s # global
55

julia> @isdefined(t) # global
false

REPL 透過判斷迴圈內部的指派是指定給全域變數還是根據是否有定義同名的全域變數來建立新的區域變數,來近似函式本體中的行為。如果存在同名的全域變數,則指派會更新它。如果不存在全域變數,則指派會建立新的區域變數。在此範例中,我們看到這兩種情況的運作

  • 沒有名為 t 的全域變數,因此 t = s + i 會建立一個新的 t,它是 for 迴圈的區域變數;
  • 有一個名為 s 的全域變數,因此 s = t 會指派給它。

第二個事實是為什麼執行迴圈會變更 s 的全域值,而第一個事實是為什麼在迴圈執行後 t 仍然未定義。現在,讓我們嘗試將這段相同的程式碼評估為一個檔案

julia> code = """
       s = 0 # global
       for i = 1:10
           t = s + i # new local `t`
           s = t # new local `s` with warning
       end
       s, # global
       @isdefined(t) # global
       """;

julia> include_string(Main, code)
┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.
└ @ string:4
ERROR: LoadError: UndefVarError: `s` not defined

這裡我們使用 include_string,將 code 評估為一個檔案的內容。我們也可以將 code 儲存到一個檔案,然後對該檔案呼叫 include,結果會是一樣的。正如你所見,這與在 REPL 中評估相同的程式碼有很大的不同。讓我們分析一下這裡發生了什麼事

  • 在評估迴圈之前,全域變數 s 被定義為值 0
  • 指派 s = t 發生在一個軟範圍內,一個在任何函式主體或其他硬範圍結構之外的 for 迴圈
  • 因此軟範圍規則的第二個子句適用,而且指派是模糊的,因此會發出一個警告
  • 執行繼續,使 s 成為 for 迴圈主體的區域變數
  • 由於 sfor 迴圈的區域變數,因此在評估 t = s + i 時它是未定義的,導致錯誤
  • 評估在那裡停止,但如果它到達 s@isdefined(t),它會傳回 0false

這展示了範圍的一些重要面向:在範圍中,每個變數只能有一個意義,而且這個意義會在不考慮表達式順序的情況下決定。迴圈中出現表達式 s = t 會導致 s 成為迴圈的局部變數,這表示當它出現在 t = s + i 的右手邊時,它也是局部變數,即使該表達式最先出現且最先評估。有人可能會想像迴圈第一行的 s 可以是全域變數,而迴圈第二行的 s 是局部變數,但這是不可能的,因為這兩行在同一個範圍區塊中,而每個變數在給定的範圍中只能有一個意義。

關於軟範圍

我們現在已經涵蓋了所有局部範圍規則,但在結束這個區段之前,也許應該說明一下為什麼在互動式和非互動式背景中,處理模稜兩可的軟範圍案例的方式不同。有兩個明顯的問題可以提出

  1. 為什麼它不能在所有地方都像 REPL 一樣運作?
  2. 為什麼它不能在所有地方都像檔案一樣運作?然後跳過警告?

在 Julia ≤ 0.6 中,所有全域範圍都像目前的 REPL 一樣運作:當 x = <value> 出現在迴圈(或 try/catch,或 struct 主體)中,但不在函式主體(或 let 區塊或理解)之外時,會根據是否定義了名為 x 的全域變數來決定 x 是否應該是迴圈的區域變數。此行為的優點在於直觀且方便,因為它盡可能地近似函式主體內的行為。特別是,當嘗試除錯函式的行為時,它可以輕鬆地在函式主體和 REPL 之間前後移動程式碼。然而,它有一些缺點。首先,這是一個相當複雜的行為:多年來,許多人對此行為感到困惑,並抱怨它很複雜,難以解釋和理解。這是個公平的觀點。其次,可以說是更糟糕的是,它不利於「大規模」程式設計。當你在一個地方看到像這樣一小段程式碼時,很清楚發生了什麼事

s = 0
for i = 1:10
    s += i
end

顯然,目的是修改現有的全域變數 s。它還能是什麼意思?然而,並非所有實際的程式碼都這麼短或這麼清楚。我們發現以下類型的程式碼經常出現在實際應用中

x = 123

# much later
# maybe in a different file

for i = 1:10
    x = "hello"
    println(x)
end

# much later
# maybe in yet another file
# or maybe back in the first one where `x = 123`

y = x + 234

這裡應該發生什麼事不太清楚。由於 x + "hello" 是方法錯誤,因此 x 可能是 for 迴圈的區域變數。但執行時期值和碰巧存在的哪些方法無法用於確定變數的範圍。對於 Julia ≤ 0.6 的行為,特別令人擔憂的是,有人可能先寫了 for 迴圈,讓它正常運作,但後來當其他人遠在其他地方(可能是不同的檔案)新增一個新的全域變數時,程式碼突然改變了意義,而且不是吵雜地中斷,就是更糟的是,靜默地做錯事。這種 "遠距幽靈行為" 是良好的程式語言設計應該防止的事情。

因此在 Julia 1.0 中,我們簡化了範圍規則:在任何區域範圍內,對尚未成為區域變數的名稱進行指定會建立新的區域變數。這完全消除了軟範圍的概念,並消除了幽靈動作的可能性。由於軟範圍的移除,我們發現並修正了大量的錯誤,證明了移除它的選擇是正確的。而且大家都非常高興!好吧,其實並沒有。因為有些人對於現在必須撰寫

s = 0
for i = 1:10
    global s += i
end

您看到那裡的 global 註解嗎?很可怕。顯然這種情況不能容忍。但說真的,對於這種頂層程式碼需要 global 有兩個主要問題

  1. 不再方便將函數主體內的程式碼複製並貼到 REPL 中進行除錯,您必須新增 global 註解,然後再將它們移除才能回到原本的狀態;

  2. 初學者會在沒有 global 的情況下撰寫這種程式碼,並且不知道為什麼他們的程式碼無法運作,他們得到的錯誤是 s 未定義,這似乎無法啟發犯下這個錯誤的人。

從 Julia 1.5 開始,此程式碼在互動式環境(例如 REPL 或 Jupyter 筆記本)中不需要 global 註解即可運作(就像 Julia 0.6 一樣),而在檔案和其他非互動式環境中,它會印出這個非常直接的警告

對軟範圍中的 s 進行指定是模稜兩可的,因為存在同名的全域變數:s 將被視為新的區域變數。使用 local s 來抑制此警告或使用 global s 來指定到現有的全域變數以消除歧義。

這同時解決了這兩個問題,同時保留了 1.0 行為的「大規模程式設計」優點:全域變數不會對遠處程式碼的意義產生令人毛骨悚然的影響;在 REPL 中,複製貼上除錯有效,初學者不會有任何問題;任何時候,如果有人忘記 global 註解或意外地使用軟範圍中的區域變數遮蔽現有的全域變數(這無論如何都會令人困惑),他們都會收到一個明確的警告。

此設計的一個重要屬性是,在沒有警告的情況下在檔案中執行的任何程式碼,在新的 REPL 中都會以相同的方式執行。相反,如果您進行 REPL 會話並將其儲存到檔案中,如果它的行為與在 REPL 中不同,那麼您將收到警告。

Let 區塊

let 陳述式會建立一個新的硬範圍區塊(見上文),並在每次執行時引入新的變數繫結。變數不需要立即指定

julia> var1 = let x
           for i in 1:5
               (i == 4) && (x = i; break)
           end
           x
       end
4

儘管指定可能會將新值重新指定給現有值位置,但 let 始終會建立一個新位置。此差異通常不重要,僅在透過封閉變數而超出其範圍的變數情況下才能偵測到。let 語法接受以逗號分隔的指定和變數名稱序列

julia> x, y, z = -1, -1, -1;

julia> let x = 1, z
           println("x: $x, y: $y") # x is local variable, y the global
           println("z: $z") # errors as z has not been assigned yet but is local
       end
x: 1, y: -1
ERROR: UndefVarError: `z` not defined

指定會按順序評估,每個右手邊會在左手邊的新變數引入之前在範圍內評估。因此,撰寫類似 let x = x 的內容是有意義的,因為兩個 x 變數是不同的,並且有獨立的儲存空間。以下是一個需要 let 行為的範例

julia> Fs = Vector{Any}(undef, 2); i = 1;

julia> while i <= 2
           Fs[i] = ()->i
           global i += 1
       end

julia> Fs[1]()
3

julia> Fs[2]()
3

在這裡,我們建立並儲存兩個會傳回變數 i 的封閉變數。然而,它始終是同一個變數 i,因此這兩個封閉變數的行為完全相同。我們可以使用 leti 建立新的繫結

julia> Fs = Vector{Any}(undef, 2); i = 1;

julia> while i <= 2
           let i = i
               Fs[i] = ()->i
           end
           global i += 1
       end

julia> Fs[1]()
1

julia> Fs[2]()
2

由於 begin 結構並未引入新的範圍,因此使用零參數 let 來僅引入新的範圍區塊而無需立即建立任何新繫結,這會很有用

julia> let
           local x = 1
           let
               local x = 2
           end
           x
       end
1

由於 let 引入了新的範圍區塊,因此內部區域變數 x 與外部區域變數 x 是不同的變數。此特定範例等同於

julia> let x = 1
           let x = 2
           end
           x
       end
1

迴圈與推論

在迴圈和 推論 中,在主體範圍中引入的新變數會在每次迴圈反覆運算時重新配置,就像迴圈主體被 let 區塊包圍一樣,此範例說明了這一點

julia> Fs = Vector{Any}(undef, 2);

julia> for j = 1:2
           Fs[j] = ()->j
       end

julia> Fs[1]()
1

julia> Fs[2]()
2

for 迴圈或推論反覆運算變數永遠都是新變數

julia> function f()
           i = 0
           for i = 1:3
               # empty
           end
           return i
       end;

julia> f()
0

不過,偶爾會需要重複使用現有的區域變數作為反覆運算變數。透過新增關鍵字 outer,可以方便地執行此動作

julia> function f()
           i = 0
           for outer i = 1:3
               # empty
           end
           return i
       end;

julia> f()
3

常數

變數的常見用途是為特定不變值命名。此類變數只會指定一次。可以使用 const 關鍵字將此意圖傳達給編譯器

julia> const e  = 2.71828182845904523536;

julia> const pi = 3.14159265358979323846;

可以在單一 const 陳述式中宣告多個變數

julia> const a, b = 1, 2
(1, 2)

const 宣告應僅用於全域範圍中的全域變數。編譯器很難最佳化涉及全域變數的程式碼,因為其值(或甚至其型別)幾乎隨時都可能變更。如果全域變數不會變更,新增 const 宣告可以解決此效能問題。

區域常數相當不同。編譯器能夠自動判斷區域變數是否為常數,因此不需要區域常數宣告,而且事實上目前並不支援。

由關鍵字 functionstruct 執行等特殊頂層指派預設為常數。

請注意 const 僅影響變數繫結;變數可能會繫結到可變物件(例如陣列),而該物件仍可能被修改。此外,當有人嘗試將值指派給宣告為常數的變數時,可能會出現以下情況

  • 如果新值與常數類型不同,則會擲回錯誤
julia> const x = 1.0
1.0

julia> x = 1
ERROR: invalid redefinition of constant x
  • 如果新值與常數類型相同,則會印出警告
julia> const y = 1.0
1.0

julia> y = 2.0
WARNING: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors.
2.0
  • 如果指派不會導致變數值變更,則不會傳送任何訊息
julia> const z = 100
100

julia> z = 100
100

最後一項規則適用於不可變物件,即使變數繫結會變更,例如

julia> const s1 = "1"
"1"

julia> s2 = "1"
"1"

julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
 Ptr{UInt8} @0x00000000132c9638
 Ptr{UInt8} @0x0000000013dd3d18

julia> s1 = s2
"1"

julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
 Ptr{UInt8} @0x0000000013dd3d18
 Ptr{UInt8} @0x0000000013dd3d18

然而,對於可變物件,會如預期般印出警告

julia> const a = [1]
1-element Vector{Int64}:
 1

julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Vector{Int64}:
 1

請注意,儘管有時可行,但強烈建議不要變更 const 變數的值,且僅供互動式使用時方便使用。變更常數可能會造成各種問題或意外行為。例如,如果方法參照常數,且在常數變更前已編譯,則它可能會繼續使用舊值

julia> const x = 1
1

julia> f() = x
f (generic function with 1 method)

julia> f()
1

julia> x = 2
WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors.
2

julia> f()
1

型別全域變數

Julia 1.8

Julia 1.8 中新增了對型別全域變數的支援

類似於宣告為常數,全域繫結也可以宣告為始終為常數類型。這可以使用語法 global x::T 在未指派實際值的情況下完成,或在指派時以 x::T = 123 完成。

julia> x::Float64 = 2.718
2.718

julia> f() = x
f (generic function with 1 method)

julia> Base.return_types(f)
1-element Vector{Any}:
 Float64

對於任何對全域變數的指派,Julia 都會先嘗試使用 convert 將其轉換為適當的類型

julia> global y::Int

julia> y = 1.0
1.0

julia> y
1

julia> y = 3.14
ERROR: InexactError: Int64(3.14)
Stacktrace:
[...]

類型不需要具體,但具有抽象類型的註解通常效能提升有限。

一旦全局變數已被指定或其類型已被設定,繫結類型就不允許變更

julia> x = 1
1

julia> global x::Int
ERROR: cannot set type for global x. It already has a value or is already set to a different type.
Stacktrace:
[...]