Julia 流程控制

流程控制语句通过程序设定一个或多个条件语句来实现。在条件为 true 时执行指定程序代码,在条件为 false 时执行其他指定代码。

Julia 提供了大量的流程控制语句:

  • 复合表达式begin;

  • 条件表达式:if-elseif-else?: (三元运算符)。

  • 短路运算:逻辑运算符 &&(与)和 ||(或),以及链式比较。

  • 循环语句:循环:whilefor

  • 异常处理:try-catcherrorthrow

  • Task(协程):yieldto


复合表达式

begin ... end 表达式可以按顺序计算若干子表达式,并返回最后一个子表达式的值:

实例代码

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 boolean_expression 1
   /* 当布尔表达式 1 为真时执行 */
elseif boolean_expression 2
   /* 当布尔表达式 2 为真时执行 */
elseif boolean_expression 3
   /* 当布尔表达式 3 为真时执行 */
else
   /* 当上面条件都不为真时执行 */

一个 if 语句后可跟一个可选的 else if...else 语句,这可用于测试多种条件。

当使用 if...else if...else 语句时,以下几点需要注意:

  • 一个 if 后可跟零个或一个 else,else 必须在所有 else if 之后。
  • 一个 if 后可跟零个或多个 else if,else if 必须在 else 之前。
  • 一旦某个 else if 匹配成功,其他的 else if 或 else 将不会被测试。

下面是对 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 < y 是 true,那么对应的代码块会被执行;否则判断条件表达式 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

三元运算符

三元运算符 ?: 类似 if-elseif-else 语法:

a ? b : c

? 之前的表达式 a, 是一个条件表达式,如果条件 a 是 true,三元运算符计算在 : 之前的表达式 b;如果条件 a 是 false,则执行 : 后面的表达式 c。

注意:?: 旁边的空格是强制的,像 a?b:c 这种表达式不是一个有效的三元表达式(但在? 和 : 之后的换行是允许的)。

实例代码

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 类似,: 之前和之后的表达式只有在条件表达式为 true 或者 false 时才会被相应地执行:

实例代码

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 中的 &&|| 运算符分别对应于逻辑 "与""或" 操作。

  • 在表达式 a && b 中,子表达式 b 仅当 atrue 的时候才会被执行,如果 a 为 false 则直接返回 false。
  • 在表达式 a || b 中,子表达式 b 仅在 afalse 的时候才会被执行,如果 a 为 true ,直接返回 a。

&& 实例

julia> isodd(3) && @warn("An odd Number!")
┌ Warning: An odd Number!
└ @ Main REPL[5]:1

julia> isodd(4) && @warn("An odd Number!")
false

|| 实例

julia> isodd(3) || @warn("An odd Number!")
true

julia> isodd(4) || @warn("An odd Number!")
┌ Warning: An odd Number!
└ @ Main REPL[8]:1

&& 和 || 都依赖于右边,但是 && 比 || 有更高的优先级,查看实例:

实例代码

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

循环语句

循环语句使用 whilefor 两个关键字来实现。

下面是一个 while 循环的例子:

实例代码

julia> i = 1;

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

while 循环语句通过会执行条件表达式( i <= 5),只要它为 true,就一直执行 while 循环的主体部分。如果条件表达式为 false,也就是 i=6 的时候,那么循环就结束了。

for 循环使用起来会更加方便,以上实例使用 for 循环实现如下:

实例代码

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

这里的 1:5 是一个范围对象,代表数字 1, 2, 3, 4, 5 的序列。

for 循环在这些值之中迭代,对每一个变量 i 进行赋值。

for 循环与之前 while 循环的一个非常重要区别是作用域,即变量的可见性。如果变量 i 没有在另一个作用域里引入,在 for 循环内,它就只在 for 循环内部可见,在外部和后面均不可见。你需要一个新的交互式会话实例或者一个新的变量名来测试这个特性:

实例代码

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

julia> j
ERROR: UndefVarError: j not defined

一般来说,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 >= 5
               break
           end
           global i += 1
       end
1
2
3
4
5

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

没有关键字 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 语句则会跳出整个嵌套循环,不仅仅是内层循环。每次内层循环运行的时候,变量(i 和 j)会被赋值为他们当前的迭代变量值。所以对 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)

julia> for x in zip(0:15, 100:110, 200:210)
               println(x)
            end
(0, 100, 200)
(1, 101, 201)
(2, 102, 202)
(3, 103, 203)
(4, 104, 204)
(5, 105, 205)
(6, 106, 206)
(7, 107, 207)
(8, 108, 208)
(9, 109, 209)
(10, 110, 210)

// 处理元素数量不一致
julia> for x in zip(0:10, 100:115, 200:210)
               println(x)
            end
