跳转到内容

Julia 简介/控制流程

来自 Wikibooks,开放世界中的开放书籍
Previous page
类型
Julia 简介 Next page
函数
控制流程

控制流程的不同方式

[编辑 | 编辑源代码]

通常,Julia 程序的每一行都是依次执行的。有几种方法可以控制和修改执行流程。这些对应于其他语言中使用的结构

  • 三元复合表达式
  • 布尔开关表达式
  • if elseif else end — 条件执行
  • for end — 迭代执行
  • while end — 迭代条件执行
  • try catch error throw 异常处理
  • do

三元表达式

[编辑 | 编辑源代码]

通常,你想要在某个条件为真的情况下执行任务 A(或调用函数 A),或者在条件不为真的情况下执行任务 B(函数 B)。编写此代码的最快方法是使用三元运算符(“?” 和 “:”)

julia> x = 1
1
julia> x > 3 ? "yes" : "no"
"no"
julia> x = 5
5
julia> x > 3 ? "yes" : "no"
"yes"

以下是另一个示例

julia> x = 0.3
0.3
julia> x < 0.5 ? sin(x) : cos(x)
0.29552020666133955

并且 Julia 返回了 `sin(x)` 的值,因为 x 小于 0.5。 `cos(x)` 根本没有被计算。

布尔开关表达式

[编辑 | 编辑源代码]

布尔运算符允许你在条件为真的情况下计算表达式。你可以使用 `&&` 或 `||` 来组合条件和表达式。`&&` 表示“和”,`||` 表示“或”。由于 Julia 是逐个计算表达式的,因此可以轻松地安排表达式,使其仅在先前的条件为真或假时才被计算。

以下示例使用一个 Julia 函数,该函数根据数字是奇数还是偶数返回真或假:`isodd(n)`。

对于 `&&`,两部分都必须为真,因此我们可以这样写

julia> isodd(1000003) && @warn("That's odd!")
WARNING: That's odd!

julia> isodd(1000004) && @warn("That's odd!")
false

如果第一个条件(数字为奇数)为真,则计算第二个表达式。如果第一个条件不为真,则不计算表达式,只返回条件。

另一方面,对于 `||` 运算符

julia> isodd(1000003) || @warn("That's odd!")
true

julia> isodd(1000004) || @warn("That's odd!")
WARNING: That's odd!

如果第一个条件为真,则无需计算第二个表达式,因为我们已经获得了“或”所需的唯一真值,并且它将返回真值。如果第一个条件为假,则计算第二个表达式,因为该表达式可能为真。

这种类型的计算也称为“短路计算”。

If 和 Else

[编辑 | 编辑源代码]

对于更通用(也是更传统)的条件执行方法,可以使用 `if`、`elseif` 和 `else`。如果你习惯于其他语言,别担心空格、大括号、缩进、方括号、分号或任何类似的东西,但要记住用 `end` 结束条件结构。

name = "Julia"
if name == "Julia"
   println("I like Julia")
elseif name == "Python"
   println("I like Python.")
   println("But I prefer Julia.")
else
   println("I don't know what I like")
end

`elseif` 和 `else` 部分也是可选的

name = "Julia"
if name == "Julia"
   println("I like Julia")
end

不要忘记 `end`!

那么“switch”和“case”语句呢?不用担心学习它们的语法,因为它们不存在!

还有一个 `ifelse` 函数。它在实际应用中是这样的

julia> s = ifelse(false, "hello", "goodbye") * " world"

`ifelse` 是一个普通的函数,它会计算所有参数,并根据第一个参数的值返回第二个或第三个参数。使用条件 `if` 或 `? ... : `,只会计算选定路线中的表达式。或者,可以编写类似以下内容

julia> x = 10
10
julia> if x > 0
          "positive"
       else
           "negative or zero"
       end
"positive"
julia> r = if x > 0
          "positive"
       else
          "negative or zero"
       end
"positive"
                                     
julia> r
"positive"

For 循环和迭代

[编辑 | 编辑源代码]

遍历列表或一组值,或从起始值到结束值,这些都是迭代的例子,`for` ... `end` 结构允许你遍历多种类型的对象,包括范围、数组、集合、字典和字符串。

以下是简单遍历值范围的标准语法

julia> for i in 0:10:100
            println(i)
       end
0
10
20
30
40
50
60
70
80
90
100

变量 `i` 依次获取数组中每个元素的值(该数组由范围对象构建)——这里以 10 为步长从 0 到 100。

