控制流程

Julia 提供各種控制流程建構

前五種控制流程機制是高級程式語言的標準。工作並非如此標準:它們提供非本機控制流程,讓暫時中斷的運算之間可以互相切換。這是一個強大的建構:例外處理和協作多工處理都是使用工作在 Julia 中實作的。日常程式設計不需要直接使用工作,但某些問題可以使用工作更輕鬆地解決。

複合表達式

有時,將一個表達式用於依序評估多個子表達式會比較方便,並傳回最後一個子表達式的值作為其值。有兩個 Julia 建構可以達成此目的:begin 區塊和 ; 鏈。複合表達式建構的值是最後一個子表達式的值。以下是 begin 區塊的範例

julia> z = begin
           x = 1
           y = 2
           x + y
       end
3

由於這些表達式相當小且簡單,因此可以輕鬆地放在單一行中,這正是 ; 鏈語法派上用場的地方

julia> z = (x = 1; y = 2; x + y)
3

此語法在 函式 中介紹的簡潔單行函式定義形式中特別有用。雖然很常見,但沒有規定 begin 區塊必須是多行或 ; 鏈必須是單行

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

條件式評估

條件式評估允許程式碼部分取決於布林表達式的值進行評估或不評估。以下是 if-elseif-else 條件語法的結構

if x < y
    println("x is less than y")
elseif x > y
    println("x is greater than y")
else
    println("x is equal to y")
end

如果條件式 x < ytrue,則對應區塊會被評估;否則條件式 x > y 會被評估,如果為 true,則對應區塊會被評估;如果兩個表達式都不是 true,則 else 區塊會被評估。以下是實際運作

julia> function test(x, y)
           if x < y
               println("x is less than y")
           elseif x > y
               println("x is greater than y")
           else
               println("x is equal to y")
           end
       end
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

elseifelse 區塊是可選的,並且可以根據需要使用任意數量的 elseif 區塊。if-elseif-else 結構中的條件式會被評估,直到第一個評估為 true,之後會評估關聯區塊,並且不會評估其他條件式或區塊。

if 區塊是「漏水的」,也就是說它們不會引入局部範圍。這表示在 if 子句內定義的新變數可以在 if 區塊之後使用,即使它們之前未定義。因此,我們可以將上述的 test 函式定義為

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           else
               relation = "greater than"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(2, 1)
x is greater than y.

變數 relationif 區塊內宣告,但在區塊外使用。然而,當依賴於此行為時,請確保所有可能的程式碼路徑都為變數定義一個值。對上述函式的以下變更會導致執行時期錯誤

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(1,2)
x is less than y.

julia> test(2,1)
ERROR: UndefVarError: `relation` not defined
Stacktrace:
 [1] test(::Int64, ::Int64) at ./none:7

if 區塊也會傳回一個值,這對於來自許多其他語言的使用者來說可能不直觀。這個值只是所選分支中最後一個執行陳述式的傳回值,因此

julia> x = 3
3

julia> if x > 0
           "positive!"
       else
           "negative..."
       end
"positive!"

請注意,非常簡短的條件式陳述式(單行式)通常使用下一個區段中概述的 Julia 中的短路評估來表達。

與 C、MATLAB、Perl、Python 和 Ruby 不同,但與 Java 和其他一些更嚴格的類型化語言類似,如果條件表達式的值不是 truefalse,則會出錯

julia> if 1
           println("true")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

此錯誤表示條件類型錯誤:Int64 而不是必需的 Bool

所謂的「三元運算子」?:if-elseif-else 語法密切相關,但用於需要在單一表達式值之間進行條件選擇的情況,而不是條件執行較長的程式碼區塊。它之所以得名,是因為它是大多數語言中唯一使用三個運算元的運算子

a ? b : c

? 前面的表達式 a 是條件表達式,三元運算會評估 : 前面的表達式 b,如果條件 atrue,或評估 : 後面的表達式 c,如果條件為 false。請注意,?: 周圍的空格是強制性的:類似於 a?b:c 的表達式不是有效的三元表達式(但在 ?: 之後都可以接受換行符號)。

