Julia提供了多种控制流构造:
- 复合表达式:
begin
和(;)
。 - 有条件的评价:
if
-elseif
-else
和?:
(三元运算符)。 - 短路计算:
&&
,||
和链接的比较。 - 重复评估:循环:
while
和for
。 - 异常处理:
try
-catch
,error()
和throw()
。 - 任务(又名协程):
yieldto()
。
前五个控制流机制是高级编程语言的标准。Task
s并不是那么标准:它们提供了非本地控制流,从而可以在临时暂停的计算之间进行切换。这是一个强大的结构:使用任务在Julia中实现异常处理和协作式多任务处理。日常编程不需要直接使用任务,但是使用任务可以更轻松地解决某些问题。
复合表达式
有时,使用单个表达式按顺序计算多个子表达式,然后返回最后一个子表达式的值作为其值,会很方便。有两个Julia结构可完成此任务:begin
块和(;)
链。这两个复合表达式构造的值都是最后一个子表达式的值。这是一个begin
块的示例:
julia> z = begin
x = 1
y = 2
x y
end
3
由于这些都是很小的简单表达式,因此可以轻松地将它们放在一行中,这是使用(;)
链语法的方便之处:
julia> z = (x = 1; y = 2; x y)
3
对于Functions中引入的简洁的单行函数定义形式,此语法特别有用。尽管很典型,但并不需要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 < y
为true
,则对相应的块求值;否则为0。否则对条件表达式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
的elseif
和else
块是可选的,并且尽可能多的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.
该变量relation
在if
块内部声明,但在外部使用。但是,根据这种行为,请确保所有可能的代码路径都为变量定义了一个值。对以上函数的以下更改导致运行时错误
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和其他一些更严格的类型化语言类似-如果条件表达式的值不是true
or ,则错误false
。
julia> if 1
println("true")
end
ERROR: TypeError: non-boolean (Int64) used in boolean context
此错误表明条件的类型错误:Int64
而不是required Bool
。
所谓的“三元运算符”,?:
被密切相关的if
- elseif
- else
语法,但被用在需要单个表达值之间的条件的选择,相对于代码长块的条件执行。它是大多数语言中唯一采用三个操作数的运算符而得名的:
a ? b : c
表达a
,以前?
,是一个条件表达式,和三元操作计算表达式b
,前:
,如果条件a
是true
或表达c
,之后:
,如果是false
。
理解这种行为的最简单方法是看一个例子。在上一个示例中,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
为true,则整个三元运算符表达式的计算结果为字符串"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"
短路评估
短路评估与条件评估非常相似。在具有&&
和||
布尔运算符的大多数命令式编程语言中都发现了这种行为:在由这些运算符连接的一系列布尔表达式中,仅对最小数量的表达式进行求值,以确定确定整个链的最终布尔值。明确地,这意味着:
- 在表达式中
a && b
,b
仅当a
对求值时,才对子表达式求值true
。 - 在表达式中
a || b
,b
仅当a
对求值时,才对子表达式求值false
。
理由是,无论is 的值如何,a && b
必须为false
if a
is false
,b
同样,无论is 的值如何,a || b
if 的值都必须为true 。两者和都关联到右侧,但是具有比更高的优先级。尝试这种行为很容易:atrueb&&||&&||
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>)。
例如,可以这样定义递归析因例程:
代码语言:javascript复制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] fact(::Int64) at ./none:2
布尔操作没有短路评价可以在推出的按位布尔运算符来进行数学运算和基本功能:&
和|
。这些是普通函数,碰巧支持中缀运算符语法,但始终会评估其参数:
julia> f(1) & t(2)
1
2
false
julia> t(1) | t(2)
1
2
true
就像在使用条件表达式if
,elseif
或三元运算符,的操作数&&
或||
必须是布尔值(true
或false
)。在条件链中除了最后一个条目之外的任何地方都使用非布尔值是一个错误:
julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context
另一方面,条件链的末尾可以使用任何类型的表达式。它将根据前面的条件进行评估并返回:
代码语言:javascript复制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 <= 5
println(i)
i = 1
end
1
2
3
4
5
的while
循环计算的条件表达式(i <= 5
在这种情况下),并且只要它保持true
,保持还评估的主体while
环。如果条件表达式是首次到达循环false
时while
,则永远不会评估主体。
该for
循环使常见的重复评估习惯用法更易于编写。由于像上面的while
循环一样向上和向下计数非常普遍,因此可以使用for
循环更简洁地表示:
julia> for i = 1:5
println(i)
end
1
2
3
4
5
此处1:5
是一个Range
对象,代表数字1、2、3、4、5的序列。for
循环遍历这些值,依次将每个值分配给变量i
。先前的while
循环形式和for
循环形式之间的一个相当重要的区别是变量可见的范围。如果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
or ∈
代替=
,因为它使代码更清晰地阅读:
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
在伪造测试条件之前终止重复a 或在for
到达可迭代对象的末尾之前停止循环迭代。这可以通过break
关键字完成:
julia> i = 1;
julia> while true
println(i)
if i >= 5
break
end
i = 1
end
1
2
3
4
5
julia> for i = 1:1000
println(i)
if i >= 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)
break
在这样一个循环中的一条语句会退出整个循环嵌套,而不仅仅是内部循环。
异常处理
当发生意外情况时,函数可能无法将合理的值返回给其调用方。在这种情况下,对于特殊情况,最好终止程序,打印诊断错误消息,或者如果程序员提供了处理此类特殊情况的代码,则允许该代码采取适当的措施。
内置Exception
的
Exception
发生意外情况时将抛出s。Exception
下面列出的内置s中断了正常的控制流程。
Exception |
---|
ArgumentError |
BoundsError |
CompositeException |
DivideError |
DomainError |
EOFError |
ErrorException |
InexactError |
InitError |
InterruptException |
InvalidStateException |
KeyError |
LoadError |
OutOfMemoryError |
ReadOnlyMemoryError |
RemoteException |
MethodError |
OverflowError |
ParseError |
SystemError |
TypeError |
UndefRefError |
UndefVarError |
UnicodeError |
例如,该sqrt()
函数将DomainError
if应用于负实数值:
julia> sqrt(-1)
ERROR: DomainError:
sqrt will only return a complex result if called with a complex argument. Try sqrt(complex(x)).
Stacktrace:
[1] sqrt(::Int64) at ./math.jl:434
您可以通过以下方式定义自己的异常:
代码语言:javascript复制julia> struct MyCustomException <: Exception end
该throw()
功能
可以使用显式创建异常throw()
。例如,如果参数为负,则可以将仅为非负数定义的函数写入throw()
a DomainError
:
julia> f(x) = x>=0 ? exp(-x) : throw(DomainError())
f (generic function with 1 method)
julia> f(1)
0.36787944117144233
julia> f(-1)
ERROR: DomainError:
Stacktrace:
[1] f(::Int64) at ./none:1
请注意,DomainError
不带括号不是一个例外,而是一种例外。需要调用它以获得一个Exception
对象:
julia> typeof(DomainError()) <: Exception
true
julia> typeof(DomainError) <: Exception
false
此外,某些异常类型采用一个或多个用于错误报告的参数:
代码语言:javascript复制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] fussy_sqrt(::Int64) at ./none:1
如果fussy_sqrt
从另一个函数用负值调用了if ,而不是尝试继续执行该调用函数,而是立即返回,并在交互式会话中显示错误消息:
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] fussy_sqrt at ./none:1 [inlined]
[2] verbose_fussy_sqrt(::Int64) at ./none:3
警告和信息性消息
Julia还提供了其他功能,这些功能可以将消息写入标准错误I / O,但不抛出任何Exception
s,因此不中断执行:
julia> info("Hi"); 1 1
INFO: Hi
2
julia> warn("Hi"); 1 1
WARNING: Hi
2
julia> error("Hi"); 1 1
ERROR: Hi
Stacktrace:
[1] error(::String) at ./error.jl:21
该try/catch
声明
该try/catch
语句允许对Exception
进行测试。例如,可以编写自定义平方根函数来使用Exception
s 自动按需调用实数或复数平方根方法:
julia> f(x) = try
sqrt(x)
catch
sqrt(complex(x, 0))
end
f (generic function with 1 method)
julia> f(1)
1.0
julia> f(-1)
0.0 1.0im
重要的是要注意,在实际代码中计算此功能时,一个将与x
零进行比较,而不是捕获异常。这个例外比简单地比较和分支慢得多。
try/catch
语句还允许Exception
将_保存在变量中。在这个人为的示例中,以下示例计算x
if 的第二个元素的平方根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:
Stacktrace:
[1] sqrt_second(::Int64) at ./none:7
请注意,后面的符号catch
将始终被解释为异常的名称,因此try/catch
在单行上编写表达式时需要格外小心。如果发生错误,以下代码将无法返回值x
:
try bad() catch x end
而是使用分号或在以下位置插入换行符catch
:
try bad() catch; x end
try bad()
catch
x
end
该catch
条款并非严格必要;省略时,默认返回值为nothing
。
julia> try error() end # Returns nothing
在的力量try/catch
结构就在于能立即放松身心的深度嵌套计算在调用函数的栈一个更高的水平。在某些情况下没有发生错误,但是希望能够使堆栈退卷并将值传递到更高的级别。朱莉娅提供rethrow()
,backtrace()
以及catch_backtrace()
更先进的错误处理功能。
finally
条款
在执行状态更改或使用资源(如文件)的代码中,通常需要在代码完成后执行清理工作(例如关闭文件)。异常可能会使此任务复杂化,因为它们可能导致代码块在到达正常末端之前退出。该finally
关键字提供了一种方式来运行一些代码,当程序退出的给定块,不管它是如何退出。
例如,这是我们可以保证关闭打开的文件的方法:
代码语言:javascript复制f = open("file")
try
# operate on file f
finally
close(f)
end
当控制离开该try
块时(例如,由于return
或刚刚正常完成),close(f)
将执行。如果该try
块由于异常而退出,则该异常将继续传播。阿catch
块可结合try
和finally
为好。在这种情况下,该finally
块将在catch
处理完错误后运行。
任务(又名协程)
任务是一种控制流功能,它允许以灵活的方式暂停和恢复计算。有时会用其他名称来调用此功能,例如对称协程,轻量级线程,协作式多任务处理或单次连续。
当一个计算工作(实际上是执行一个特定功能)指定为a时Task
,可以通过切换到另一个来中断它Task
。Task
稍后可以恢复原件,这时它将在停下来的位置重新拾取。首先,这似乎类似于函数调用。但是,有两个主要区别。首先,切换任务不占用任何空间,因此可以在不消耗调用堆栈的情况下进行任意数量的任务切换。其次,与函数调用不同,任务之间的切换可以按任何顺序进行,在这种情况下,被调用函数必须在控制返回到调用函数之前完成执行。
这种控制流程可以使解决某些问题变得更加容易。在某些问题中,各种所需的工作与功能调用之间并不是很自然的联系。在需要完成的工作中,没有明显的“呼叫者”或“被呼叫者”。一个例子是生产者-消费者问题,其中一个复杂的过程正在产生值,而另一个复杂的过程正在消耗它们。消费者不能简单地调用生产者函数来获取值,因为生产者可能要生成更多的值,因此可能还没有准备好返回。有了任务,生产者和消费者都可以根据需要运行,并根据需要来回传递值。
Julia提供了Channel
解决此问题的机制。A Channel
是可等待的先进先出队列,可以有多个任务对其进行读写操作。
让我们定义一个生产者任务,该任务通过put!
调用产生值。要消耗值,我们需要安排生产者在新任务中运行。Channel
接受1-arg函数作为参数的特殊构造函数可用于运行绑定到通道的任务。然后,我们可以take!()
从channel对象重复进行赋值:
julia> function producer(c::Channel)
put!(c, "start")
for n=1:4
put!(c, 2n)
end
put!(c, "stop")
end;
julia> chnl = Channel(producer);
julia> take!(chnl)
"start"
julia> take!(chnl)
2
julia> take!(chnl)
4
julia> take!(chnl)
6
julia> take!(chnl)
8
julia> take!(chnl)
"stop"
考虑这种行为的一种方法producer
是能够多次返回。在对的调用之间put!()
,生产者的执行被挂起,并且消费者拥有控制权。
返回的Channel
值可用作for
循环中的可迭代对象,在这种情况下,循环变量采用所有产生的值。通道关闭时,循环终止。
julia> for x in Channel(producer)
println(x)
end
start
2
4
6
8
stop
注意,我们不必显式关闭生产者中的通道。这是因为将a绑定Channel
到a 的行为Task()
将通道的开放生存期与绑定任务的生存期相关联。任务终止时,通道对象自动关闭。可以将多个通道绑定到一个任务,反之亦然。
虽然Task()
构造函数期望一个0参数的函数,但是Channel()
创建通道绑定任务的方法期望一个接受单个type参数的函数Channel
。常见的模式是对生产者进行参数化,在这种情况下,需要部分函数应用程序来创建0或1参数匿名函数。
对于Task()
对象,这可以直接完成,也可以通过使用便捷宏来完成:
function mytask(myarg)
...
end
taskHdl = Task(() -> mytask(7))
# or, equivalently
taskHdl = @task mytask(7)
为了编排更先进的工作分布模式,bind()
并且schedule()
可以配合使用Task()
,并Channel()
构造明确链接一组信道的一组生产者/消费者的任务。
请注意,当前Julia任务尚未计划在单独的CPU内核上运行。真正的内核线程将在“ 并行计算 ”主题下进行讨论。
核心任务操作
让我们探索底层结构yieldto()
以了解任务切换的工作原理。yieldto(task,value)
挂起当前任务,切换到指定的任务,task
并使该任务的最后一次yieldto()
调用返回指定的任务value
。注意,这yieldto()
是使用任务样式控制流所需的唯一操作;而不是调用并返回,我们始终只是切换到其他任务。这就是为什么此功能也称为“对称协程”的原因;每个任务都使用相同的机制来回切换。
yieldto()
它功能强大,但是大多数任务使用并不直接调用它。考虑为什么会这样。如果您退出当前任务,则可能会在某个时候切换回该任务,但是知道何时切换回去,以及知道哪个任务负责切换,可能需要大量的协调。例如,put!()
和take!()
是阻塞操作,当它们在通道的上下文中使用时,它们保持状态以记住使用者是谁。put!()
比起底层工具,更易于使用的是无需手动跟踪消耗任务的方法yieldto()
。
除了yieldto()
,还需要一些其他基本功能才能有效地使用任务。
current_task()
获取对当前正在运行的任务的引用。istaskdone()
查询任务是否已退出。istaskstarted()
查询任务是否已经运行。task_local_storage()
操作特定于当前任务的键值存储。
任务和事件
大多数任务切换是由于等待事件(例如I / O请求)而发生的,并且由标准库中包含的调度程序执行。调度程序维护可运行任务的队列,并执行事件循环,该循环根据外部事件(例如消息到达)重新启动任务。
等待事件的基本功能是wait()
。几个对象的实现wait()
; 例如,给定一个Process
对象,wait()
将等待其退出。wait()
通常是隐式的;例如,wait()
在呼叫内可能会发生,read()
以等待数据可用。
在所有这些情况下,wait()
最终都在Condition
对象上运行,该对象负责排队和重新启动任务。当任务调用wait()
时Condition
,该任务被标记为不可运行,被添加到条件队列中,并切换到调度程序。然后,调度程序将选择另一个要运行的任务,或者阻止等待外部事件。如果一切顺利,最终事件处理程序将notify()
对该条件进行调用,这将导致等待该条件的任务再次变为可运行状态。
Task
最初调度程序不知道通过调用显式创建的任务。这使您可以根据需要手动管理任务yieldto()
。但是,当此类任务等待事件发生时,它仍会如您所期望的那样在事件发生时自动重新启动。也可以使调度程序尽可能地运行任务,而不必等待任何事件。这可以通过调用schedule()
或使用@schedule
或@async
宏来完成(有关更多详细信息,请参见并行计算)。
任务状态
任务有一个state
描述其执行状态的字段。A 是以下符号之一:Task
state
当前正在运行,或可以切换到