# 第 11 章 流程控制

12 篇文章 1 订阅

{{TOC}}

## 第 11 章 流程控制

### 11.1 最简单的代码块

julia> a = 5 * 8; b = a^2
1600

julia>


julia> c1 = (a = 5 * 8; b = a^2)
1600

julia> c1
1600

julia>


julia> begin a = 5 * 8; b = a^2 end
1600

julia> c2 = begin a = 5 * 8; b = a^2 end
1600

julia> c2
1600

julia>


julia> c2 = begin
a = 5 * 8
b = a^2
end
1600

julia>



julia> c2 = (a = 5 * 8;
b = a^2)
1600

julia>


### 11.2 if 语句

# 假设在这之前已经定义了变量name，并为它赋予了某个值。
if name == ""
name = "handsome"
end


if语句总是以if关键字开头，并以end关键字结尾。与if关键字处于同一行的必须是一个结果类型为Bool的表达式。我们通常称这样的表达式为条件表达式。在上面的这个例子中，name == ""显然就是条件表达式。只有当它的结果值为true时，if语句中的子语句，即name = "handsome"，才会被执行。否则，其中的子语句就会被跳过而不执行。这里的子语句的数量可多可少，也可以是零个。因此，我们也可以称之为子语句组，或if子语句组。

if name == ""
name = "handsome"
else
name = "dear " * name
end


else关键字和end关键字之间的就是当条件不满足时会执行的子语句。与if子语句组一样，这里的子语句的数量也可以是任意的。我们可称之为else子语句组。

if name == ""
name = "handsome"
elseif name == "Robert"
name = "my master"
else
name = "dear " * name
end


julia> name = "Robert"
"Robert"

julia> if name == ""
title = "handsome"
elseif name == "Robert"
title = "my master"
else
title = "dear " * name
end
"my master"

julia> title
"my master"

julia>


julia> result = if name == ""
title = "handsome"
elseif name == "Robert"
title = "my master"
else
title = "dear " * name
end
"my master"

julia> result
"my master"

julia>


# 假设在这之前已经定义了变量action和weather，并为它们赋予了值。
# 假设在这之前已经定义了变量prompt。
if action == "walk" && weather == "rain"
prompt = "Don't forget to bring an umbrella."
end


julia> action = "walk"; weather = "rain"; prompt = "";

julia> is_walk(action) = (println("Check action (1)"); action == "walk")
is_walk (generic function with 1 method)

julia> is_rain(weather) = (println("Check weather (1)"); weather == "rain")
is_rain (generic function with 1 method)

julia> if is_walk(action) && is_rain(weather)
prompt = "Don't forget to bring an umbrella."
end
Check action (1)
Check weather (1)
"Don't forget to bring an umbrella."

julia> action = "sleep";

julia> if is_walk(action) && is_rain(weather)
prompt = "Don't forget to bring an umbrella."
end
Check action (1)

julia>


julia> is_sleep(action) = (println("Check action (2)"); action == "sleep")
is_sleep (generic function with 1 method)

julia> is_sunny(action) = (println("Check weather (2)"); action == "sunny")
is_sunny (generic function with 1 method)

julia> if is_sleep(action) || is_sunny(weather)
prompt = "The idea looks good."
end
Check action (2)
"The idea looks good."

julia>


julia> action = "drive"; weather = "rain"; road_condition = "bad"; prompt = ""
""

julia> if weather != "sunny" && (road_condition != "good" && (action == "ride" || action == "drive"))
prompt = "Please pay attention to traffic safety."
end
"Please pay attention to traffic safety."

julia>


### 11.3 for 语句

julia> for e in [[1,2] [3,4] [5,6]]
print(e, " ")
end
1 2 3 4 5 6
julia>


julia> for (k, v) in Dict([(1,"a"), (2,"b"), (3,"c")])
print("$(k)=>$(v) ")
end
2=>b 3=>c 1=>a
julia>


