Ruby 块从入门到精通

作者: 老码农不上班 | 来源:发表于2016-07-19 12:42 被阅读226次

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

    1. 入门:Ruby 块是什么
    2. yield 的内部原理是什么
    • 为方法传递块
    • Yield 同样能接受参数
    1. 块如何转换为 Proc 对象
    2. .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:0x0000010124e5a8@tmp/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 }) 的引用。不得不说这实现方式真的是漂亮至极。

    相关文章

      网友评论

        本文标题:Ruby 块从入门到精通

        本文链接:https://www.haomeiwen.com/subject/bkudjttx.html