了解此行為最簡單的方法是看一個範例。在前面的範例中,println 呼叫由所有三個分支共用:唯一真正的選擇是要列印哪個文字字串。可以使用三元運算子更簡潔地撰寫此內容。為了清楚起見,我們先嘗試雙向版本

julia> x = 1; y = 2;

julia> println(x < y ? "less than" : "not less than")
less than

julia> x = 1; y = 0;

julia> println(x < y ? "less than" : "not less than")
not less than

如果表達式 x < y 為真,則整個三元運算式表達式會評估為字串 "less than",否則會評估為字串 "not less than"。原始的三元範例需要將三元運算式的多個用途串連在一起

julia> test(x, y) = println(x < y ? "x is less than y"    :
                            x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

為了方便串連,運算子從右到左關聯。

重要的是,就像 if-elseif-else,在 : 之前和之後的表達式僅在條件表達式分別評估為 truefalse 時才會評估

julia> v(x) = (println(x); x)
v (generic function with 1 method)

julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"

julia> 1 > 2 ? v("yes") : v("no")
no
"no"

短路評估

Julia 中的 &&|| 運算子分別對應於邏輯「且」和「或」運算,並且通常用於此目的。但是,它們具有短路評估的附加屬性:它們不一定會評估其第二個參數,如下所述。(還有按位元 &| 運算子可用作邏輯「且」和「或」,沒有短路行為,但要注意 &| 在評估順序上具有比 &&|| 更高的優先順序。)

短路評估與條件評估非常相似。大多數具有 &&|| 布林運算子的命令式程式語言中都可找到此行為:在由這些運算子連接的一系列布林表達式中,僅評估確定整個鏈的最終布林值所需的最小數量表達式。有些語言(例如 Python)將它們稱為 and (&&) 和 or (||)。明確地說,這表示

  • 在表達式 a && b 中,只有在 a 評估為 true 時,才會評估子表達式 b
  • 在表達式 a || b 中,子表達式 b 只有在 a 評估為 false 時才會被評估。

推理是如果 afalse,則 a && b 必為 false,而與 b 的值無關,同樣地,如果 atrue,則 a || b 的值必為 true,而與 b 的值無關。&&|| 都向右結合,但 && 的優先權高於 ||。很容易用此行為來做實驗

julia> t(x) = (println(x); true)
t (generic function with 1 method)

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

julia> t(1) && t(2)
1
2
true

julia> t(1) && f(2)
1
2
false

julia> f(1) && t(2)
1
false

julia> f(1) && f(2)
1
false

julia> t(1) || t(2)
1
true

julia> t(1) || f(2)
1
true

julia> f(1) || t(2)
1
2
true

julia> f(1) || f(2)
1
2
false

你可以用相同的方式輕鬆地實驗 &&|| 算子的各種組合的結合性和優先權。

此行為經常在 Julia 中用於形成非常短的 if 陳述式的替代方案。代替 if <cond> <statement> end,可以寫成 <cond> && <statement>(可以讀作:<cond> 然後 <statement>)。類似地,代替 if ! <cond> <statement> end,可以寫成 <cond> || <statement>(可以讀作:<cond> 否則 <statement>)。

例如,遞迴階乘例程可以這樣定義

julia> function fact(n::Int)
           n >= 0 || error("n must be non-negative")
           n == 0 && return 1
           n * fact(n-1)
       end
fact (generic function with 1 method)

julia> fact(5)
120

julia> fact(0)
1

julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fact(::Int64) at ./none:2
 [3] top-level scope

沒有短路求值的布林運算可以使用在 數學運算和基本函數 中介紹的位元布林運算子來完成:&|。這些是正常函數,碰巧支援中綴運算子語法,但總是評估其引數

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

就像在 ifelseif 或三元運算子中使用的條件表達式一樣,&&|| 的運算元必須是布林值(truefalse)。在條件鏈中的最後一個條目以外的任何地方使用非布林值都是錯誤的

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

另一方面,任何類型的表達式都可以用在條件鏈的結尾。它將根據前面的條件進行評估和返回

julia> true && (x = (1, 2, 3))
(1, 2, 3)

julia> false && (x = (1, 2, 3))
false

重複評估:迴圈

有兩個建構用於重複評估表達式:while 迴圈和 for 迴圈。以下是 while 迴圈的範例

julia> i = 1;

julia> while i <= 3
           println(i)
           global i += 1
       end
1
2
3

while 迴圈評估條件表達式(在本例中為 i <= 5),只要它保持為 true,就會繼續評估 while 迴圈的主體。如果在第一次到達 while 迴圈時條件表達式為 false,則主體永遠不會被評估。

for 迴圈讓常見的重複評估慣用語更易於撰寫。由於像上述 while 迴圈那樣向上向下計數非常常見,因此可以用 for 迴圈更簡潔地表達

julia> for i = 1:3
           println(i)
       end
1
2
3

這裡的 1:3 是範圍物件,代表數字 1、2、3 的順序。for 迴圈會反覆運算這些值,將每個值依序指定給變數 i。先前 while 迴圈形式和 for 迴圈形式之間一個相當重要的區別是變數可見的範圍。for 迴圈總會在其主體中引入新的反覆運算變數,無論封閉範圍中是否存在同名的變數。這意味著一方面 i 不需要在迴圈之前宣告。另一方面,它在迴圈外不會可見,同名的外部變數也不會受到影響。您需要新的互動式工作階段執行個體或不同的變數名稱來測試這一點

julia> for j = 1:3
           println(j)
       end
1
2
3

julia> j
ERROR: UndefVarError: `j` not defined
julia> j = 0;

julia> for j = 1:3
           println(j)
       end
1
2
3

julia> j
0

使用 for outer 來修改後面的行為並重新使用現有的區域變數。

有關變數範圍、變數範圍outer 的詳細說明,請參閱以及它們在 Julia 中如何運作。

一般而言,for 迴圈建構可以反覆運算任何容器。在這些情況下,通常會使用替代的(但完全等效的)關鍵字 in 來取代 =,因為它使程式碼更易於閱讀

julia> for i in [1,4,0]
           println(i)
       end
1
4
0

julia> for s ∈ ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

本手冊的後續章節中將介紹和討論各種可反覆運算的容器類型(例如,請參閱 多維陣列)。

有時在測試條件變為 false 之前終止 while 的重複,或在到達可反覆運算物件的結尾之前停止 for 迴圈中的反覆運算會比較方便。這可以使用 break 關鍵字來完成

julia> i = 1;

julia> while true
           println(i)
           if i >= 3
               break
           end
           global i += 1
       end
1
2
3

julia> for j = 1:1000
           println(j)
           if j >= 3
               break
           end
       end
1
2
3

如果沒有 break 關鍵字,上述 while 迴圈將永遠不會自行終止,而 for 迴圈將反覆運算到 1000。這兩個迴圈都使用 break 提早退出。

在其他情況下,能夠停止反覆運算並立即繼續下一個反覆運算會很方便。continue 關鍵字可以完成此操作

julia> for i = 1:10
           if i % 3 != 0
               continue
           end
           println(i)
       end
3
6
9

這是一個有點牽強的範例,因為我們可以透過否定條件並將 println 呼叫置於 if 區塊中來更清楚地產生相同的行為。在實際使用中,continue 之後有更多程式碼要評估,而且通常有多個呼叫 continue 的點。

多個巢狀 for 迴圈可以合併成一個單一的外部迴圈,形成其可反覆運算項的笛卡兒積

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

使用此語法時,可迭代物件仍可參照外層迴圈變數;例如 for i = 1:n, j = 1:i 是有效的。然而,此類迴圈中的 break 陳述式會退出迴圈巢狀結構的全部,而並非僅退出內層迴圈。每次執行內層迴圈時,兩個變數 (ij) 都會設定為其目前的迭代值。因此,對 i 的指派不會顯示在後續的迭代中

julia> for i = 1:2, j = 3:4
           println((i, j))
           i = 0
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

如果將此範例改寫為對每個變數使用 for 關鍵字,則輸出結果會不同:第二和第四個值會包含 0

使用 zip,可以在單一 for 迴圈中同時迭代多個容器

julia> for (j, k) in zip([1 2 3], [4 5 6 7])
           println((j,k))
       end
(1, 4)
(2, 5)
(3, 6)

使用 zip 會建立一個迭代器,該迭代器是一個包含傳遞給它的容器的子迭代器的元組。zip 迭代器會依序迭代所有子迭代器,在 for 迴圈的第 $i$ 次迭代中選取每個子迭代器的第 $i$ 個元素。一旦任一子迭代器用盡,for 迴圈就會停止。

例外狀況處理

當發生意外狀況時,函式可能無法傳回合理的數值給呼叫函式。在這種情況下,最好的方式可能是讓例外狀況終止程式,同時印出診斷錯誤訊息,或者如果程式設計師已提供處理此類例外狀況的程式碼,則允許該程式碼採取適當的動作。

內建 Exception

當發生意外情況時,會擲回 Exception。下方列出的內建 Exception 會中斷正常的控制流程。

Exception
ArgumentError
BoundsError
CompositeException
DimensionMismatch
DivideError
DomainError
EOFError
ErrorException
InexactError
InitError
InterruptException
InvalidStateException
KeyError
LoadError
OutOfMemoryError
ReadOnlyMemoryError
RemoteException
MethodError
OverflowError
Meta.ParseError
SystemError
TypeError
UndefRefError
UndefVarError
StringIndexError

例如,sqrt 函式如果套用於負實數值,會擲回 DomainError

julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

您可以用以下方式定義自己的例外狀況

julia> struct MyCustomException <: Exception end

The throw function

可以使用 throw 明確建立例外狀況。例如,僅定義給非負數的函式可以寫成在引數為負數時 throw DomainError

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative"))
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError with -1:
argument must be nonnegative
Stacktrace:
 [1] f(::Int64) at ./none:1

請注意,沒有括號的 DomainError 不是例外狀況,而是例外狀況的類型。需要呼叫它才能取得 Exception 物件

julia> typeof(DomainError(nothing)) <: Exception
true

julia> typeof(DomainError) <: Exception
false

此外,有些例外狀況類型會採用一個或多個用於錯誤回報的引數

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: `x` not defined

這個機制可以透過自訂例外狀況類型輕鬆實作,方式請參照 UndefVarError 的寫法

julia> struct MyUndefVarError <: Exception
           var::Symbol
       end

julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
注意事項

撰寫錯誤訊息時,建議將第一個字元設為小寫。例如,

size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))

