12.8 do 代码块

我们自从在讲BigFloat的时候提了一下do代码块,之后就再也没有谈论到它了。但实际上,do代码块是一个很棒的语法糖。它是建立在“函数是 Julia 语言中的第一等公民”这一特性的基础之上的。

就像我们在先前所展示的那样,当一个函数需要另一个函数作为其第一个参数值的时候,do代码块就能够派上用场了:

  1. julia> setprecision(35) do
  2. BigFloat(1.01) + parse(BigFloat, "0.2")
  3. end
  4. 1.2099999999

这里调用的函数Base.MPFR.setprecision的签名是这样的:

  1. setprecision(f::Function, [T=BigFloat,] precision::Integer)

它的第一个参数是Function类型的,即代表函数的数据类型。

为了完整的演示,我们需要先稍微改造一下之前定义过的函数map1。如下所示:

  1. julia> function map1(f::Function, vec::Vector)
  2. [f(e) for e in vec]
  3. end
  4. map1 (generic function with 1 method)
  5. julia>

在这里,f变成了map1函数的第一个参数。又由于可选的位置参数只能被排在位置参数列表的最后,所以f现在是必选的参数。另外,我还为f参数声明了类型。

现在,我们可以像下面这样调用改造后的map1函数:

  1. julia> map1(e->e*10, [1,2,3,4])
  2. 4-element Array{Int64,1}:
  3. 10
  4. 20
  5. 30
  6. 40
  7. julia>

或者,在调用它的时候携带一个do代码块:

  1. julia> map1([1,2,3,4]) do x
  2. x*10
  3. end
  4. 4-element Array{Int64,1}:
  5. 10
  6. 20
  7. 30
  8. 40
  9. julia>

我们这次一共向 REPL 环境输入了三行代码。第一行代码包括针对map1函数的调用表达式map1([1,2,3,4])、关键字do以及标识符x。实际上,后两者与第二行的表达式x*10和第三行的关键字end共同组成了一个do代码块。

请注意,虽然map1函数的必选参数有两个,但我们只在调用表达式中传给了它一个参数值。你应该也看出来了,被传入的这个参数值是给该函数的第二个参数的。那么,第一个参数值在哪里呢?

答案是,我们这次传给map1函数的第一个参数值就是那个do代码块。我们可以把do代码块看成函数定义的一种变体。一个do代码块就代表了一个匿名函数。在这里,处于do关键字右边的标识符x就相当于函数定义中的一个参数声明。

不过,这有一个限制,那就是:do代码块代表的匿名函数只能有一个参数。当然了,我们可以通过一些手段突破这个限制。为了加以说明,我们再来定义一个map1方法:

  1. julia> function map1(f::Function, vec::Vector, extra)
  2. [f((e, extra)) for e in vec]
  3. end
  4. map1 (generic function with 2 methods)
  5. julia>

这个map1方法有三个参数。参数extra代表了额外的附加值。另外,这个方法传给f的参数值是元组(e, extra),而不是之前的单一变量e。相应的,我们调用该方法时所携带的do代码块也需要有所变化:

  1. julia> map1([1,2,3,4], 1) do (x, y)
  2. x*10+y
  3. end
  4. 4-element Array{Int64,1}:
  5. 11
  6. 21
  7. 31
  8. 41
  9. julia>

可以看到,我们在这里的调用表达式中向这个map1方法传入了第二个参数值[1,2,3,4]和第三个参数值1,而第一个参数值仍然由后面的do代码块代表。但不同的是,在do关键字右边的是由一个圆括号包裹的两个标识符。你也可以把这一小段代码看成一个元组。因为它表示的只是一个参数,而不是两个。这也正是这个map1方法在调用f时传入(e, extra)而非eextra的原因。

do代码块的意义在于,当我们需要临时定义一个函数并把它作为第一个参数值传入另一个函数的时候,使用do代码块会让代码变得非常的清晰。因为它看起来就是(事实上也是)一个独立的代码块。我们可以在这样的代码块中写入各种复杂的逻辑,而丝毫不会对前面的调用表达式以及周边的代码造成视觉上的干扰。如果在这种情况下不使用do代码块,那么就很可能会降低相关代码的可读性,甚至会间接地导致一些代码编写方面的错误。由此看来,do代码块在特定的场景下是很有用处的。