代码块(block)

def math(a, b)
  yield(a, b)
end

math(2, 3) { |x, y| x * y } # => 6

从底层看,使用代码块分为两步:1. 将代码块打包备用; 2. 调用代码块(通过 yield)执行代码。

Proc 对象

代码块并不是对象,如果需要存储,就需要一个对象,Ruby 中提供了一个名为 Proc 的类。Proc 是由代码块转换来的对象,可以通过 Proc#call 方法执行由代码块转换而来的对象。

转换 Proc

有五种方式可将代码块转换为 Proc。

Proc.new 方法

把代码块传给 Proc.new 方法可创建一个 Proc。

inc = Proc.new { |x| x + 1 }
inc.inspect  # "#<Proc:0x00000001d1e470@(irb):1>"
inc.call(2)  # => 3

proc 方法

Kernel 中的 proc 方法也可将代码块转换为 Proc。

inc = proc { |x| x + 1 }
inc.inspect  # => "#<Proc:0x00000001d0d030@(irb):4>"
inc.call(2)  # => 3

lambda 方法

有两种 lambda 方式可将代码块转换为 Proc。

dec = lambda { |x| x - 1 }
dec.inspect # => "#<Proc:0x00000001afb850@(irb):10 (lambda)>"
dec.call(2) # => 1

与下面的方式等效:

dec = ->(x)  { x - 1 }
dec.inspect # => "#<Proc:0x00000001934030@(irb):19 (lambda)>"
dec.call(2) # => 1

注意,inspect 的输出结果中末尾多了一个 lambda。

&操作符

代码块就像是方法额外的匿名参数,在方法的参数列表中使用&操作符可以将代码块参数附加到一个绑定上,该参数必须是位于参数列表末尾。

&操作符也可将 Proc 转换为代码块。

def math(a, b)
  yield(a, b)
end

def do_math(a, b, &operation)
  puts operation.inspect
  math(a, b, &operation)
end

do_math(2, 3) { |x, y| x * y } # #<Proc:0x00000001df5cb8@(irb):41>
  • 调用 do_math 时,如果没有附加的代码块 &operation 会被设置为 nil。

  • 在 do_math 中,operation 为一个 Proc 对象。

  • 调用 math 方法时,&操作符把 Proc 对象转换为代码块(&operation)。

Proc 与 Lambda 对比

用 lambda 创建的 Proc 与其他方式创建的 Proc 有细微区别, 重要差别有两个: return 和参数校验。

Proc、Lambda 和 return

lambda 中,return 仅仅表示从这个 lambda 中返回。

def another_double
  l = lambda { return 10 }
  result = l.call
  return result * 2
end

another_double # => 20

proc 中的 return 是从定义的 proc 的作用域中返回。

def another_double
  p = Proc.new { return 10 }
  result = p.call
  return result * 2
end

another_double # => 10

Proc、Lambda 和参数数量

当调用参数数量不正确时,proc 会调整参数形式(忽略多余的参数或对未指定的参数赋值为nil),而 lambda 会抛出异常。

其他

["1", "2", "3"].map(&:to_i)

与下面代码等效:

["1", "2", "3"].map {|i| i.to_i }

符号&会触发 :to_i 的 to_proc 方法,to_proc 执行后会返回一个 proc 实例,之后 & 会把这个 proc 实例转换成一个代码块。

:to_i.to_proc # => #<Proc:0x00000001d36cf0(&:to_i)>

参考