優於

size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B")).

不過,有時保留第一個大寫字母是有意義的,例如函數的引數是大寫字母

size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension...")).

錯誤

error 函數用於產生 ErrorException,會中斷正常的控制流程。

假設我們想要在取負數的平方根時立即停止執行。為此,我們可以定義 sqrt 函數的一個囉嗦版本,如果其引數為負數,則會引發錯誤

julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)

julia> fussy_sqrt(2)
1.4142135623730951

julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt(::Int64) at ./none:1
 [3] top-level scope

如果從另一個函數呼叫 fussy_sqrt,並傳入負值,它不會嘗試繼續執行呼叫函數,而是立即傳回,並在互動式工作階段中顯示錯誤訊息

julia> function verbose_fussy_sqrt(x)
           println("before fussy_sqrt")
           r = fussy_sqrt(x)
           println("after fussy_sqrt")
           return r
       end
verbose_fussy_sqrt (generic function with 1 method)

julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951

julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt at ./none:1 [inlined]
 [3] verbose_fussy_sqrt(::Int64) at ./none:3
 [4] top-level scope

try/catch 陳述式

try/catch 陳述式允許測試 Exception,並以優雅的方式處理可能會中斷應用程式的問題。例如,在以下程式碼中,平方根函數通常會擲回例外。透過在函數周圍放置 try/catch 區塊,我們可以緩解此問題。你可以選擇如何處理此例外,無論是記錄、傳回佔位符值,或像以下情況中只印出陳述式。在決定如何處理意外情況時,需要考慮的一件事是使用 try/catch 區塊比使用條件分支來處理這些情況慢得多。以下是使用 try/catch 區塊處理例外的更多範例

