Ruby 块从入门到精通

“块”是 Ruby 中最有用的特性之一,但是也常常被忽略。 开始学习 Ruby 块时,经常被 yield 弄的难以理解。这篇文章将会谈及这些语法概念并提供一些例子,通篇理解这篇文章,相信读者对 Ruby 块有深入的理解。

  1. 入门:Ruby 块是什么
  2. yield 的内部原理是什么
    • 为方法传递块
    • Yield 同样能接受参数
  3. 块如何转换为 Proc 对象
  4. .map(&:something)的工作原理是什么

入门:Ruby 块是什么

Ruby 块就是放在 doend 之间的代码。这就是它的全部。那么它的魔法是什么呢?
有两种方式可实现块:

  • 嵌套在 doend之间多行代码
  • 被大括号包裹的一行代码

两种实现方式都是做一样的事情,如果块中的代码超过一行(这里不是指代码行数,而是显示上有换行),建议使用第一种,可读性更强。例如:

# 多行块
ruby [1, 2, 3].each do |n|
  puts "Number #{n}"
end
# 单行块
[1, 2, 3].each {|n| puts "Number #{n}"}

上面的代码中,两个竖线中间的 n 是块的参数,值为每次遍历出来的数组值。运行结果如下:

Number 1
Number 2
Number 3
 => [1, 2, 3]

yield 的内部原理是什么

yield是 Ruby块的重要概念,也是很多人疑惑的概念。在我看来,疑惑来源于yield如何调用了块和它如何传递参数给块。通过下面代码解释一下。

def my_method
  puts "reached the top"
  yield
  puts "reached the bottom"
end

my_method do
  puts "reached yield"
end
reached the top
reached yield
reached the bottom
 => nil

my_method方法被调用,执行到yield,这时块中的代码被执行。然后,当块的代码被执行完成之后,my_method方法中yield后面的代码继续执行。

ruby_block_flow.png

把块传到方法

块通常不在方法中定义,而是通过作为一个参数传递给块。块可以传递给任何方法,前提是方法中有yield关键字块才回被调用并执行。
另外,如果你的方法中有yield,但是调用该方法时却没有传递块的话,程序会报错,此时调用方法时必须传递块。不过,使用block_given? 方法,我们可以把块作为一个可选参数传递给方法。block_given?方法根据是否有块传递给被调用的方法返回一个布尔值。

yield 关键字同样能带参数

任何传递给 yield 关键字的参数都能作为参数传递给块。所以当块的代码被执行时,它能使用从被调用方法传过来的参数。这些参数可以是被调用方法中的局部变量。
参数的顺序需要注意,传递到块的参数顺序就是块中接收这些参数的顺序。
ruby_block_arguments.png
上面的那段代码中,块的参数|name ,age|在块中是局部变量。与被调用方法中的"John",2)是不一样的,它们只是按照对应的顺序接收了从方法中传过来的参数。

块如何转换为 Proc 对象

从底层看, 代码块使用分为两步:

  1. 将代码打包为块
  2. 调用代码块

通常在调用方法时才会定义一个块,但如果要先把代码打包起来(组织为块),以后备用呢。由于块不是对象,那么这时候就需要把Ruby 块转化为一个对象保存下来供以后调用。
为了解决这个问题, Ruby 标准库引入了 Proc 类。Proc 对象就是由块转换而来。
inc = Proc.new { |x| x+1 }
执行块:inc.call(1)
块就像方法的一个匿名参数。绝大多数情况下,在方法中可通过yield语句直接运行一个块。但是有两种情况需要使用 Proc 对象。

  • 被调用方法A调用另外一个方法B,而块在方法B 中才被调用。
  • 使用 Proc 的 call 方法调用块而不是 yield

基于以上两种情况,我们需要一个“块的引用”。Ruby 中使用 &符 实现。
Ruby 允许任何对象以“块”的形式传递给方法,而 Proc 对象是“块”的引用,因为 Ruby 方法中最后一个参数有 & 参数时,其必须是 Proc 对象,如果不是,则会调用这个对象的to_proc方法把其转换为 Proc 对象,也就是块的引用。
&a 表示,a 是一个 Proc 对象,我想把它当成块来使用。看下面一个例子。

def my_method(&block)
  puts block
  block.call
end

my_method { puts "Hello!" }
#<Proc:[email protected]/example.rb:6>
Hello!

&block就是块的引用,block 就是 Proc 对象,通过调用 call 方法,块就被执行。这里也可使用 yield ,只不过使用 call方法代码可读性更佳。

.map(&:some_method)的工作原理是什么

You’ve probably used shortcuts like .map(&:capitalize) a lot, especially if you’ve done any Rails coding. It’s a very clean shortcut to .map { |title| title.capitalize }.
写了这么多年 Ruby & Rails ,写过以及遇到上面的这段代码很多吧。但是,你是否真正理解这段代码吗? Ruby 这种漂亮的法术值得去研究一下。
基于上文讨论的 Proc 对象以及方法中的 &参数,我们举个展开讨论:

name = ['xiaoming','laowang']
name.map(&:capitalize)
# => ["Xiaoming","Laowang"]

等价于

name = ['xiaoming','laowang']
name.map { |name| name.capitalize }
# => ["Xiaoming","Laowang"]

这是为什么呢?
:capitalize这个 Symbol 对象在 &符之后,而 &符号 后面又必须是 Proc 对象,所以: &:capitalize意味着向上推导出 .map(&:capitalize.to_proc),然后 Symbol 中必定有一个 to_proc 的实例方法。而这个 to_proc 也必定调用了 capitalize方法实现了 { |name| name.capitalize }

class Symbol
  def to_proc
    Proc.new { |x| x.send(self) }
  end
end

所以如果 :capitalize调用了 Symbol to_proc 实例方法,那么则返回一个带有参数的 proc ,并且这个参数通过 send方法动态调用了 capitalize 方法(这里 self 代表调用了 to_proc 方法的的对象,也就是 :capitalize)。
总结一下:
&:capitalize 调用了 Symbol 的 to_proc 方法,返回 Proc 对象,而 proc 就是 代码块({ |name| name.capitalize }) 的引用。不得不说这实现方式真的是漂亮至极。

1 条评论
您想说点什么吗?
comment test 评论于 2017-04-02 00:25

comment test