控制流

Julia 提供了各种控制流结构

前五种控制流机制在高级编程语言中是标准的。 Task 并不那么标准:它们提供了非局部控制流,使得在临时挂起的计算之间切换成为可能。这是一个强大的结构:异常处理和协作式多任务处理都在 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,则评估相应的块;如果这两个表达式都不为真,则评估 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 是一个条件表达式,如果条件 atrue,则三元运算会评估 : 之前的表达式 b,或者如果它为 false,则评估 : 之后的表达式 c。请注意,?: 周围的空格是必需的:类似 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中,只有当a计算结果为false时,才会计算子表达式b

原因是,如果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循环遍历这些值,依次将每个值赋给变量iwhile循环形式和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

各种类型的可迭代容器将在手册的后面部分介绍和讨论(例如,参见多维数组)。

有时需要在测试条件被证伪之前终止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都会中断正常的控制流程。

异常
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 函数

可以使用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 处理错误后运行。

任务(也称为协程)

任务是一种控制流特性,允许以灵活的方式暂停和恢复计算。我们在这里仅出于完整性而提及它们;有关完整讨论,请参阅 异步编程