julia> for color in ["red", "green", "blue"] # an array
           print(color, " ")
       end
red green blue
julia> for letter in "julia" # a string
           print(letter, " ")
       end
j u l i a
julia> for element in (1, 2, 4, 8, 16, 32) # a tuple
           print(element, " ")
       end
1 2 4 8 16 32
julia> for i in Dict("A"=>1, "B"=>2) # a dictionary
           println(i)
       end
"B"=>2
"A"=>1
julia> for i in Set(["a", "e", "a", "e", "i", "o", "i", "o", "u"])
           println(i)
       end
e
o
u
a
i

我们还没有介绍集合和字典,但遍历它们的方式完全相同。

你可以遍历二维数组,从上到下依次遍历第 1 列,然后遍历第 2 列,依此类推

julia> a = reshape(1:100, (10, 10))
10x10 Array{Int64,2}:
 1  11  21  31  41  51  61  71  81   91
 2  12  22  32  42  52  62  72  82   92
 3  13  23  33  43  53  63  73  83   93
 4  14  24  34  44  54  64  74  84   94
 5  15  25  35  45  55  65  75  85   95
 6  16  26  36  46  56  66  76  86   96
 7  17  27  37  47  57  67  77  87   97
 8  18  28  38  48  58  68  78  88   98
 9  19  29  39  49  59  69  79  89   99
10  20  30  40  50  60  70  80  90  100
julia> for n in a
           print(n, " ")
       end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

可以使用 `=` 代替 `in`。

遍历数组并更新它
[编辑 | 编辑源代码]

当你在遍历数组时,数组会在每次循环时被检查,以防它发生改变。你应该避免犯的一个错误是在循环过程中使用 `push!` 来增加数组的大小。仔细运行以下文本,并准备好在你看到足够的内容时使用 `Ctrl-C`(否则你的计算机最终会崩溃)

julia> c = [1]
1-element Array{Int64,1}:
1
 
julia> for i in c
          push!(c, i)
          @show c
          sleep(1)
      end

c = [1,1]
c = [1,1,1]
c = [1,1,1,1]
...

循环变量和作用域

[编辑 | 编辑源代码]

遍历每个项目的变量——“循环变量”——只存在于循环内部,并在循环结束后消失。

julia> for i in 1:10
         @show i
       end
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10

julia> i
ERROR: UndefVarError: i not defined

如果你想在循环结束后记住循环变量的值(例如,如果你必须退出循环,并且需要知道你达到的值),请使用 `global` 关键字来定义一个比循环更持久的变量。

julia> for i in 1:10
         global howfar 
         if i % 4 == 0 
            howfar = i 
         end 
       end 
julia> howfar
8

在这里,`howfar` 在循环开始之前不存在,但它在循环结束后幸存下来,并讲述了它的故事。如果 `howfar` 在循环开始之前存在,则只能在循环中使用 `global` 更改它的值。

在 REPL 中的工作方式与在函数内部编写代码略有不同。在一个函数中,你会这样写

function f()
    howfar = 0
    for i in 1:10
        if i % 4 == 0 
            howfar = i 
        end 
    end 
    return howfar
end

@show f()
8

在循环内部声明的变量

[编辑 | 编辑源代码]

类似地,如果你在循环内部声明一个新变量,它在循环结束后将不再存在。在这个例子中,`k` 是在内部创建的

julia> for i in 1:5
          k = i^2 
          println("$(i) squared is $(k)")
       end 
1 squared is 1
2 squared is 4
3 squared is 9
4 squared is 16
5 squared is 25

因此它在循环结束后不再存在

julia> k
ERROR: UndefVarError: k not defined

在循环的每次迭代中创建的变量在每次迭代结束时都会被遗忘。在这个循环中

for i in 1:10
    z = i
    println("z is $z")
end
z is 1
z is 2
z is 3
z is 4
z is 5
z is 6
z is 7
z is 8
z is 9
z is 10

`z` 会在每次循环中重新创建。如果你想让变量在每次迭代中保持不变,它必须是全局的

julia> counter = 0
0

julia> for i in 1:10
               global counter
               counter += i
           end 

julia> counter
55

为了更详细地了解这一点,请考虑以下代码。

for i in 1:10
    if ! @isdefined z
        println("z isn't defined")
    end
    z = i
    println("z is $z")
end