julia> for e in "Julia 编程"
print(e)
end
Julia 编程
julia> for e = 1:10
print("$(e) ") end 1 2 3 4 5 6 7 8 9 10 julia>  关于for语句可以迭代字符串就不用我多说了吧？它会依次地迭代出字符串中的每一个字符。 在这里的第二段代码中，我们迭代的是一个类型为UnitRange{Int64}的可迭代对象。这类对象用于表达一种数值序列。这种序列中的任意两个相邻元素值的差总会是1 我们在以前其实已经多次使用过这种序列。只不过，我还没有正式介绍过它。 如上所示，我使用值字面量的方式表示了这类对象。更具体地说，1:10表示的是一个从1开始、到10结束且相邻值间隔为1的数值序列。因此，我们可以把在英文冒号左侧的数值称为开始值，并把在它右侧的数值称为结束值。 此外，与UnitRange相似的类型还有StepRangeLinRange。简要地说，前者的值用于表示等差序列，而后者的值则用于表示等分序列。例如： julia> typeof(10:10:30) StepRange{Int64,Int64} julia> Array(10:10:30) 3-element Array{Int64,1}: 10 20 30 julia> LinRange(1.2, 2.6, 9) 9-element LinRange{Float64}: 1.2,1.375,1.55,1.725,1.9,2.075,2.25,2.425,2.6 julia>  让我们再把焦点放回到前一个例子中的第二段代码上。我们在之前也说过，符号=在这里的含义并不是单纯的“赋值”，而是“每一次迭代均赋值”。它与关键字in的含义是相同。在通常情况下，后者更加常用。不过，在嵌套着迭代多个对象的时候，我们常常使用的是=而不是in。示例如下： julia> for x=1:2, y=10:10:30 println((x, y)) end (1, 10) (1, 20) (1, 30) (2, 10) (2, 20) (2, 30) julia>  请注意看在关键字for右边的那些代码。由这些代码可知，这是一个两层的嵌套循环。其中，左侧的x=1:2代表着外层的循环，而右侧的y=10:10:30则代表内层的循环。它们之间由英文逗号“,”分隔。因此，x1:2就分别是外层循环中的迭代变量和被迭代对象，而y10:10:30则分别是内层循环中的迭代变量和被迭代对象。 在进行迭代的时候，for语句会先迭代一次外层的对象，并把迭代出的值赋给外层的迭代变量。然后，它会暂停对外层对象的迭代，转而去迭代内层的对象，并把每一次迭代出的值都赋给内层的迭代变量。直到对内层对象从头到尾地迭代一遍（或者说遍历一次）之后，它才会再去迭代一次外层的对象。就像这样，迭代一次外层对象、遍历一次内层对象、再迭代一次外层对象、再遍历一次内层对象，交替往复。直至完整地遍历一次最外层的对象，这个嵌套的循环才算执行完毕。 对于这样的嵌套循环，我们说的“一次迭代”通常指的是在最内层对象上的某一次迭代。但是，不要忘了，这样的“一次迭代”的背后还体现着基于那些外层对象的某个迭代状态。比如，在上例中，当for语句从内层的被迭代对象那里迭代出20时，外层对象的迭代状态有两个可能。也就是说，与之对应的外层迭代变量的值可能是1，也可能是2。至于实际上是哪一个，就要看for语句正在对内层对象进行第几次遍历了。 反过来讲，由于for语句每迭代一次外层对象之后都会先遍历一次内层对象，所以当x的值为1时，y的值就可能是102030中的某一个。当x的值为2时也是如此。这其实就是在对多个被迭代对象中的元素值进行穷举式的组合，或者说在求多个被迭代对象的笛卡尔积。 在这其中，还有一个需要我们特别注意的规则。对于这样拥有多个被迭代对象的单条for语句，无论它有多少层嵌套的循环，每当“一次迭代”开始之际，Julia 都会为所有层次上的迭代变量进行赋值。即使这些迭代变量在此次将要被赋予的值与前一次被赋予的值一摸一样，也会是如此。请看下面的示例： julia> for x=1:2, y=10:10:30 println((x, y)) x = 2 end (1, 10) (1, 20) (1, 30) (2, 10) (2, 20) (2, 30) julia>  请注意，在这条for语句里的子语句组中有这样一行代码，即：x = 2。从表面上看，它会在每一次迭代快要结束的时候修改外层迭代变量x的值。但事实上，这样做是不会奏效的。其原因就是，Julia 在这里总是会遵循我们刚刚阐述的那个规则。更具体地说，它会在这条for语句的每一次迭代刚刚开始的时候，依据当前的迭代状态分别对xy进行赋值（或重新赋值）。显然，这会使代码x = 2所做的更改失效，尤其是在第二次迭代和第三次迭代执行的时候。 然而，当我们使用多条for语句表达一个嵌套循环的时候，Julia 就不会这样做了。也就是说，在这种情况下，它不会在每一次内层迭代开始的时候再对外层的迭代变量进行赋值。相应的示例如下： julia> for x in 1:2 for y in 10:10:30 println((x, y)) x = 2 end end (1, 10) (2, 20) (2, 30) (2, 10) (2, 20) (2, 30) julia>  我们可以看到，在这个执行结果中，第二个元组和第三个元组里的第一个元素值都变成了2，而不是原先的1。这就是代码x = 2在这里的内层for语句中所起到的作用。 也许我这样说会更便于你记忆：当嵌套的循环被合并在一起时，其中的迭代变量的值就必定不会受到任何干扰，它们只取决于对应的被迭代对象和当时的迭代状态。而当嵌套的循环是由多条for语句松散地表达时，上述干扰就很容易发生。 被合并在一起的嵌套循环的另一大优势是，它可以让代码更加简洁。但它的劣势也比较明显，那就是for语句中只能有一组子语句。如果我们想在多层的迭代之间做点什么的话，这样的for语句就无能为力了。在这种情况下，我们还是需要使用松散的多条for语句来表达。例如： julia> for x in 1:5 if x % 2 == 0 continue end for y in 10:10:30 print("($x,$y) ") end end (1,10) (1,20) (1,30) (3,10) (3,20) (3,30) (5,10) (5,20) (5,30) julia>  在此示例中，外层循环用于对1:5进行迭代，而内层循环被用来迭代10:10:30。可以看到，这两层循环是各由一条for语句来表示的。它们可以包含各自的子语句组。也正因为如此，我可以像上面那样去控制什么时候不遍历内层的被迭代对象。 我使用一条if语句制定了一个小规则：当x可以被2整除时，不要遍历内层的对象，并直接对外层的对象进行下一次迭代。在这里，continue起到了很重要的作用。 关键字continue首先会让 Julia 放弃执行当次迭代中剩余的那些子语句。更明确地说，这些子语句处于直接包含这个continue的那条for语句之中，并且位于这个continue的下方。在此示例中，处在这个位置上的子语句只有负责内层循环的那条for语句。 紧接着，continue还会让直接包含它的那条for语句继续对它携带的被迭代对象进行下一次迭代。也就是说，continue并不会让当前的循环结束，只是让它跳过一些子语句的执行而已。 正是由于那条if语句和continue，这个例子的结果中才没有包含整数24。你可以自己模拟一下这个例子的执行流程，并依次写下它应该打印出的内容，然后再回过头来与实际的执行结果对照一下，看看你是否已经完全理解了这些代码的含义。 现在，让我们再看一个很相似的例子： julia> for x in 1:5 if x % 3 == 0 break end for y in 10:10:30 print("($x,$y) ") end end (1,10) (1,20) (1,30) (2,10) (2,20) (2,30) julia>  与前一段代码相比，这段代码只有if语句有所不同。它体现了不同的规则，即：当x可以被3整除时，结束当前循环的执行。在这里起到关键作用的是break 关键字break做起事来非常的干脆，它不会去管当前的循环进行到哪一步了，也不会理会当前迭代的执行状态如何，而是直接让 Julia 中断对当前的for语句的执行。所以，我们在此示例的结果中才看不到整数345 我们在编写用于循环的代码时经常会碰到需要continuebreak的处理流程。所以，你需要记住它们的作用和异同。另外还要注意，它们都只能对当前的那个循环起作用。比如说，当我们在内层循环使用它们时，只有内层的循环才会受到影响，而绝不会波及到外层的循环。相应的示例如下： julia> for x in 1:5 for y in 10:10:30 if x % 3 == 0 break end print("($x,$y) ") end end (1,10) (1,20) (1,30) (2,10) (2,20) (2,30) (4,10) (4,20) (4,30) (5,10) (5,20) (5,30) julia>  在阅读了这段代码之后，你一定会发现我把那条if语句搬到了内层的for语句之中。这个改动看起来很小，但是它对流程的改变却不小。 当外层的迭代变量x被赋予3的时候，内层的循环在第一次迭代时就会发现if语句的条件满足了。这时，break语句就有了被执行的机会（breakcontinue都可以被看做是仅包含了一个关键字的语句）。它的执行会让当前的循环（也就是内层循环）的执行中止。 然而，外层的循环却不会受到任何的影响，它只会因当次迭代中没有更多的语句可执行而继续进行下一次迭代。在外层循环的下一次迭代执行时，x的值就不再是3了。所以那条break语句就再也没有被执行的机会了。因此，在这个示例的结果中，只有整数3不会出现。 请注意，对于合并在一起的嵌套循环，或者说拥有多个被迭代对象的单条for语句，breakcontinue都会直接对整个嵌套循环起作用，而不区分它正处于循环的哪一层。这是理所当然的，不是吗？因为在这样的for语句中确实也无法识别出循环的层次。 下面，我们再来讨论另外一个很重要的问题——for语句的作用域。没错，每一条for语句都会自成一个作用域。关于此，最直观的表现就是，for语句所声明的迭代变量不能被该语句之外的代码引用到。请看下面的示例： julia> for x in 1:5 print("$x ")
end
1 2 3 4 5
julia> x
ERROR: UndefVarError: x not defined

