block, proc, lambda

2016-01-02  本文已影响115人  AQ王浩

&:to_i是一个block,block不能独立存在,同时你也没有办法直接存储或传递它,
必须把block挂在某个方法后面。

一、从最简单的看起


  def f1
    yield
  end

  def f2(&p)
    p.call
  end

  def f3(p)
    p.call
  end

  f1 { puts "f1" }
  f2 { puts "f2" }

  f3(proc{ puts "f3" })
  f3(Proc.new{ puts "f3" })
  f3(lambda{ puts "f3" })
  f3(->{ puts "f3" })

由于 &p 并不作为方法的参数,所以f2
不能传递一个参数,f2需要的是一个block。

&p 相等于一种申明,当方法后面有block的时候,会把block捕捉进来。

f3 需要一个Proc的参数,所以就需要传递一个Proc进去。

二、block

block,ruby中的block是方法一个重要但非必要的组成部分,我们可以认为方法
的完整定义类似于


  def f(零个或多个参数,&p)
    ...
  end

注意&p不是参数,&p类似于一种声明,当方法后面有block时,会将block捕捉起来
存放在变量p中,如果方法后面没有block,那么&p什么也不干。


>> def f(&p)
>>   end
=> :f
>> f(1)
ArgumentError: wrong number of arguments (1 for 0)
    from (irb):72:in `f'
    from (irb):74
    from /Users/wanghao/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'
>> f
=> nil
>> f(){ puts "f" }
=> nil
>> f()
=> nil

从以上代码的运行结果可以知道&p不是参数


>> def f(a)
>>   puts a
>>   end
=> :f
>> f(1) { puts 2 }
1
=> nil

所以任何方法都可以挂载一个block,如果你定义的方法想使用block做点事情,
那么你需要使用yield关键字或&p


>> def f1
>>   yield
>>   end
=> :f1
>> def f2(&p)
>>   p.call
>>   end
=> :f2
>> f1
LocalJumpError: no block given (yield)
    from (irb):88:in `f1'
    from (irb):93
    from /Users/wanghao/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'
>> f2
NoMethodError: undefined method `call' for nil:NilClass
    from (irb):91:in `f2'
    from (irb):94
    from /Users/wanghao/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'
>>

为了保证不抛出异常,我们可以这么修改


  def f1
    yield if block_given?
  end

  def f2(&p)
    p.call if block_given?
  end


这样,f1和f2后面无论挂不挂block都不会抛异常了。


def f2(&p)
  puts p.class
  puts p.inspect
  p.call
end

>> f2{->{ puts "123" }}
Proc
#<Proc:0x007fb57c0884d0@(irb):102>
=> #<Proc:0x007fb57c088390@(irb):102 (lambda)>

["1", "2", "3"].map(&:to_i),其效果和["1", "2", "3"].map {|i| i.to_i }一样, 但简洁了许多,并且更加拉风。
这里的魔法在于符号&会触发:to_i的to_proc方法, to_proc执行后会返回一个proc实例, 然后&会把这个proc实例转换成一个block,我们需要要明白map方法后挂的是一个block,而不是接收一个proc对象做为参数。&:to_i是一个block,block不能独立存在,同时你也没有办法直接存储或传递它,必须把block挂在某个方法后面。

:to_i是Symbol类的实例,Symbol中的to_proc方法的实现类似于


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



给自己的类编写to_proc 方法,然后使用&耍下酷,比如


>> class AddBy
>>   def initialize(num = 0)
>>     @num = num
>>     end
>>   def to_proc
>>     Proc.new { |obj| obj.send('+', @num) }
>>     end
>>   end
=> :to_proc
>> add_by_9 = AddBy.new(9)
=> #<AddBy:0x007fba3997c2c8 @num=9>
>> puts [1,2,3].map(&add_by_9)
10
11
12
=> nil


ruby中,block 存在形式


do |...|
  ...
end

有时候是这样


{|...| ...}

或者类似 &p, &:to_i, &add_by_9 之类,但是它无体,无体
的意思就是说block无法单独存在,必须挂在方法后面,并且你
没有办法直接把它存到变量中,也没有办法直接将它作为参数
传递给方法,所以当你想存储,传递block时,你可以使用proc
对象了。


 p = Proc.new(&:to_i)
 p = Proc.new {|obj| obj.to_i }
 p = Proc.new do |obj|
   obj.to_i
 end
 p = proc(&:to_i)
 p = proc {|obj| obj.to_i}
 p = proc do |obj|
   obj.to_i
 end