也许你认为只有第一个循环才会产生“z 未定义错误”?事实上,即使 `z` 是在循环主体中创建的,它在下次迭代开始时也是未定义的。

z isn't defined
z is 1
z isn't defined
z is 2
z isn't defined
z is 3
z isn't defined
z is 4
z isn't defined
z is 5
z isn't defined
z is 6
z isn't defined
z is 7
z isn't defined
z is 8
z isn't defined
z is 9
z isn't defined
z is 10

同样,使用 `global` 关键字强制 `z` 在创建后在循环外部可用

for i in 1:10
    global z
    if ! @isdefined z
        println("z isn't defined")
    else
        println("z was $z")
    end
    z = i
    println("z is $z")
end
z isn't defined
z is 1
z was 1
z is 2
z was 2
...
z is 9
z was 9
z is 10

不过,如果你在全局作用域中工作,则 `z` 现在在任何地方都可用,其值为 10。

这种行为是因为我们正在 REPL 中工作。通常,最好将代码放在函数内部,这样你就不需要将从循环外部继承的变量标记为全局变量

function f()
   counter = 0
   for i in 1:10
      counter += i
   end
   return counter
end
julia> f()
55

微调循环:Continue

[edit | edit source]

有时,在特定循环中,您可能希望跳过到下一个值。您可以使用 continue 跳过循环中剩余的代码,并从下一个值开始再次循环。

for i in 1:10
    if i % 3 == 0
       continue
    end
    println(i) # this and subsequent lines are
               # skipped if i is a multiple of 3
end

1
2
4
5
7
8
10

推导式

[edit | edit source]

这个奇怪的名字概念仅仅是生成和收集项目的一种方法。在数学领域,您会这样说

"Let S be the set of all elements n where n is greater than or equal to 1 and less than or equal to 10". 

在 Julia 中,您可以将其写为

julia> S = Set([n for n in 1:10])
Set([7,4,9,10,2,3,5,8,6,1])

[n for n in 1:10] 结构被称为 **数组推导式** 或 **列表推导式** (“推导式”在“获取所有内容”而不是“理解”的意义上)。外部括号收集由在 for 迭代之前放置的表达式求值生成的元素。用方括号代替 end 来结束。

julia> [i^2 for i in 1:10]
10-element Array{Int64,1}:
  1
  4
  9
 16
 25
 36
 49
 64
 81
100

可以指定元素类型

julia> Complex[i^2 for i in 1:10]
10-element Array{Complex,1}:
  1.0+0.0im
  4.0+0.0im
  9.0+0.0im
 16.0+0.0im
 25.0+0.0im
 36.0+0.0im
 49.0+0.0im
 64.0+0.0im
 81.0+0.0im
100.0+0.0im

但 Julia 可以计算出您正在生成的結果的类型

julia> [(i, sqrt(i)) for i in 1:10]
10-element Array{Tuple{Int64,Float64},1}:
(1,1.0)
(2,1.41421)
(3,1.73205)
(4,2.0)
(5,2.23607)
(6,2.44949)
(7,2.64575)
(8,2.82843)
(9,3.0)
(10,3.16228)

以下是使用推导式创建字典的方法

julia> Dict(string(Char(i + 64)) => i for i in 1:26)
Dict{String,Int64} with 26 entries:
 "Z" => 26
 "Q" => 17
 "W" => 23
 "T" => 20
 "C" => 3
 "P" => 16
 "V" => 22
 "L" => 12
 "O" => 15
 "B" => 2
 "M" => 13
 "N" => 14
 "H" => 8
 "A" => 1
 "X" => 24
 "D" => 4
 "G" => 7
 "E" => 5
 "Y" => 25
 "I" => 9
 "J" => 10
 "S" => 19
 "U" => 21
 "K" => 11
 "R" => 18
 "F" => 6

接下来,这里有两个推导式中的迭代器,用逗号分隔,这使得生成表格非常容易。这里我们正在创建一个元组表格

julia> [(r,c) for r in 1:5, c in 1:2]
5×2 Array{Tuple{Int64,Int64},2}:
(1,1)  (1,2)
(2,1)  (2,2)
(3,1)  (3,2)
(4,1)  (4,2)
(5,1)  (5,2)

r 经历了五个循环,每个循环对应 c 的每个值。嵌套循环 以相反的方式工作。在这里,列优先顺序得到尊重,如当数组用纳秒时间值填充时所示

