Ruby的block

2018-03-18  本文已影响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, &:+),要怎么实现?

上一篇下一篇

猜你喜欢

热点阅读