11.3 for 语句

对于for语句,我相信你已经不会感觉到陌生了。我们在前面使用for语句迭代过不少的容器。例如:

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

又例如:

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

怎么样?想起来了吗?

当然,我们还可以使用for语句迭代任何其他的可迭代对象,就像这样:

  1. julia> for e in "Julia 编程"
  2. print(e)
  3. end
  4. Julia 编程
  5. julia> for e = 1:10
  6. print("$(e) ")
  7. end
  8. 1 2 3 4 5 6 7 8 9 10
  9. julia>

关于for语句可以迭代字符串就不用我多说了吧?它会依次地迭代出字符串中的每一个字符。

在这里的第二段代码中,我们迭代的是一个类型为UnitRange{Int64}的可迭代对象。这类对象用于表达一种数值序列。这种序列中的任意两个相邻元素值的差总会是1。 我们在以前其实已经多次使用过这种序列。只不过,我还没有正式介绍过它。

如上所示,我使用值字面量的方式表示了这类对象。更具体地说,1:10表示的是一个从1开始、到10结束且相邻值间隔为1的数值序列。因此,我们可以把在英文冒号左侧的数值称为开始值,并把在它右侧的数值称为结束值。

此外,与UnitRange相似的类型还有StepRangeLinRange。简要地说,前者的值用于表示等差序列,而后者的值则用于表示等分序列。例如:

  1. julia> typeof(10:10:30)
  2. StepRange{Int64,Int64}
  3. julia> Array(10:10:30)
  4. 3-element Array{Int64,1}:
  5. 10
  6. 20
  7. 30
  8. julia> LinRange(1.2, 2.6, 9)
  9. 9-element LinRange{Float64}:
  10. 1.2,1.375,1.55,1.725,1.9,2.075,2.25,2.425,2.6
  11. julia>

让我们再把焦点放回到前一个例子中的第二段代码上。我们在之前也说过,符号=在这里的含义并不是单纯的“赋值”,而是“每一次迭代均赋值”。它与关键字in的含义是相同。在通常情况下,后者更加常用。不过,在嵌套着迭代多个对象的时候,我们常常使用的是=而不是in。示例如下:

  1. julia> for x=1:2, y=10:10:30
  2. println((x, y))
  3. end
  4. (1, 10)
  5. (1, 20)
  6. (1, 30)
  7. (2, 10)
  8. (2, 20)
  9. (2, 30)
  10. 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 都会为所有层次上的迭代变量进行赋值。即使这些迭代变量在此次将要被赋予的值与前一次被赋予的值一摸一样,也会是如此。请看下面的示例:

  1. julia> for x=1:2, y=10:10:30
  2. println((x, y))
  3. x = 2
  4. end
  5. (1, 10)
  6. (1, 20)
  7. (1, 30)
  8. (2, 10)
  9. (2, 20)
  10. (2, 30)
  11. julia>

请注意,在这条for语句里的子语句组中有这样一行代码,即:x = 2。从表面上看,它会在每一次迭代快要结束的时候修改外层迭代变量x的值。但事实上,这样做是不会奏效的。其原因就是,Julia 在这里总是会遵循我们刚刚阐述的那个规则。更具体地说,它会在这条for语句的每一次迭代刚刚开始的时候,依据当前的迭代状态分别对xy进行赋值(或重新赋值)。显然,这会使代码x = 2所做的更改失效,尤其是在第二次迭代和第三次迭代执行的时候。

然而,当我们使用多条for语句表达一个嵌套循环的时候,Julia 就不会这样做了。也就是说,在这种情况下,它不会在每一次内层迭代开始的时候再对外层的迭代变量进行赋值。相应的示例如下:

  1. julia> for x in 1:2
  2. for y in 10:10:30
  3. println((x, y))
  4. x = 2
  5. end
  6. end
  7. (1, 10)
  8. (2, 20)
  9. (2, 30)
  10. (2, 10)
  11. (2, 20)
  12. (2, 30)
  13. julia>

我们可以看到,在这个执行结果中,第二个元组和第三个元组里的第一个元素值都变成了2,而不是原先的1。这就是代码x = 2在这里的内层for语句中所起到的作用。