julia> [Int(time_ns()) for r in 1:5, c in 1:2]
5×2 Array{Int64,2}:
1223184391741562  1223184391742642
1223184391741885  1223184391742817
1223184391742067  1223184391743009
1223184391742256  1223184391743184
1223184391742443  1223184391743372

您也可以提供一个测试表达式来过滤生产。例如,生成 1 到 100 之间的所有正好可以被 7 整除的整数

julia> [x for x in 1:100 if x % 7 == 0]
14-element Array{Int64,1}:
  7
 14
 21
 28
 35
 42
 49
 56
 63
 70
 77
 84
 91
 98
生成器表达式
[edit | edit source]

与推导式类似,生成器表达式可用于从迭代变量中生成值,但与推导式不同,这些值是按需生成的。

julia> sum(x^2 for x in 1:10)
385
julia> collect(x for x in 1:100 if x % 7 == 0)
14-element Array{Int64,1}:
  7
 14
 21
 28
 35
 42
 49
 56
 63
 70
 77
 84
 91
 98

枚举数组

[edit | edit source]

通常,您希望逐元素遍历数组,同时跟踪每个元素的索引号。enumerate() 函数为您提供了一些内容的 **可迭代** 版本,生成索引号和每个索引号的值

julia> m = rand(0:9, 3, 3)
3×3 Array{Int64,2}:
6  5  3
4  0  7
1  7  4

julia> [i for i in enumerate(m)]
3×3 Array{Tuple{Int64,Int64},2}:
(1, 6)  (4, 5)  (7, 3)
(2, 4)  (5, 0)  (8, 7)
(3, 1)  (6, 7)  (9, 4)

在循环的每次迭代中,都会检查数组是否有可能的变化。

压缩数组

[edit | edit source]

有时您希望同时遍历两个或多个数组,先取每个数组的第一个元素,然后是第二个元素,依此类推。使用名称恰当的 zip() 函数可以实现这一点

julia> for i in zip(0:10, 100:110, 200:210)
           println(i) 
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 i in zip(0:10, 100:110, 200:215)
           println(i)
       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 不会被欺骗 - 任何数组中的过剩或不足都会被优雅地处理。

julia> for i in zip(0:15, 100:110, 200:210)
           println(i)
       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)

但是,这在填充数组时不起作用,在这种情况下,维度必须匹配

(v1.0) julia> [i for i in zip(0:4, 100:102, 200:202)]
ERROR: DimensionMismatch("dimensions must match")
Stacktrace:
 [1] promote_shape at ./indices.jl:129 [inlined]
 [2] axes(::Base.Iterators.Zip{UnitRange{Int64},Base.Iterators.Zip2{UnitRange{Int64},UnitRange{Int64}}}) at ./iterators.jl:371
 [3] _array_for at ./array.jl:611 [inlined]
 [4] collect(::Base.Generator{Base.Iterators.Zip{UnitRange{Int64},Base.Iterators.Zip2{UnitRange{Int64},UnitRange{Int64}}},getfield(Main, Symbol("##5#6"))}) at ./array.jl:624
 [5] top-level scope at none:0
(v1.0) julia> [i for i in zip(0:2, 100:102, 200:202)]
3-element Array{Tuple{Int64,Int64,Int64},1}:
 (0, 100, 200)
 (1, 101, 201)
 (2, 102, 202)

可迭代对象

[edit | edit source]

“for something in something” 结构对于所有可以迭代的内容都是一样的:数组、字典、字符串、集合、范围等等。在 Julia 中,这是一个通用原则:您可以创建“可迭代对象”的多种方式,该对象旨在用作迭代过程的一部分,该过程一次提供一个元素。

我们已经见过的最明显的例子是范围对象。当您将其输入 REPL 时,它看起来并不多

julia> ro = 0:2:100
0:2:100

但当您开始遍历它时,它会为您提供数字

julia> [i for i in ro]
51-element Array{Int64,1}:
  0
  2
  4
  6
  8
 10
 12
 14
 16
 18
 20
 22
 24
 26
 28
  ⋮
 74
 76
 78
 80
 82
 84
 86
 88
 90
 92
 94
 96
 98
100

如果您想将范围(或其他可迭代对象)中的数字放到数组中,您可以使用 collect() 将它们收集起来

julia> collect(0:25:100)
5-element Array{Int64,1}:
  0
 25
 50
 75
100