julia>


julia> for x in 1:5
for y in 10:10:30
print("($x,$y) ")
end
if y % 10 != 0
break
end
end
(1,10) (1,20) (1,30) ERROR: UndefVarError: y not defined
# 省略了一些回显的内容。

julia>


julia> for x in 1:3
for x in 10:10:30
print("($x) ") end end (10) (20) (30) (10) (20) (30) (10) (20) (30) julia>  在这里，内层和外层的迭代变量都叫x。当内层for语句中的代码引用x时，它拿到的只是内层的迭代变量x。也就是说，在寻常的情况下，这些内层作用域中的代码是无法引用到外层的重名变量的。 当然了，还存在不寻常的情况。这涉及到了关键字outer。示例如下： julia> for x in 1:3 for outer x in 10:10:30 print("($x) ")
end
print("[$x] ") end (10) (20) (30) [30] (10) (20) (30) [30] (10) (20) (30) [30] julia>  请注意看，我把关键字outer添加在了内层for语句的第一行里，更具体的位置是迭代变量x的左侧。如此一来，这个x代表的就不再是一个在内层作用域中新定义的局部变量了，而是一个指代了那个外层的迭代变量的标识符。 从这个示例打印出的内容我们也可以看到，每当内层的for语句执行结束之后，x的值都会是30。这是因为内层for语句在每一次迭代开始时都在为外层的迭代变量x赋值。它的最后一次迭代总会把30赋给外层的迭代变量x 至此，outer关键字在这里所起的作用也就很明朗了，即：让for语句复用一个在外层作用域中定义的局部变量，并将其作为自己的迭代变量。 现在，让我们稍稍总结一下。我们可以使用for语句实现循环，还可以用它依次地取出任何可迭代对象中的元素值。一条for语句中可以有若干个被迭代对象和相应的迭代变量。当一条for语句中同时存在多个被迭代对象时，我们可以说它实现了一个嵌套的循环。当然，我们也可以把这样的嵌套循环拆成多条for语句。另外，我们还可以在for语句中添加continue语句和break语句，以达到精细控制的目的。 这里的另一个重点是，每一条for语句都会自成一个作用域。在一般情况下，for语句中的迭代变量都是仅属于该语句的局部变量，它们在外界是无法被引用的。不过，outer关键字可以对这种情况有所改变。正如前文所述。 ### 11.4 while 语句 for语句的用途相似，while语句也可以被用来实现循环。不过，在代码的编写方面，这两者却截然不同。while语句总是需要携带一个条件表达式。这个条件表达式非常关键，它可以控制当前的循环在什么时候开始，以及在什么时候结束。下面是一个简单的例子： julia> num = 0; julia> while num <= 9 global num += 1 print("$num ")
end
1 2 3 4 5 6 7 8 9 10
julia>