(0, 100, 200)
(1, 101, 201)
(2, 102, 202)
(3, 103, 203)
(4, 104, 204)
(5, 105, 205)
(6, 106, 206)
(7, 107, 207)
(8, 108, 208)
(9, 109, 209)
(10, 110, 210)

使用 zip 将创建一个迭代器,它是一个包含传递给它的容器的子迭代器的元组。 zip 迭代器将按顺序迭代所有子迭代器,在 for 循环的第 ii 次迭代中选择每个子迭代器的第 ii 个元素。 一旦任何子迭代器用完,for 循环就会停止。

for 语句可以嵌套多个循环条件,使用逗号 , 分隔:

实例代码

julia> for n in 1:5, m in 1:5
                  @show (n, m)
               end
(n, m) = (1, 1)
(n, m) = (1, 2)
(n, m) = (1, 3)
(n, m) = (1, 4)
(n, m) = (1, 5)
(n, m) = (2, 1)
(n, m) = (2, 2)
(n, m) = (2, 3)
(n, m) = (2, 4)
(n, m) = (2, 5)
(n, m) = (3, 1)
(n, m) = (3, 2)
(n, m) = (3, 3)
(n, m) = (3, 4)
(n, m) = (3, 5)
(n, m) = (4, 1)
(n, m) = (4, 2)
(n, m) = (4, 3)
(n, m) = (4, 4)
(n, m) = (4, 5)
(n, m) = (5, 1)
(n, m) = (5, 2)
(n, m) = (5, 3)
(n, m) = (5, 4)
(n, m) = (5, 5)

推导式

推导式是一种独特的数据处理方式,可以从一个数据序列构建另一个新的数据序列的结构体。

格式如下:

[表达式 for 变量 in 列表]
[out_exp_res for out_exp in input_list]

或者

[表达式 for 变量 in 列表 if 条件]
[out_exp_res for out_exp in input_list if condition]


实例代码

julia> [X^2 for X in 1:5]
5-element Array{Int64,1}:
 1
 4
 9
 16
 25

我们还可以指定想要生成的元素类型:

实例代码

julia> Complex[X^2 for X in 1:5]
5-element Array{Complex,1}:
 1 + 0im
 4 + 0im
 9 + 0im
 16 + 0im
 25 + 0im

遍历数组

有时我们希望遍历数组的每个元素,包含元素的索引号。

Julia 提供了 enumerate(iter) 函数,参数 iter 为可迭代对象,该函数将生成索引号以及每个索引号对应的值。

实例代码

julia> a = ["a", "b", "c"];

julia> for (index, value) in enumerate(a)
           println("$index $value")
       end
1 a
2 b
3 c

julia> arr = rand(0:9, 4, 4)
4×4 Array{Int64,2}:
 7 6 5 8
 8 6 9 4
 6 3 0 7
 2 3 2 4
 
julia> [x for x in enumerate(arr)]
4×4 Array{Tuple{Int64,Int64},2}:
 (1, 7) (5, 6) (9, 5) (13, 8)
 (2, 8) (6, 6) (10, 9) (14, 4)
 (3, 6) (7, 3) (11, 0) (15, 7)
 (4, 2) (8, 3) (12, 2) (16, 4)

异常处理

程序执行过程中,如果发生意外条件,一个函数可能无法向调用者返回一个合理的值。在这种情况下,最好让意外条件终止程序并打印出调试的错误信息,这样可以方便开发者处理它们的代码。

通过 try / catch 语句,可以方便处理异常情况。

例如, 在下面的代码中,平方根函数 sqrt 会引发异常。 通过 try / catch 我们可以准确输出异常信息。

实例代码

julia> try
         sqrt("ten")
       catch e
         println("你需要输入一个数字")
       end
你需要输入一个数字

finally 子句

在进行状态改变或者使用类似文件的资源的编程时,经常需要在代码结束的时候进行必要的清理工作(比如关闭文件)。由于异常会使得部分代码块在正常结束之前退出,所以可能会让上述工作变得复杂。finally 关键字提供了一种方式,无论代码块是如何退出的,都能够让代码块在退出时运行某段代码。

这里是一个确保一个打开的文件被关闭的例子:

实例代码

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

当控制流离开 try 代码块(例如,遇到 return,或者正常结束),close(f) 就会被执行。如果 try 代码块由于异常退出,这个异常会继续传递。catch 代码块可以和 try 还有 finally 配合使用。这时 finally 代码块会在 catch 处理错误之后才运行。

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")

Task(协程)

Task(协程)提供了非局部的流程控制,这使得在暂时挂起的计算任务之间进行切换成为可能。在后面的异步编程章节我们会想象介绍。