我们经常在该挂block的时候,却把proc对象当参数传给方法了,
或者不明白&p就是block可以直接交给方法使用

** &p是block, p是proc 不到万不得已的情况下不要显式地创建proc **

三、lambda

lambda是匿名方法,lambda和proc也是两种不同的东西,但是在
ruby中lambda只能依附proc而存在,这点和block不同,block并不
依赖proc


  >> p = Proc.new {}
  => #<Proc:0x007fba3a8dd708@(irb):11>
  >> puts p
  #<Proc:0x007fba3a8dd708@(irb):11>
  => nil
  >> l = lambda {}
  => #<Proc:0x007fba3a8cd498@(irb):13 (lambda)>
  >> puts l
  #<Proc:0x007fba3a8cd498@(irb):13 (lambda)>

从这里可以理解ruby的设计者们确实在有意的区分
lambda和proc,并不想把lambda和proc混在一起,如同ruby中
没有叫Block的类,除非你自己定义一个,ruby中也没有叫
Lambda的类,于是将lambda对象化的活儿就交给了Proc。

当你用lambda弄出一个匿名方法时,发现它是一个proc对象,并且
这个匿名方法能干的活,proc对象都能做,于是我们不淡定了。


  Proc.new{} 这样可以
  proc {} 这样也没有问题
  lambda {} 这样做也不错
  -> {} 这个还是能行

lambda 和 proc 之间的区别除了那个经常用做面试题的return之外
还有一个区别就是lambda 不能完美的转换为block。而proc可以完美
的转换为block,注意,我说的lambda指的是lambda方法或者->符号生成
的proc,当然和方法一样lambda是严格检查参数的,这个特点也和proc不一样。


def f0()
  p = Proc.new { return 0}
  p.call
  1
end

def f1()
  l = lambda { return 0}
  l.call
  1
end

f0 # 返回0
f1 # 返回1

如果你能够理解proc在行为上更像block,lambda其实就是方法只不过是匿名的,那么你对上面的结果不会感到惊讶。

如果把f0,f1做一些修改,就更容易理解上面的结果了。


def f0()
  return 0
  1
end

def f1()
  def __f1
    return 0
  end
  __f1
  1
end

f0 # 返回0
f1 # 返回1


验证proc不需要参数校验,而lambda需要参数校验。


>> def f5()
>>   yield 1,2
>>   end
=> :f5
>> p1 = proc { |i,j| puts j }
=> #<Proc:0x007fba3a856370@(irb):42>
>> p2 = proc { |i| puts i}
=> #<Proc:0x007fba398f0c28@(irb):43>
>> l1 = lambda { |i,j| puts j }
=> #<Proc:0x007fba398e0580@(irb):44 (lambda)>
>> l2 = lambda { |i| puts i }
=> #<Proc:0x007fba3a845160@(irb):45 (lambda)>
>> f5(&p1)
2
=> nil
>> f5(&p2)
1
=> nil
>> f5(&l1)
2
=> nil
>> f5(&l2)
ArgumentError: wrong number of arguments (2 for 1)
    from (irb):45:in `block in irb_binding'
    from (irb):40:in `f5'
    from (irb):49
    from /Users/wanghao/.rvm/rubies/ruby-2.2.2/bin/irb:11:in `<main>'
>>

四. 总结

  1. block和Proc都是代码块,Proc可以复用,block不可以复用

  2. block和lambda不是类,Proc是类,所以block和lambda是小写

  3. lambda 是匿名函数

  4. lambda对参数个数验证,Proc不会验证

  5. lambda会执行return,Proc遇到return会中断

  6. lambda不会转换成block,Proc可以转换成block

方法定义中&p参数,相等于声明,如果后面包括block的话,就会将后面的block捕捉进来放在p中

任何方法都可以挂载block,如果想要block做一些事情,就需要在定义方法的时候,指定yield或&p

  1. ["1", "2", "3"].map(&:to_i) 相等于 ["1", "2", "3"].map{ |obj|
    obj.to_i }
    这里的魔法在于符号&会触发:to_i的to_proc方法,to_proc方法执行后
    会返回一个proc实例,然后&会把这个proc转换成block。并且map后面跟
    的也是一个block

  2. &p中,&p是block,而p是proc

ruby-china

上一篇下一篇

猜你喜欢

热点阅读