julia> num = 0
0

julia> while num <= 9
num = 10
print("$num ") end  因为，如此一来就会出现这种情况：虽然我们在while语句的子语句组中定义了局部变量num，但是在其条件表达式中引用的依然是全局变量num。显然，我们为局部变量num赋值并不会影响到全局变量num。因此，这个循环会一直执行下去。倘若我们不采取任何的措施（如杀掉进程），那么它就永远不会结束。这就是一个简单的死循环！ 这种行为是由上面这段代码的编写方式决定的。而且，我们只有在定义一个变量或者给一个变量赋值的时候才能添加像global这样的关键字。所以这个问题没有其他更好的解决方案。我在前面使用的语句global num += 1其实就是最优的。请记住，若想在局部的作用域中为全局的变量赋值，那么就一定要在该变量的左边添加global 既然我们讲到了global这个关键字，那我就再说一下与之相对应的关键字local。与global正好相反，local会让 Julia 认为处于该关键字右边的标识符指代的是一个在当前作用域之下的局部变量。 local的适用场景没有global广泛。不过，对于嵌套在一起的局部作用域而言，它还是很有用的。请看下面的示例： julia> num = 0; julia> while num < (10-1) global num += 1 sign = num while num % 2 != 0 sign = num + 1 global num += 1 end print("$sign")
# print("(num=$num)") print(" ") end 2 4 6 8 10 julia>  这段代码包含了一个两层的循环。这两层循环都是用while语句实现的。同时，它们也代表了两个嵌套在一起的局部作用域。 可以看到，循环中引用的num仍然是一个全局变量。外层循环的条件是num小于9，而内层循环的条件是num不能被2整除。而且，无论是哪一层循环，都会在当前的条件满足的情况下对num进行加1的操作。 此外，我在外层的循环里还定义了一个局部的变量sign。并且，我在内层的循环中还引用了这个局部变量，并对它进行了重新赋值。这个局部变量代表了我们在每一次外层迭代的最后将要打印的内容。你肯定也发现了，每当外层的迭代即将完成的时候，变量sign的值都会与num的值相等。其实，我在这里添加这个局部变量只是为了体现local的用法和作用。 上面的这个双层循环的作用是打印出10以内的所有正偶数。但是，如果我们在内层循环中的代码sign = num + 1的左边添加一个关键字local，那么情况就会明显不同。代码如下： julia> num = 0; julia> while num < (10-1) global num += 1 sign = num while num % 2 != 0 local sign = num + 1 global num += 1 end print("$sign")
# print("(num=$num)") print(" ") end 1 3 5 7 9 julia>  可以看到，在我添加了local之后，这段代码打印出了10以内的所有正奇数。 让我们来一起分析一下原因。我刚刚说过，local关键字的作用是，让 Julia 认为处于该关键字右边的标识符指代的是一个在当前作用域之下的局部变量。因此，在这个local右边的sign就会被视为一个在内层的while语句中定义的局部变量。以下简称这个sign为内层的sign。显然，这个内层的sign与在外层的while语句中定义的那个sign（以下简称外层的sign）就不再是同一个变量了。又由于我对内层sign的赋值肯定不会影响到外层sign的值，所以外层迭代打印出的内容才都会是奇数。这就是local关键字对这段代码的实际影响。 关键字local可以把一个原本引用自外层局部作用域的变量变成一个仅属于当前作用域的新变量。这是对重名变量的另一种解法。但它可以解决的只是在多个嵌套在一起的局部作用域当中出现重名变量的问题。别忘了，在默认的情况下，Julia 会让同名的局部变量遮蔽掉那个对应的全局变量。所以，在这里并不会涉及到（也用不着涉及到）全局作用域。 反观关键字global，它面向的则是在某个局部作用域和全局作用域当中出现重名变量的问题。它的添加会改变 Julia 的默认行为，让当前作用域下的标识符不再代表一个新的变量，而是代表同名的全局变量。 由于while语句的编写特点，global往往会在这种语句中经常出现。然而，localwhile语句中出现的次数就明显少了许多。原因是，我们通常很少会编写拥有很多层的嵌套循环。即使编写了这样的代码，我们一般也不会写出重名的局部变量。因为这么做会大大降低代码的可读性，同时还会加重我们自己的心智负担。 最后，顺便说一下，我们在while语句中也可以使用break语句和continue语句。而且，它们在这里的用法和作用与在for语句中的没有什么两样。但特别的是，当我们仅仅把true作为while语句的条件表达式时，break语句的加入就显得尤为重要了。例如： julia> num = 0; julia> while true global num += 1 print("$num ")
if num >= 10
break
end
end
1 2 3 4 5 6 7 8 9 10
julia>