julia> try
           sqrt("ten")
       catch e
           println("You should have entered a numeric value")
       end
You should have entered a numeric value

try/catch 陳述式也允許將 Exception 儲存在變數中。以下的人工範例會計算 x 第二個元素的平方根,如果 x 可索引,否則假設 x 是實數並傳回其平方根

julia> sqrt_second(x) = try
           sqrt(x[2])
       catch y
           if isa(y, DomainError)
               sqrt(complex(x[2], 0))
           elseif isa(y, BoundsError)
               sqrt(x)
           end
       end
sqrt_second (generic function with 1 method)

julia> sqrt_second([1 4])
2.0

julia> sqrt_second([1 -4])
0.0 + 2.0im

julia> sqrt_second(9)
3.0

julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

請注意,catch 之後的符號將始終被解釋為例外的名稱,因此在單行上撰寫 try/catch 表達式時需要小心。以下程式碼在發生錯誤時不會傳回 x 的值

try bad() catch x end

請改用分號或在 catch 之後插入換行符號

try bad() catch; x end

try bad()
catch
    x
end

try/catch 建構的強大之處在於能夠立即將深度巢狀的運算解開到呼叫函數堆疊中的更高層級。有些情況下沒有發生錯誤,但需要解開堆疊並將值傳遞到更高層級。Julia 提供 rethrowbacktracecatch_backtracecurrent_exceptions 函數以進行更進階的錯誤處理。