您不必收集可迭代对象中的所有元素,您只需遍历它即可。当您使用其他 Julia 函数创建的可迭代对象时,这将特别有用。例如,permutations() 创建一个可迭代对象,其中包含数组的所有排列。当然,您可以使用 collect() 来获取它们并创建一个新的数组

julia> collect(permutations(1:4))
24-element Array{Array{Int64,1},1}:
 [1,2,3,4]
 [1,2,4,3]
 
 [4,3,2,1]

但对于任何大型事物,都会有数百或数千个排列。这就是迭代器对象不会同时生成迭代中的所有值的缘故:内存和性能。范围对象占用的空间并不大,即使遍历它可能需要很长时间,具体取决于范围的大小。如果您一次生成所有数字,而不是仅在需要时生成它们,那么所有这些数字都必须存储在某个地方,直到您需要它们……

Julia 为处理其他类型的数据提供了可迭代对象。例如,当您处理文件时,可以将打开的文件视为可迭代对象

 filehandle = "/Users/me/.julia/logs/repl_history.jl"
 for line in eachline(filehandle)
     println(length(line), line)
 end
使用 eachindex()
[edit | edit source]

遍历数组时的一种常见模式是对 i 的每个值执行某些任务,其中 i 是每个元素的索引号,而不是元素本身

 for i in eachindex(A)
   # do something with i or A[i]
 end

这是惯用的 Julia 代码,在所有情况下都是正确的,并且在某些情况下(比接下来的代码)速度更快。一个不好的代码模式来完成相同的事情,在它起作用的情况下(并非总是如此),是

 for i = 1:length(A)
   # do something with i or A[i]
 end
注意高级用户
[edit | edit source]

为了介绍的目的,假设数组和矩阵的索引从 1 开始可能是可以的(对于完全通用的代码来说并非如此,即用于在注册的包中引入)。但是,在 Julia 中,当然可以使用其他索引基 - 例如,OffsetArrays.jl 包允许您选择任何起始索引。当您开始使用更高级的数组索引类型时,最好阅读 [1] 中的官方文档。

更多迭代器

[edit | edit source]

有一个名为 IterTools.jl 的 Julia 包,提供了一些高级迭代器函数。

julia> ]
(v1.0) pkg> add IterTools
julia> using IterTools

例如,partition() 将迭代器中的对象分组为易于处理的块

julia> collect(partition(1:10, 3, 1))
8-element Array{Tuple{Int64,Int64,Int64},1}:
(1, 2, 3) 
(2, 3, 4) 
(3, 4, 5) 
(4, 5, 6) 
(5, 6, 7) 
(6, 7, 8) 
(7, 8, 9) 
(8, 9, 10)

chain() 遍历所有迭代器,一个接一个

 for i in chain(1:3, ['a', 'b', 'c'])
   @show i
end

 i = 1
 i = 2
 i = 3
 i = 'a'
 i = 'b'
 i = 'c'

subsets() 遍历对象的所以子集。您可以指定大小

 for i in subsets(collect(1:6), 3)
   @show i
end

 i = [1,2,3]
 i = [1,2,4]
 i = [1,2,5]
 i = [1,2,6]
 i = [1,3,4]
 i = [1,3,5]
 i = [1,3,6]
 i = [1,4,5]
 i = [1,4,6]
 i = [1,5,6]
 i = [2,3,4]
 i = [2,3,5]
 i = [2,3,6]
 i = [2,4,5]
 i = [2,4,6]
 i = [2,5,6]
 i = [3,4,5]
 i = [3,4,6]
 i = [3,5,6]
 i = [4,5,6]

嵌套循环

[edit | edit source]

如果您想将一个循环嵌套在另一个循环中,则不必重复 for end 关键字。只需使用逗号即可

julia> for x in 1:10, y in 1:10
          @show (x, y)
       end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
(x,y) = (1,4)
(x,y) = (1,5)
(x,y) = (1,6)
(x,y) = (1,7)
(x,y) = (1,8)
(x,y) = (1,9)
(x,y) = (1,10)
(x,y) = (2,1)
(x,y) = (2,2)
(x,y) = (2,3)
(x,y) = (2,4)
(x,y) = (2,5)
(x,y) = (2,6)
(x,y) = (2,7)
(x,y) = (2,8)
(x,y) = (2,9)
(x,y) = (2,10)
(x,y) = (3,1)
(x,y) = (3,2)
...
(x,y) = (9,9)
(x,y) = (9,10)
(x,y) = (10,1)
(x,y) = (10,2)
(x,y) = (10,3)
(x,y) = (10,4)
(x,y) = (10,5)
(x,y) = (10,6)
(x,y) = (10,7)
(x,y) = (10,8)
(x,y) = (10,9)
(x,y) = (10,10)