也许我这样说会更便于你记忆:当嵌套的循环被合并在一起时,其中的迭代变量的值就必定不会受到任何干扰,它们只取决于对应的被迭代对象和当时的迭代状态。而当嵌套的循环是由多条for语句松散地表达时,上述干扰就很容易发生。

被合并在一起的嵌套循环的另一大优势是,它可以让代码更加简洁。但它的劣势也比较明显,那就是for语句中只能有一组子语句。如果我们想在多层的迭代之间做点什么的话,这样的for语句就无能为力了。在这种情况下,我们还是需要使用松散的多条for语句来表达。例如:

  1. julia> for x in 1:5
  2. if x % 2 == 0
  3. continue
  4. end
  5. for y in 10:10:30
  6. print("($x,$y) ")
  7. end
  8. end
  9. (1,10) (1,20) (1,30) (3,10) (3,20) (3,30) (5,10) (5,20) (5,30)
  10. julia>

在此示例中,外层循环用于对1:5进行迭代,而内层循环被用来迭代10:10:30。可以看到,这两层循环是各由一条for语句来表示的。它们可以包含各自的子语句组。也正因为如此,我可以像上面那样去控制什么时候不遍历内层的被迭代对象。

我使用一条if语句制定了一个小规则:当x可以被2整除时,不要遍历内层的对象,并直接对外层的对象进行下一次迭代。在这里,continue起到了很重要的作用。

关键字continue首先会让 Julia 放弃执行当次迭代中剩余的那些子语句。更明确地说,这些子语句处于直接包含这个continue的那条for语句之中,并且位于这个continue的下方。在此示例中,处在这个位置上的子语句只有负责内层循环的那条for语句。

紧接着,continue还会让直接包含它的那条for语句继续对它携带的被迭代对象进行下一次迭代。也就是说,continue并不会让当前的循环结束,只是让它跳过一些子语句的执行而已。

正是由于那条if语句和continue,这个例子的结果中才没有包含整数24。你可以自己模拟一下这个例子的执行流程,并依次写下它应该打印出的内容,然后再回过头来与实际的执行结果对照一下,看看你是否已经完全理解了这些代码的含义。

现在,让我们再看一个很相似的例子:

  1. julia> for x in 1:5
  2. if x % 3 == 0
  3. break
  4. end
  5. for y in 10:10:30
  6. print("($x,$y) ")
  7. end
  8. end
  9. (1,10) (1,20) (1,30) (2,10) (2,20) (2,30)
  10. julia>

与前一段代码相比,这段代码只有if语句有所不同。它体现了不同的规则,即:当x可以被3整除时,结束当前循环的执行。在这里起到关键作用的是break

关键字break做起事来非常的干脆,它不会去管当前的循环进行到哪一步了,也不会理会当前迭代的执行状态如何,而是直接让 Julia 中断对当前的for语句的执行。所以,我们在此示例的结果中才看不到整数345

我们在编写用于循环的代码时经常会碰到需要continuebreak的处理流程。所以,你需要记住它们的作用和异同。另外还要注意,它们都只能对当前的那个循环起作用。比如说,当我们在内层循环使用它们时,只有内层的循环才会受到影响,而绝不会波及到外层的循环。相应的示例如下:

  1. julia> for x in 1:5
  2. for y in 10:10:30
  3. if x % 3 == 0
  4. break
  5. end
  6. print("($x,$y) ")
  7. end
  8. end
  9. (1,10) (1,20) (1,30) (2,10) (2,20) (2,30) (4,10) (4,20) (4,30) (5,10) (5,20) (5,30)
  10. julia>

在阅读了这段代码之后,你一定会发现我把那条if语句搬到了内层的for语句之中。这个改动看起来很小,但是它对流程的改变却不小。

当外层的迭代变量x被赋予3的时候,内层的循环在第一次迭代时就会发现if语句的条件满足了。这时,break语句就有了被执行的机会(breakcontinue都可以被看做是仅包含了一个关键字的语句)。它的执行会让当前的循环(也就是内层循环)的执行中止。