else 子句

Julia 1.8

此功能至少需要 Julia 1.8。

在某些情況下,您可能不僅想要適當地處理錯誤情況,還希望僅在 try 區塊成功時執行一些程式碼。為此,可以在 catch 區塊之後指定一個 else 子句,該子句會在先前未擲出任何錯誤時執行。與其將此程式碼包含在 try 區塊中的優點在於,任何進一步的錯誤都不會被 catch 子句靜默捕捉。

local x
try
    x = read("file", String)
catch
    # handle read errors
else
    # do something with x
end
注意事項

trycatchelsefinally 子句各自引入自己的範圍區塊,因此如果變數僅在 try 區塊中定義,則 elsefinally 子句無法存取它

julia> try
           foo = 1
       catch
       else
           foo
       end
ERROR: UndefVarError: `foo` not defined

try 區塊外部使用 local 關鍵字,讓變數可以從外部範圍內的任何位置存取。

finally 子句

在執行狀態變更或使用檔案等資源的程式碼中,通常需要在程式碼完成時執行清理工作(例如關閉檔案)。例外情況可能會使這項任務複雜化,因為它們可能導致程式碼區塊在到達正常結束之前就退出。finally 關鍵字提供了一種方法,可以在給定的程式碼區塊退出時執行一些程式碼,無論它是如何退出的。

例如,以下是我們如何保證已開啟的檔案會關閉

f = open("file")
try
    # operate on file f
finally
    close(f)
end

當控制離開 try 區塊時(例如因為 return,或只是正常完成),將執行 close(f)。如果 try 區塊因為例外狀況而離開,例外狀況將繼續傳播。catch 區塊也可以與 tryfinally 結合使用。在這種情況下,finally 區塊將在 catch 處理錯誤後執行。

任務(也稱為協程)

任務是一種控制流程功能,允許以彈性的方式暫停和恢復運算。我們在此僅為了完整性而提及它們;有關完整討論,請參閱 非同步程式設計