(有用的 @show 宏打印出事物的名称及其值。)

嵌套循环的简短形式和长形式之间的一个区别是 break 的行为

julia> for x in 1:10
          for y in 1:10
              @show (x, y)
              if y % 3 == 0
                 break
              end
          end
       end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)
(x,y) = (2,1)
(x,y) = (2,2)
(x,y) = (2,3)
(x,y) = (3,1)
(x,y) = (3,2)
(x,y) = (3,3)
(x,y) = (4,1)
(x,y) = (4,2)
(x,y) = (4,3)
(x,y) = (5,1)
(x,y) = (5,2)
(x,y) = (5,3)
(x,y) = (6,1)
(x,y) = (6,2)
(x,y) = (6,3)
(x,y) = (7,1)
(x,y) = (7,2)
(x,y) = (7,3)
(x,y) = (8,1)
(x,y) = (8,2)
(x,y) = (8,3)
(x,y) = (9,1)
(x,y) = (9,2)
(x,y) = (9,3)
(x,y) = (10,1)
(x,y) = (10,2)
(x,y) = (10,3)

julia> for x in 1:10, y in 1:10
          @show (x, y)
         if y % 3 == 0
           break
         end
       end
(x,y) = (1,1)
(x,y) = (1,2)
(x,y) = (1,3)

请注意,在简短形式中, break 会从内部和外部循环中退出,但在长形式中,它只会从内部循环中退出。

优化嵌套循环

[edit | edit source]

在 Julia 中,内部循环应该关注行而不是列。这是因为数组在内存中的存储方式。例如,在这个 Julia 数组中,单元格 1、2、3 和 4 在内存中是相邻存储的(“列优先”格式)。因此,从 1 到 2 到 3 向下移动列比沿着行移动更快,因为从 1 到 5 到 9 跳跃列需要额外的计算。

+-----+-----+-----+--+
|  1  |  5  |  9  |
|     |     |     |
+--------------------+
|  2  |  6  |  10 |
|     |     |     |
+--------------------+
|  3  |  7  |  11 |
|     |     |     |
+--------------------+
|  4  |  8  |  12 |
|     |     |     |
+-----+-----+-----+--+

以下示例包含简单的循环,但行和列的迭代方式不同。“错误”版本沿着第一行逐列查看,然后向下移动到下一行,依此类推。

function laplacian_bad(lap_x::Array{Float64,2}, x::Array{Float64,2})
    nr, nc = size(x)
    for ir = 2:nr-1, ic = 2:nc-1 # bad loop nesting order
        lap_x[ir, ic] =
            (x[ir+1, ic] + x[ir-1, ic] +
            x[ir, ic+1] + x[ir, ic-1]) - 4*x[ir, ic]
    end
end

在“正确”版本中,两个循环嵌套得当,因此内部循环向下移动,沿着数组的内存布局。

function laplacian_good(lap_x::Array{Float64,2}, x::Array{Float64,2})
    nr,nc = size(x)
    for ic = 2:nc-1, ir = 2:nr-1 # good loop nesting order
        lap_x[ir,ic] =
            (x[ir+1,ic] + x[ir-1,ic] +
            x[ir,ic+1] + x[ir,ic-1]) - 4*x[ir,ic]
    end
end

另一种提高速度的方法是使用宏 `@inbounds` 来移除数组边界检查。

function laplacian_good_nocheck(lap_x::Array{Float64,2}, x::Array{Float64,2})
    nr,nc = size(x)
    for ic = 2:nc-1, ir = 2:nr-1 # good loop nesting order
        @inbounds begin lap_x[ir,ic] = # no array bounds checking
            (x[ir+1,ic] +  x[ir-1,ic] +
            x[ir,ic+1] + x[ir,ic-1]) - 4*x[ir,ic]
        end
    end
end

这是测试函数

function main_test(nr, nc)
    field = zeros(nr, nc)
    for ic = 1:nc, ir = 1:nr
        if ir == 1 || ic == 1 || ir == nr || ic == nc
            field[ir,ic] = 1.0
        end
    end
    lap_field = zeros(size(field))

    t = @elapsed laplacian_bad(lap_field, field)
    println(rpad("laplacian_bad", 30), t)
    
    t = @elapsed laplacian_good(lap_field, field)
    println(rpad("laplacian_good", 30), t)
    
    t = @elapsed laplacian_good_nocheck(lap_field, field)
    println(rpad("laplacian_good no check", 30), t)
