控制流
分支和循环等控制流是命令式编程风格的核心。作为一种多范式编程语言,Julia支持所有知名的命令式风格和结构。
复合语句
语句是程序员给计算机下达的命令。如打印一段文字,或给一个变量赋初值。
julia> println("Hello, world!")Hello, world!julia> a = 10.010.0
Julia每次只能执行一条语句。如果需要把多条语句作为一个大型的复合执行单元(如同一条语句)来看待,则可以使用begin和end块将任意语句集包围起来。
julia> z = begin x = 1 y = 2 x + y end3julia> z3
块中最后一个表达式的值作为复合块的值。同样的表达式可以在单行中写成使用大括号包围的由分号分隔的语句集。
julia> z = (x = 1; y = 2; x + y)3
下面也是有效的复合语句。
julia> z = (x = 1; y = 2; x + y)3julia> z = begin x=1; y=2; x+y end3
由上可见,要在单行中编写多条语序,需要使用;将每条语句分隔开来。另外,Julia是通过最小语义单元来判断输入的内容是否是单行的,而不是通过回车换行来判断。
julia> a = 11
注释
随着程序越来越长,越来越复杂,阅读起来也变得更加困难。因此,通过对程序增加笔记来说明它到底做了些什么是一个非常明智的方法。这些笔记被称为注释,它们以 # 号开头:
julia> # 计算已用时间的百分比(总时长是60分) percentage = (20 * 100) / 6033.333333333333336
在这个例子中,注释独占一行。你也可以将注释放到一行的末尾:
julia> percentage = (20 * 100) / 60 # 已用时间的百分比33.333333333333336
# 号以后的所有内容都被程序忽略掉——它对程序的执行不起任何作用。
当我们写的注释需要连续占用很多行,这时在每行的开头都使用#会很麻烦。还有一种情况,我们可能需要在同一行的一条语句的中间插入注释,这时#会完全失效(因为#以后的内容被全部忽略),Julia提供的另一种注释方法#=...=#可以解决这类问题。
julia> #= 一行注释 另外一行注释 =# v = 55julia> b = #=在中间插入注释=# 3.23.2
分支
条件语句是编程语言的核心。它们提供了在代码中定义分支的途径,并且是迭代编程的基础。使用本地goto进行分支足以模拟编程语言中的任何迭代模型。
if...else
if...else是Julia中最常见的条件执行形式。一个典型的if条件如下所示:
if <Boolean condition>
# do something
else
# do the something else
endelse部分是可选的。条件必须是布尔值。与布尔值true或false相同的整数不能代替对应的布尔值。
julia> if 1 println("Integer is good for Bool") endERROR: TypeError: non-boolean (Int64) used in boolean contextjulia> if Bool(1) println("Bool now") endBool now
if表达式返回被执行分支的值。
julia> i = 11julia> str = if i > 1 "Greater" else "Less" end"Less"julia> str"Less"
同样的表达式也可以用三元运算符?和:
julia> str = i > 1 ? "Greater" : "Less""Less"julia> str"Less"
虽然在大多数表达式中可以不使用空格,但在?和:两侧的空格则是必须的。
julia> str = i > 1? "Greater" : "Less"
ERROR: syntax: space required before "?" operator
julia> str = i > 1 ? "Greater": "Less"
ERROR: syntax: space required before colon in "?" expression当遇到多重分支操作时,可使用if...elseif...else结构。
julia> val = 33julia> if val == 1 "one" elseif val == 2 "two" elseif val == 3 "three" elseif val == 4 "four" else "unknown" end"three"
非结构分支
诸如if...elseif...else的结构分支,是编程语言中条件执行的最优选择。然而,一些语言具有goto语句来将执行分支跳转到代码中的特定位置。Julia也使用宏@goto和@label提供了这样的功能。它们有以下限制:
它们被限制在特定的代码块中。
@goto可以在同一代码块中使用@label。它们不能跨函数使用。
它们会影响代码的可读性。因此,通常在大多数结构化编程语言中不使用。
Julia具有结构化分支和迭代,因此完全可以不使用非结构化分支。下面的代码仅是使用@goto和@label来对从1到10的连续数进行求和的示例:
julia> begin s = 0 n = 10 @label loop s = s + n n = n - 1 if n > 0 @goto loop end s end55
显然,对于序列求和,迭代要比非结构化分支简单得多。
短路求值
Julia中的&&和||运算符分别对应逻辑“和”和“或”运算。此外,它们还有一个附加的短路求值属性:它们不一定对第二个参数求值(也有按位的&和|运算符,可以用作逻辑的“和”和“或”,它们不具有短路行为,但要注意&和|的优先级高于&&和||)。
短路求值与条件求值非常相似:在由这些运算符连接的一系列布尔表达式中,整个链的最终布尔值由计算最小数量的表达式来确定。这意味着:
在表达式
a && b中,子表达式b仅在a的计算结果为true时才被计算。在表达式
a || b中,子表达式b仅在a的计算结果为false时才计算。
理由是:如果是a是false,不管b的值如何,a && b必是false。同样,如果是a是true,不管b的值如何,a || b必是true。&&和||都是右结合的,但&&的优先级高于||。验证这种行为的方法很容易:
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 truejulia> t(1) && f(2)1 2 falsejulia> f(1) && t(2)1 falsejulia> f(1) && f(2)1 falsejulia> t(1) || t(2)1 truejulia> t(1) || f(2)1 truejulia> f(1) || t(2)1 2 truejulia> f(1) || f(2)1 2 false
可以很容易地以同样的方式验证&&和||运算符各种组合的结合性和优先级。
这种行为在Julia中经常被用来代替非常简短的if语句。if <条件> <语句> end可以写成<条件> && <语句>(可以读作:<条件>成立则执行<语句>)。类似地,if !<条件> <语句> end可以写成<条件> || <语句>可以读作:<条件>成立否则执行<语句>)。
例如,一个递归的阶乘可以这样定义:
julia> function fact(n::Int) n >= 0 || error("n must be non-negative") n == 0 && return 1 n * fact(n-1) endfact (generic function with 1 method)julia> fact(5)120julia> fact(0)1julia> fact(-1)ERROR: n must be non-negative
不需要短路求值的布尔运算可以通过按位布尔运算符来完成:&和|。它们是普通函数,支持中缀运算符语法,总是对实参进行求值计算:
julia> f(1) & t(2)1 2 falsejulia> t(1) | t(2)1 2 true
就像if、elseif或三元运算符中使用的条件表达式一样,&&或||的运算数必须是布尔值(true或false)。在条件链中除了最后一项的任何地方使用非布尔值都是错误的:
julia> 1 && trueERROR: TypeError: non-boolean (Int64) used in boolean context
另一方面,可以在条件链的末尾使用任何类型的表达式。它将根据前面的条件被计算和返回:
julia> true && (x = (1, 2, 3))(1, 2, 3)julia> false && (x = (1, 2, 3))false
迭代
虽然我们可以使用非结构化分支goto实现代码的迭代执行,但作为一种高级语言,Julia提供了迭代执行的特定语言结构。最常见的是for和while。
for
julia> s = 0;julia> for i = 1:10 s = s + i endjulia> s55
在上面的代码片段中,我们从1枚举到10,并将值累加到s。在for循环中,我们将一个步长为1的范围对象1:10赋给参数i。该对象也可以写成1:1:10,表示<初始值>:<步长>:<最终值>。现在,我们修改代码,使其只对奇数进行累加。
julia> s = 0;julia> for i = 1:2:10 println(i) s = s + i end1 3 5 7 9julia> s25
可以看出,将范围对象的步长更改为2就可以达到预期的结果。
continue和break
让我们在求和时忽略所有能被3整除的数。
julia> s = 0;julia> for i = 1:10 if i % 3 == 0 continue end println(i) s = s + i end1 2 4 5 7 8 10julia> s37
在这种情况下,continue用于跳过所有能被3整除的数。continue确保在迭代器仍处于活动状态时,表达式后面的代码块被排除在计算之外,而在代码中使用break将终止循环。
julia> s = 0;julia> for i = 1:10 if i % 3 == 0 break end println(i) s = s + i end1 2julia> s3
for...in
除了范围对象,for也可以与其他具有迭代器功能的序列对象一起使用。我们将在后面详细讨论迭代器接口。下面是一个for...in语法的代码片段。
julia> for i in [5,10,15] println(i) end5 10 15
多范围对象
一个for循环可以遍历多个范围对象。效果是在由左侧指定外层循环的范围的笛卡尔积上迭代。
julia> for i=1:3, j=1:2 println((i,j)) end(1, 1) (1, 2) (2, 1) (2, 2) (3, 1) (3, 2)
内部范围也可以受外部范围值的影响。
julia> for i=1:3, j=1:i println((i, j)) end(1, 1) (2, 1) (2, 2) (3, 1) (3, 2) (3, 3)
这里的for循环是一个单独的循环,break语句可以完全终止它,这与嵌套的for循环只终止内部循环不同。
julia> for i=1:3, j=1:2 println((i, j)) if i == j break end end(1, 1)julia> for i=1:3 for j=1:2 println((i, j)) if i == j break end end end(1, 1) (2, 1) (2, 2) (3, 1) (3, 2)
while
在Julia中,for用于迭代器和范围对象。while则更灵活,可以在任何条件下发挥作用。只要条件为真,循环就会继续执行。下面是一般语法。
while <Boolean_condition is true>
<loop>
end现在,如果我们考虑相同的1-10的加法例子,那么代码将如下所示:
julia> s, n = 0, 10;julia> while n > 0 s = s + n n = n - 1 endjulia> s55
与for循环类似,可以分别通过continue和break来跳过或终止执行。
while循环重复计算起始处的条件,如果结果为true,则执行循环。在某些情况下,存在不管条件如何,都需要至少执行一次的循环,并在循环结束时计算新的条件来判断是否继续执行循环。这相当于其他语言中的do...while循环。Julia目前没有这个构造。但实现这个语义逻辑却很简单。
julia> println("press q <enter> to end loop")
press q <enter> to end loop
julia> while true
ch = readline()
ch == "q" && break
end
a
q异常处理
当程序出现异常时,执行的函数可能无法返回一个有效值或使程序处于可恢复状态。有些程序可能会报告错误并终止执行。然而,在报告异常情况的同时,使程序恢复状态是非常有益的。其次,恢复的状态一定在函数调用的多个深度的调用堆栈中。因此,异常可能导致控制流移动到调用堆栈中的另一个函数,而不仅仅是调用方的地址空间。
try...catch
将预期具有异常条件的代码放置在try之后。异常处理代码添加到catch之后的部分。end表达式结束try块。
julia> try sqrt(-1) catch e println(e) endDomainError(-1.0, "sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).")
没有try...catch块的代码将在REPL中报告以下内容:
julia> sqrt(-1)ERROR: DomainError with -1.0: sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
可以看到,REPL提供了默认的try...catch实现,它接受DomainError异常并打印一个带有堆栈跟踪的错误。Julia有相当多的标准异常,如ErrorException、DomainError、ArgumentError、BoundsError等。所有错误都派生自抽象类型Exception。
throw/rethrow
虽然catch提供了捕获异常并对异常采取操作的选项,但人们可以利用rethrow函数使异常进一步传播,而不是处理它。打印的错误信息是REPL默认的错误处理方法。
julia> try sqrt(-1) catch e rethrow() endERROR: DomainError with -1.0: sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
当出现一些意外的条件时,还可以抛出自定义异常。抛出的对象不一定总是Exception类型。它可以是任何对象。下面给出了一个抛出整数的例子:
julia> try throw(1) catch e println((e,typeof(e))) end(1, Int64)
异常用于管理异常条件。如果行为只是代码流中的常规条件的表示,应使用其他控制流如if...else。其次,异常会增加大量的执行开销。因此,只有在需要时才有选择地使用它们。
finally
当资源正在使用时,异常情况可能会迫使函数突然退出执行,这种情况下的资源无法回收。finally子句代码在正常和异常条件下都会被执行,确保资源被回收。在下面的示例中,即使出现异常,文件句柄f也会关闭。
julia> f = open("/etc/hosts")IOStream(<file /etc/hosts>)julia> isopen(f)truejulia> try b = write(f,"abc") catch e println(e) finally close(f) endArgumentError("write failed, IOStream is not writeable")julia> isopen(f)false
异常的信息
因为每次堆栈展开时都需要验证异常对象,因此它们会影响执行速度。那么,如此复杂架构的异常对象为程序添加了什么价值?它们提供的主要优势之一是能够跟踪异常发生的堆栈。因此,可以得到错误的传播轨迹。其次,异常对象的形式提供了关于了解错误足够信息的能力。
julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[1] sqrt
@ ./math.jl:582 [inlined]
[2] sqrt(x::Int64)
@ Base.Math ./math.jl:608
[3] top-level scope
@ REPL[73]:1DomainError有两个属性。val属性表示导致错误的值,msg属性详细描述消息细节。你可以通过创建Exception抽象类型的子类型来定义自己的异常。
julia> struct MyException <: Exception params1 params2 # ... paramsn end
堆栈跟踪
堆栈跟踪是从异常处理中获得的一组非常重要的信息。函数catch_backtrace()在抛出异常时报告堆栈跟踪,而stacktrace()函数提供了对前面函数返回结果的解释。
julia> try
sqrt(-1)
catch e
stacktrace(catch_backtrace())
end
3-element Vector{Base.StackTraces.StackFrame}:
sqrt at math.jl:582 [inlined]
sqrt(x::Int64) at math.jl:608
top-level scope at REPL[76]:2