### 11.5 let 语句

Julia 的let语句本身既不包含条件也没有循环，但它的功能却是独树一帜的。

let语句能够自成一个作用域。通常，我们会让let语句在开始处携带赋值语句，并以此定义相应的局部变量。另外，与其他的代码块一样，let语句也可以包含子语句组。下面是一个简单的例子：

julia> x = "Python";

julia> let x = "Julia", y = "Golang"
println("$x,$y")
end
Julia, Golang

julia>


julia> let x, y = "Julia", "Golang"
println("$x,$y")
end
ERROR: syntax: invalid let syntax
# 省略了一些回显的内容。

julia>


julia> let x = x, y = "Golang"
println("$x,$y")
end
Python, Golang

julia>


julia> let
y = "Golang"
println("$x,$y")
end
Python, Golang

julia>


let语句本身携带的赋值语句一定会在当前的作用域下创建新的局部变量，即使这个局部变量与外界的变量重名也是如此。或者说，这里的赋值语句总是会执行“定义并赋值”的操作。正因为如此，这里才存在着一种特殊的赋值方式。就像我在前面讲过的那样。

### 11.6 错误的报告与处理

#### 11.6.1 程序错误的载体

Julia 中所有的异常类型都直接或间接地继承自Exception类型。通过执行调用表达式subtypes(Exception)，你会发现仅仅是Exception的直接子类型就多达近 60 个。其中有不少我们之前见过的异常类型，比如：代表函数参数错误的ArgumentError、代表索引越界错误的BoundsError、代表类型转换错误的InexactError，以及在字典中不存在指定键时报出的KeyError、在衍生方法不存在时报出的MethodError、在变量未定义时报出的UndefVarError，等等。

julia> ex = ErrorException("Something wrong!")
ErrorException("Something wrong!")

julia> ex.msg
"Something wrong!"

julia>


#### 11.6.2 异常的抛出

