美文网首页
Ruby的block

Ruby的block

作者: 普通的人形使 | 来源:发表于2018-03-18 22:53 被阅读0次

    初识block

    # 内部迭代器
    [1, 2, 3].each {|x| puts x }
    # 用f(x) = x * 2将原集合映射到一个新集合
    [1, 2, 3].map {|x| x * 2 }
    # 挑选出一个集合里的所有奇数
    [1, 2, 3].select {|x| x%2 != 0 }
    # 动态定义一个问候方法
    define_method :greet do |name|
      puts "hello, #{name}"
    end
    ...
    

    带block方法的定义

    假设我们自己要定义一个可以接受block的方法,我们应该怎样来定义,又怎样使用传进来的block呢?
    这里假设我们要给Array类实现一个方法my_each使之与each有相同的效果,大概有下面两种途径

    1.可以直接传递给方法, 在方法内部可以通过yield来调用传入的代码块:

    class Array
      def my_each
        i = 0
        if block_given?
          while i < self.size
            yield self[i]
            i += 1
          end
        end
        self
      end
    end
    [1, 2, 3].my_each {|x| puts x }
    

    这里通过block_given?方法判断调用者是否给方法传递了block, 通过yield调用传入的block并将参数传递给block,整个过程都不直接与block打交道而是用block_given?yield这种专门处理block的方式

    2.block可以和Proc对象相互转换, 变成对象之后,可以像其他对象一样被传递,被返回,被复制等

    # 代码块被转换成了Proc的对象p
    p = Proc.new {|x| puts x }
    # p叫做可调用对象,可以通过call方法被调用
    p.call('hello')
    # 语法糖, 和上面的call等效
    p.('hello')
    p['hello']
    

    那么可以这样来实现my_each方法:

    class Array
      def my_each(p=nil)
        i = 0
        unless p.nil?
          while i < self.size
            p.call(self[i])
            i += 1
          end
        end
        self
      end
    end
    p = Proc.new{|x| puts x}
    [1, 2, 3].my_each(p)
    

    这样做是把p完全当成了方法的普通参数, 因此必须要先把block转换成Proc对象才能传入, 这与Array#each相比太麻烦了,幸好Ruby提供了语法糖:

    class Array
      def my_each(&p)
        i = 0
        unless p.nil?
          while i < self.size
            p.call(self[i])
            i += 1
          end
        end
        self
      end
    end
    [1, 2, 3].my_each {|x| puts x }
    

    如果将参数写成&p这样的形式并且放在参数表的最末,那么方法接受到block之后就会自动把block转换成Proc对象并且存进p中,然后在方法内部我们就知道p是一个Proc对象,就可以按照之前的思路处理了

    &是一个运算符, 这个运算符总是会把Proc对象转换成一个block.由于block对于Ruby程序员总是不可见的,所以不能单独使用它,但却可以把block当作参数传递给方法:

    p = Proc.new{|x| puts x }
    [1, 2, 3].my_each(&p)
    

    block的语法糖

    # 将字符串列表里的每个元素都转换成整数
    ['1', '2', '3'].map{|x| x.to_i } # => [1, 2, 3]
    # 求列表里所有数的和
    [1, 2, 3].reduce(0) {|x, y| x + y } # => 6
    

    实际上还有更简单的写法:

    ['1', '2', '3'].map(&:to_i) # => [1, 2, 3]
    [1, 2, 3].reduce(0, &:+)
    

    之所以能这么做,是因为有Symbol#to_proc这个方法的存在
    to_proc会返回一个Proc对象:

    to_i = :to_i.to_proc
    to_i.call('1') # => 1
    
    add = :+.to_proc
    add.call(1, 2) # => 3
    

    一个实现了to_proc方法的对象如果和&运算符结合,会导致对象的to_proc方法被调用,然后被&转换成block传递进方法

    ['1', '2', '3'].map(&:to_i)
    # 相当于以下两步
    to_i = :to_i.to_proc
    ['1', '2', '3'].map(&to_i)
    

    想象一下Symbol#to_proc的实现

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

    思考题: 如果to_proc要支持多个参数,比如上面的[1, 2, 3].reduce(0, &:+),要怎么实现?

    相关文章

      网友评论

          本文标题:Ruby的block

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