end

结果显示了仅基于行/列扫描顺序的性能差异。“无检查”版本更快……

julia> main_test(10000,10000)
laplacian_bad                 1.947936034
laplacian_good                0.190697149
laplacian_good no check       0.092164871

创建您自己的可迭代对象

[编辑 | 编辑源代码]

可以设计您自己的可迭代对象。在定义类型时,需要向 Julia 的 `iterate()` 函数添加几个方法。然后可以使用类似 `for` .. `end` 的循环遍历对象的组件,这些 `iterate()` 方法会在需要时自动调用。

以下示例演示如何创建一个可迭代对象,该对象生成将大写字母与 1 到 9 的数字组合在一起的字符串序列。因此,我们序列中的第一个项目是“A1”,然后是“A2”、“A3”,一直到“A9”,然后是“B1”、“B2”,依此类推,最后是“Z9”。

首先,我们将定义一个名为 SN(StringNumber)的新类型

mutable struct SN
    str::String
    num::Int64
end

稍后我们将使用类似这样的方式创建此类型的可迭代对象

sn = SN("A", 1)

迭代器将产生所有字符串,直到“Z9”。

现在必须向 `iterate()` 函数添加两种方法。此函数已经存在于 Julia 中(这就是您可以迭代所有基本数据对象的原因),因此需要 `Base` 前缀:我们正在向现有的 `iterate()` 函数添加一个新方法,该方法旨在处理这些特殊对象。

第一个方法不接受任何参数,除了类型,用于启动迭代过程。

function Base.iterate(sn::SN)
    str = sn.str 
    num = sn.num

    if num == 9
        nextnum = 1
        nextstr = string(Char(Int(str[1])) + 1)    
    else
        nextnum = num + 1
        nextstr = str
    end

    return (sn, SN(nextstr, nextnum))
end

它返回一个元组:第一个值和迭代器的未来值,我们已经计算出来了(以防我们想要从“A1”以外的点开始迭代器)。

`iterate()` 的第二个方法接受两个参数:一个可迭代对象和当前状态。它再次返回两个值的元组,下一个项目和下一个状态。但首先,如果没有更多可用的值,则 `iterate()` 函数应该不返回任何内容。

function Base.iterate(sn::SN, state)

    # check if we've finished?
    if state.str == "[" # when Z changes to [ we're done
        return 
    end 

    # we haven't finished, so we'll use the incoming one immediately
    str = state.str
    num = state.num

    # and prepare the one after that, to be saved for later
    if num == 9
        nextnum = 1
        nextstr = string(Char(Int(str[1])) + 1)    
    else
        nextnum = num + 1
        nextstr = state.str
    end

    # return: the one to use next, the one after that
    return (SN(str, num), SN(nextstr, nextnum))
end