julia> # 用于打印某人的体重的函数。
function print_weight(kg::Int)
if kg <= 0
throw(DomainError(kg, "The argument is too small!"))
elseif kg > 500
throw(DomainError(kg, "The argument is too big!"))
end
println("$(kg) kg") end print_weight (generic function with 1 method) julia> print_weight(100) 100 kg julia> print_weight(-1) ERROR: DomainError with -1: The argument is too small! Stacktrace: [1] print_weight(::Int64) at ./REPL[1]:4 [2] top-level scope at REPL[3]:1 julia>  我先定义了一个叫做print_weight的函数。这个函数的功能非常简单，只是打印一下某人的体重而已。它有一个参数，名称为kg，类型为Int 针对这个函数的功能，我已经通过参数的类型对输入进行了一定的约束。但这显然还不够。所以我又添加了一条防卫语句，也就是处于该函数的函数体最上面的那条if语句。其含义是，参数kg的值既不能小于或等于0也不能大于500，否则就主动抛出一个异常。注意，这里的条件有两个。我分别为这两个条件创建了不同的异常值。 通常，当由于参数的值超出了有效的值域而需要抛出异常的时候，我们通常会使用DomainError类型的值来表述异常。有两个构造函数可以产生此类型的异常值。其中的一个构造函数只有一个名为var的参数，而另一个构造函数的参数除了var还有msg。顾名思义，参数var应该被赋予的就是那个超出了值域的参数值，而参数msg则应该被赋予关于此异常的描述信息。 我在上面的例子中使用的是拥有两个参数的构造函数DomainError。因为单单给予print_weight函数所接受的参数值还不足以说明问题。紧接着，我把刚刚创建的异常值传给了throw函数。到了这里，异常就即将被抛出了。 严格来说，throw并不是一个通常意义上的函数。它的不普通之处在于，它被调用之后会立即中断当前程序正在执行的正常流程。你应该也看到了，在我向print_weight函数传入了超出值域的参数值之后，该函数并没有执行完它的正常流程（或者说没有打印出任何内容），而是直接使 REPL 环境显示出了一段异常提示信息。 在解释throw函数都做了什么之前，我们先来认识一个概念——调用栈。调用栈是编程语言用来实时记录和控制应用程序的执行过程的一种辅助工具。它基于的是一种被称为栈的数据结构。你可能已经知道，栈其实也是一种容器，而且它是先进后出的。更具体地讲，调用其他代码的代码（以下简称调用代码）会先被放入调用栈，然后被调用的代码才会入栈。另一方面，在通常情况下，调用代码要等到被调用代码执行完毕之后才会继续执行。所以被调用代码会先出栈，然后才是调用代码。也就是说，出栈的顺序与入栈的顺序是完全相反的。此外，调用栈通常无法描绘出应用程序运行过程的全貌。因为只有正在执行的代码调用才可能会出现在调用栈中。 现在，让我们来一起看一下上例中的异常提示信息。通过查看其中的前两行内容我们可以知道，被引发的异常的类型是DomainError，而引发的原因是参数值-1太小了。它比参数kg的有效值域中的最小值还要小。显然，这两行内容恰恰源自我在print_weight函数中传给throw函数的那个异常值。 接着往下看。紧挨在Stacktrace:下面的、左边以序号开头的那几行内容就是 Julia 向我们展示的调用栈信息。注意，这里的信息是以出栈的顺序展示的。也就是说，与序号1对应的是最后被调用的代码。 在包含了[1]的这行内容当中，我们需要关注两个地方。第一个地方是左边的print_weight(::Int64)。它是print_weight函数的签名，由函数名称、参数列表以及可选的结果声明组成。更宽泛地说，它代表的是被调用代码的标识。第二个地方是右边的./REPL[1]:4。它代表着被调用代码中抛出异常的语句的具体位置。在这里它显示了，那条语句处于当前的 REPL 环境所解析的第 1 段代码中的第 4 行，即： throw(DomainError(kg, "The argument is too small!"))  相应的，包含了[2]的内容告诉我们，在异常抛出时，调用print_weight函数的那条语句处于当前 REPL 环境所解析的第 3 段代码中的第 1 行。这条语句正是print_weight(-1)。而这行内容中的top-level scope是在告诉我们，这条语句是顶层作用域中的代码。所谓的顶层作用域指的就是Main模块所代表的作用域。已知，我们在 REPL 环境中直接写入的代码就都属于Main模块。如果这些代码未被包含在更小的作用域里，那么我们就可以说它们是顶层作用域中的代码。上例中的print_weight(100)print_weight(-1)就都是这样的代码，但是print_weight函数中的代码却不是。 在查看了这些调用栈信息之后你可能会意识到，throw函数不但会中断当前代码的执行，还会沿着调用栈的反方向（即与入栈顺序相反的方向）传播异常，直到碰到能够处理此异常的程序为止。对于上面的例子，REPL 环境本身会处理掉我们写入的代码所抛出的异常。也正因为如此，REPL 环境才能依然良好地运行着，并不会受到如此异常的影响。而且，我们可以看到，上例中的调用栈信息只有 2 行。这正说明此异常并没有被传播到 Julia 语言本身的程序当中。 如果我们把上述代码写入到一个源码文件中，并使用julia命令来运行，那么就可以在异常抛出后获得更多的信息。实际上，我已经把几乎一模一样的代码写进了Programs项目的src/ch11/exception/throw/main.jl文件中。现在，我们在命令行中运行一下它，结果如下： $ julia main.jl
100 kg
The argument is too big!
Stacktrace:
[1] print_weight(::Int64) at /Users/haolin/Projects/Programs.jl/src/ch11/exception/throw/main.jl:11
[2] top-level scope at /Users/haolin/Projects/Programs.jl/src/ch11/exception/throw/main.jl:17
[3] include at ./boot.jl:328 [inlined]
[5] include(::Module, ::String) at ./Base.jl:31
[6] exec_options(::Base.JLOptions) at ./client.jl:287
[7] _start() at ./client.jl:460
in expression starting at /Users/haolin/Projects/Programs.jl/src/ch11/exception/throw/main.jl:17