然而,外层的循环却不会受到任何的影响,它只会因当次迭代中没有更多的语句可执行而继续进行下一次迭代。在外层循环的下一次迭代执行时,x的值就不再是3了。所以那条break语句就再也没有被执行的机会了。因此,在这个示例的结果中,只有整数3不会出现。

请注意,对于合并在一起的嵌套循环,或者说拥有多个被迭代对象的单条for语句,breakcontinue都会直接对整个嵌套循环起作用,而不区分它正处于循环的哪一层。这是理所当然的,不是吗?因为在这样的for语句中确实也无法识别出循环的层次。

下面,我们再来讨论另外一个很重要的问题——for语句的作用域。没错,每一条for语句都会自成一个作用域。关于此,最直观的表现就是,for语句所声明的迭代变量不能被该语句之外的代码引用到。请看下面的示例:

  1. julia> for x in 1:5
  2. print("$x ")
  3. end
  4. 1 2 3 4 5
  5. julia> x
  6. ERROR: UndefVarError: x not defined
  7. julia>

同理,像下面这样做也是不行的:

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

与前面的例子差不多,这也是两条嵌套在一起的for语句。但不同的是,我把if语句放到了内层的for语句之后,并且试图在它的条件表达式中引用内层的迭代变量y。你也看到了,这样做是不可以的。因为这超出了变量y的作用域。

更宽泛地说,在任何一条for语句中直接定义的变量的作用域都总会是这条for语句所占据的区域。就拿上例来说,变量y的作用域是内层的for语句所代表的代码块,而变量x的作用域则是外层的for语句所代表的代码块。正因为如此,我们在内层的for语句中是可以直接引用到外层的迭代变量的,但反过来却不行。

一个相关的问题是,如果内、外层作用域各自定义的变量存在重名的情况,那么会发生什么呢?我们可以用一个简单的例子来说明:

  1. julia> for x in 1:3
  2. for x in 10:10:30
  3. print("($x) ")
  4. end
  5. end
  6. (10) (20) (30) (10) (20) (30) (10) (20) (30)
  7. julia>

在这里,内层和外层的迭代变量都叫x。当内层for语句中的代码引用x时,它拿到的只是内层的迭代变量x。也就是说,在寻常的情况下,这些内层作用域中的代码是无法引用到外层的重名变量的。

当然了,还存在不寻常的情况。这涉及到了关键字outer。示例如下:

  1. julia> for x in 1:3
  2. for outer x in 10:10:30
  3. print("($x) ")
  4. end
  5. print("[$x] ")
  6. end
  7. (10) (20) (30) [30] (10) (20) (30) [30] (10) (20) (30) [30]
  8. julia>

请注意看,我把关键字outer添加在了内层for语句的第一行里,更具体的位置是迭代变量x的左侧。如此一来,这个x代表的就不再是一个在内层作用域中新定义的局部变量了,而是一个指代了那个外层的迭代变量的标识符。

从这个示例打印出的内容我们也可以看到,每当内层的for语句执行结束之后,x的值都会是30。这是因为内层for语句在每一次迭代开始时都在为外层的迭代变量x赋值。它的最后一次迭代总会把30赋给外层的迭代变量x

至此,outer关键字在这里所起的作用也就很明朗了,即:让for语句复用一个在外层作用域中定义的局部变量,并将其作为自己的迭代变量。

现在,让我们稍稍总结一下。我们可以使用for语句实现循环,还可以用它依次地取出任何可迭代对象中的元素值。一条for语句中可以有若干个被迭代对象和相应的迭代变量。当一条for语句中同时存在多个被迭代对象时,我们可以说它实现了一个嵌套的循环。当然,我们也可以把这样的嵌套循环拆成多条for语句。另外,我们还可以在for语句中添加continue语句和break语句,以达到精细控制的目的。

这里的另一个重点是,每一条for语句都会自成一个作用域。在一般情况下,for语句中的迭代变量都是仅属于该语句的局部变量,它们在外界是无法被引用的。不过,outer关键字可以对这种情况有所改变。正如前文所述。