告诉迭代器何时完成很容易,因为一旦传入的状态包含“[",我们就完成了,因为“["(91)的代码紧随“Z”(90)的代码之后。

通过将这两个方法添加到 SN 类型中,现在可以迭代它们了。添加一些其他 Base 函数的方法也很有用,例如 `show()` 和 `length()`。`length()` 方法计算从 `sn` 开始有多少个 SN 字符串可用。

Base.show(io::IO, sn::SN) = print(io, string(sn.str, sn.num))

function Base.length(sn::SN) 
    cn1 = Char(Int(Char(sn.str[1]) + 1)) 
    cnz = Char(Int(Char('Z')))
    (length(cn1:cnz) * 9) + (10 - sn.num)
end

迭代器现在已准备好使用

julia> sn = SN("A", 1)
A1

julia> for i in sn
          @show i 
       end 
i = A1
i = A2
i = A3
i = A4
i = A5
i = A6
i = A7
i = A8
...
i = Z6
i = Z7
i = Z8
i = Z9
julia> for sn in SN("K", 9)
           print(sn, " ") 
       end
K9 L1 L2 L3 L4 L5 L6 L7 L8 L9 M1 M2 M3 M4 M5 M6 M7 M8 M9 N1 N2 N3 N4 N5 N6 N7 N8
N9 O1 O2 O3 O4 O5 O6 O7 O8 O9 P1 P2 P3 P4 P5 P6 P7 P8 P9 Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8
Q9 R1 R2 R3 R4 R5 R6 R7 R8 R9 S1 S2 S3 S4 S5 S6 S7 S8 S9 T1 T2 T3 T4 T5 T6 T7 T8
T9 U1 U2 U3 U4 U5 U6 U7 U8 U9 V1 V2 V3 V4 V5 V6 V7 V8 V9 W1 W2 W3 W4 W5 W6 W7 W8
W9 X1 X2 X3 X4 X5 X6 X7 X8 X9 Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Y9 Z1 Z2 Z3 Z4 Z5 Z6 Z7 Z8
Z9
julia> collect(SN("Q", 7)),
(Any[Q7, Q8, Q9, R1, R2, R3, R4, R5, R6, R7  …  Y9, Z1, Z2, Z3, Z4, Z5, Z6, Z7, Z8, Z9],)

While 循环

[编辑 | 编辑源代码]

要重复一些表达式,直到某个条件为真,请使用 `while` ... `end` 结构。

julia> x = 0
0
julia> while x < 4
           println(x)
           global x += 1
       end

0
1
2
3

如果在函数之外工作,则在更改 `x` 的值之前需要 `global` 声明。在函数内部,不需要 `global`。

如果希望在语句之后而不是之前测试条件,从而产生“do .. until”形式,请使用以下结构

while true
   println(x)
   x += 1
   x >= 4 && break
end

0
1
2
3

这里我们使用布尔开关而不是 `if` ... `end` 语句。

While 循环模板

[编辑 | 编辑源代码]

这是一个 `while` 循环的基本模板,它将重复运行函数 `find_value`,直到它返回的值不大于 0。

function find_value(n) # find next value if current value is n
    return n - 0.5
end 

function main(start=10)
    attempts = 0
    value = start # starting value
    while value > 0.0 
        value = find_value(value) # next value given this value
        attempts += 1
        println("value: $value after $attempts attempts" )
    end
    return value, attempts
end

final_value, number_of_attempts = main(0)

println("The final value was $final_value, and it took $number_of_attempts attempts.")

例如,通过少量更改,此代码探索了著名的 Collatz 猜想

function find_value(n)
    ifelse(iseven(n), n ÷ 2, 3n + 1) # Collatz calculation
end 

function main(start=10)
    attempts = 0
    value = start # starting value
    while value > 1 # while greater than 1
        value = find_value(value)
        attempts += 1
        println("value: $value after $attempts attempts" )
    end
    return value, attempts
end

final_value, number_of_attempts = main(27)

println("The final value was $final_value, and it took $number_of_attempts attempts.")

`main(12)` 需要 9 次尝试,而 `main(27)` 需要 111 次尝试。

使用 Julia 的宏,可以创建自己的控制结构。请参阅 元编程

如果希望编写检查错误并优雅地处理它们的代码,请使用 `try` ... `catch` 结构。

使用 `catch` 语句,可以处理代码中出现的错误,可能允许程序继续运行,而不是突然停止。

在下一个示例中,我们的代码尝试直接更改字符串的第一个字符(这是不允许的,因为 Julia 中的字符串不能就地修改)

julia> s = "string";
julia> try
          s[1] = "p"
       catch e
          println("caught an error: $e")
          println("but we can continue with execution...")
       end

 caught an error: MethodError(setindex!,("string","p",1)) but we can continue with execution...

`error()` 函数使用给定消息引发错误异常。

最后,让我们看一下 `do` 块,它是另一种语法形式,与列表推导一样,乍一看有点倒退(即,可能通过从末尾开始,逐步向前理解)。

还记得 之前 的 `find()` 示例吗?

julia> smallprimes = [2,3,5,7,11,13,17,19,23];
julia> findall(x -> isequal(13, x), smallprimes)
1-element Array{Int64,1}:
6

匿名函数 ( `x -> isequal(13, x)` ) 是 `find()` 的第一个参数,它对第二个参数进行操作。但是使用 `do` 块,可以将函数提升出来,并将其放在 `do ... end` 块结构之间

julia> findall(smallprimes) do x
         isequal(x, 13) 
      end
1-element Array{Int64,1}:
6

您只需要删除箭头,并更改顺序,将 `find()` 函数及其目标参数放在前面,然后在 `do` 之后添加匿名函数的参数和主体。

这样做的目的是,将较长的匿名函数写在表单末尾的几行中,比将其作为第一个参数插入更容易。

华夏公益教科书