throw(DomainError(kg, "The argument is too big!"))


print_weight(501)


#### 11.6.3 异常的处理

julia> # 用于获取 BMI（身体质量指数）函数。
function get_bmi(weight::Int, height::Float64)::Float64
if weight <= 0 || weight > 500
throw(DomainError(weight, "Invalid weight! (range: (0, 500])"))
elseif height <= 0.0 || height > 3.0
throw(DomainError(height, "Invalid height! (range: (0.0, 3.0])"))
end
return weight / height^2
end
get_bmi (generic function with 1 method)

julia> try
bmi = get_bmi(0, 1.78)
catch e
println("WARNING: captured an exception: $e") end WARNING: captured an exception: DomainError(0, "Invalid weight! (range: (0, 500])") julia>  函数get_bmi可以计算并返回一个人的身体质量指数（BMI）。参数weight代表体重，单位是公斤。参数height代表身高，单位是米。 我们可以看到，这里的try子句和catch子句的拼接方式与if语句中的if分支和else分支的拼接方式是一样的。它们都是紧挨在一起的，并且只在最后有一个end。不过，这两种语句的处理逻辑就大相径庭了。 在这条try-catch语句被执行之后，REPL 环境并没有显示任何的异常提示信息。这说明这段代码的执行成功完成了。并且，我们可以看到，对这段代码的执行使 REPL 环境打印出了一行表示了警告信息的内容。很显然，相应的打印语句正是在catch子句当中的那一条语句。在细看这行警告信息之后，我们也可以确定，catch子句捕获到的异常值恰恰代表了当我们调用get_bmi函数并为它的weight参数传入0时应该抛出的那种异常。 这里有一点需要我们特别注意。try子句中的正常流程依然会因异常的抛出而被中断。只不过，try子句在异常即将被传播出去的时候将其拦下并传递给了catch子句。这使得try-catch语句重新获得了流程的控制权。可即使是这样，程序也不会再从抛出异常的那条语句那里继续执行下去了。我再向上例的代码中添加两行打印语句，你肯定就可以看出端倪了： julia> try println("Invoke get_bmi (before)") bmi = get_bmi(0, 1.78) println("Invoke get_bmi (after)") catch e println("WARNING: captured an exception:$e")
end
Invoke get_bmi (before)
WARNING: captured an exception: DomainError(0, "Invalid weight! (range: (0, 500])")

julia>


julia> # 用于获取 BMI（身体质量指数）函数。
function get_bmi(weight::Int, height::Float64)::Float64
if weight <= 0 || weight > 500
throw(DomainError(weight, "Invalid weight! (range: (0, 500])"))
elseif height <= 0.0 || height > 3.0
throw(DomainError(height, "Invalid height! (range: (0.0, 3.0])"))
end
return weight / height^2
end
get_bmi (generic function with 1 method)

julia> try
println("Invoke get_bmi (before)")
bmi = get_bmi(0, 1.78)
println("Invoke get_bmi (after)")
catch e
println("WARNING: captured an exception: $e") println("Invoke rethrow (before)") rethrow(e) println("Invoke rethrow (after)") end Invoke get_bmi (before) WARNING: captured an exception: DomainError(0, "Invalid weight! (range: (0, 500])") Invoke rethrow (before) ERROR: DomainError with 0: Invalid weight! (range: (0, 500]) Stacktrace: [1] get_bmi(::Int64, ::Float64) at ./REPL[1]:4 [2] top-level scope at REPL[2]:3 julia>  根据 REPL 环境输出的前几行内容，你应该已经可以分析出这段代码的执行流程了。try子句中的调用语句get_bmi(0, 1.78)抛出了异常，使得控制流直接从那里跳到了catch子句。但由于其中的调用语句rethrow(e)的存在，异常又被重新抛了出去。最后，REPL 环境捕获并处理掉了这个异常。 从后面的那几行异常提示信息我们可以看出，虽然这个异常是被catch子句中的代码重新抛出来的，但它的各项信息都没有丝毫改变，包括引发异常的那个参数值、异常值本身的描述信息，以及调用栈信息中的所有细节，如同异常没有被捕获过一样。这就是rethrow函数所起到的作用。 我们现在知道了，catch子句在try-catch语句当中起到了举足轻重的作用。怎样处理可能发生的异常，几乎完全取决于catch子句以及其中的代码。不过，除了catch子句，try子句还可以后接finally子句。而后者在某些方面更有用处。 finally子句只能被编写在try子句和catch子句的下面。在这种情况下就形成了try-catch-finally语句。而且，一旦后接了finally子句，我们就可以不编写catch子句，而直接把try子句和finally子句拼接在一起。但是，从功能上说，这只适用于特定的情况。我在后面会讲到。 为了方便讲解，我在下面会把以这几种形式编写出的代码统称为try语句。因为无论怎样，try子句总是要写的，而且总是会写在最上面。 finally子句有一个特权，那就是：不论try子句中的代码是否抛出了异常，在它里面的语句都一定会被执行。具体的执行时机是，在try子句和catch子句中的代码被执行完毕之后，且在try语句的整体被执行完毕之前。即便其中的异常会被抛到外界，Julia 也会保证在这之前执行完finally子句。显然，finally子句在这方面与catch子句截然不同。Julia 只会在异常真的被抛出时执行catch子句。不过，finally子句却不能像catch子句那样捕获和处理异常。 正因为如此，finally子句非常适合做一些善后的处理工作。比如，记录日志、检查并修正计算结果、释放不再需要的计算资源，等等。下面是一个没有抛出异常的例子： julia> bmi = 0; julia> try global bmi = get_bmi(65, 1.78) catch e println("WARNING: captured an exception:$e")
finally
global bmi
println("BMI: $(bmi)") end BMI: 20.515086478979924 20.515086478979924 julia>  REPL 环境在最后回显的第一行内容是finally子句中的打印语句打印出来的，而第二行内容表示的则是try语句的结果值。没错，try语句也属于一种复合表达式。显然，上面这条try语句的结果值就是变量bmi的值。这是由try子句或catch子句中的最后一条语句决定的。 你可以自行调整一下传给get_bmi函数的参数值，让它们可以引发异常，然后看一看程序执行的结果会有什么不同。下面，我们将对这条try语句进行另外一项调整——删除catch子句： julia> try global bmi = 0 bmi = get_bmi(0, 1.78) finally global bmi println("BMI:$(bmi)")
end
BMI: 0
ERROR: DomainError with 0:
Invalid weight! (range: (0, 500])
Stacktrace:
[1] get_bmi(::Int64, ::Float64) at ./REPL[1]:4
[2] top-level scope at REPL[5]:3

julia>


julia> try
bmi2 = get_bmi(0, 1.78)
catch e
try bmi2 catch e1 println("[INNER ERROR 1] $e1") end finally try bmi2 catch e2 println("[INNER ERROR 2]$e2") end
try e catch e3 println("[INNER ERROR 3] $e3") end end [INNER ERROR 1] UndefVarError(:bmi2) [INNER ERROR 2] UndefVarError(:bmi2) [INNER ERROR 3] UndefVarError(:e) julia>  为了让代码更加整洁，我在其中使用了try语句的简写形式，如： try bmi2 catch e1 println("[INNER ERROR 1]$e1") end


Julia 应用程序中的异常可能是由 Julia 语言抛出的，也可能是由应用程序中的某段代码自行抛出的。但无论怎样，Julia 中的异常都会由值来承载。我们称之为异常值。这些异常值的类型一定都是Exception类型的某个子类型。我们在应用程序中的任何地方都可以使用throw函数来抛出异常。并且，我们也可以在try语句的catch子句中使用rethrow函数重新抛出已经捕获到的异常。在抛出异常的时候，我们应该仔细斟酌异常的类型和异常值的构造细节，以求尽量为异常的识别、定位和处理提供有利的条件。这也是我们在编写应用程序时必须要考虑的一个很重要的方面。

### 11.7 小结

• 0
点赞
• 0
收藏
觉得还不错? 一键收藏
• 0
评论
04-25 3767
07-13 415
01-21 3293
03-09 1046
09-18 1万+
10-06 1388
11-21

1.余额是钱包充值的虚拟货币，按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载，可以购买VIP、付费专栏及课程。