聊聊ruby中的block, proc和lambda
做为热身,从一些简单的例子开始,
1 | def f1 |
若你用的是 ruby1.9 及以上的版本,还可以这样,
1 | f3(-> {puts "f3"}) |
上面是 block, proc 和 lambda 的一些基本用法。
# block
先说说 block, ruby 中的 block 是方法的一个重要但非必要的组成部分,我们可以认为方法的完整定义类似于,
1 | def f(零个或多个参数, &p) |
注意 &p
不是参数, &p
类似于一种声明,当方法后面有 block 时,会将 block 捕捉起来存放到变量 p 中,如果方法后面没有 block,那么 & p 什么也不干。
1 | def f(&p) |
从上面代码的运行结果可以知道 &p
不是参数
1 | def f(a) |
所以任何方法后面都可以挂载一个 block,如果你定义的方法想使用 block 做点事情,那么你需要使用 yield
关键字或者 &p
1 | def f1 |
此时 f1, f2 执行时后面必须挂一个 block,否则会抛出异常,f1 抛出 LocalJumpError: no block given (yield) 的异常,f2 抛出 NoMethodError: undefined method ‘call’ for nil:NilClass 的异常,ruby 提供 了 block_given?
方法来判断方法后面是否挂了 block,于是我们可以这样修改 f1 和 f2,
sz es rhersvb
此时 f1, f2 执行时后面必须挂一个 block,否则会抛出异常,f1 抛出 LocalJumpError: no block given (yield) 的异常,f2 抛出 NoMethodError: undefined method ‘call’ for nil:NilClass 的异常,ruby 提供了 block_given?
方法来判断方法后面是否挂了 block,于是我们可以这样修改 f1 和 f2,
1 | def f1 |
这样的话,f1 和 f2 后面无论挂不挂 block 都不会抛异常了。 我们再来看看 f2 修改前抛出的 NoMethodError: undefined method `call’ for nil:NilClass 异常,这种情况说明当 f2 后面没有挂 block 的时候 p 是 nil, 那么我们给 f2 挂个 block,再打印出 p,看看 p 究竟是什么,
1 | def f2(&p) |
1 | f2 {} # 输出Proc和类似<Proc:0x007fdc72829780@(irb):21> |
这说明 p 是一个 Proc 实例对象,在 ruby 中,& 还可以这么用,[1,2] & [2,3] 或者 puts true if 1 && 1 或者在某个类中将它作为一个方法名。
很多 ruby 老鸟会写类似下面的代码,
["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
方法的实现类似于,
1 | class Symbol |
同理我们可以给自己写的类定义 to_proc 方法,然后使用 & 耍下酷,比如,
1 | class AddBy |
1 | add_by_9 = AddBy.new(9) |
在 ruby 中,block 有形,它有时候是这样
1 | do |...| |
有时候是这样
1 | {|...| ...} |
或者类似 &p
, &:to_i
, &add_by_9
之类,但是它无体,无体的意思就是说 block 无法单独存在,必须挂在方法后 面,并且你没有办法直接把它存到变量里,也没有办法直接将它作为参数传递给方法,所以当你想存储,传递 block 时,你可以使用 proc 对象了,
1 | p = Proc.new(&:to_i) |
虽然我在开发中经常用到 block,但是我很少显式地去使用 Proc 或 proc 去实例化 block,比如我几乎没有写过这样的代码,
1 | f(Proc.new {|...| ...}) |
在使用 block 时,我会忽略 proc 的存在,我将 proc 定位为一个幕后的工作者。我经常写类似下面的代码,
1 | def f(...) |
有些新手会写这样的代码,
1 | def f(..., &p) |
也有这样写的,虽然这没有问题,但是看起来不舒服
1 | def f(..., &p) |
或者
1 | def f(...) |
我甚至写过类似下面的代码,
1 | def f(...) |
我们经常在该挂 block 的时候,却把 proc 对象当参数传给方法了, 或者不明白 & p 就是 block 可以直接交给方法使用,我曾经也犯过这样的错误就是因为没有把 block 和 proc 正确的区分开来,&p 是 block, p 是 proc,不到万不得已的 情况下不要显式地创建 proc,每当我对 block 和 proc 之间的关系犯糊涂时,我就会念上几句。
再来聊聊 yield 和 & p,我们经常这样定义方法,
1 | def f(...) |
yield
和 call
后面都可以接参数,如果你是这样定义方法
1 | def f(...) |
那么可以这样执行代码,
1 | f(...) do |i, j| |
但是这样做也不会有错,
1 | f(...) do |
p.call (…) 的情况类似,也就是说 block 和 proc 都不检查参数 (其实通过 proc 关键字创建的 proc 在 1.8 是严格检查参数 的,但是在 1.9 及以上的版本是不检查参数的),为什么 block 和 proc 不检查参数呢?其实这个很好理解, 因为在实际应用中你可能需要在一个方法中多次调用 block 和 proc 并且给的参数个数不一样,比如,
1 | def f() |
由于方法后面只能挂一个 block,所以要实现上面的代码功能,就不能去严格检查参数了。
转入正题,这两种方式效果差不多,都能很好地利用 block。使用 yield,看起来简洁,使用 & p,看起来直观,并且你可以将 & p 传给其他方法使用。 但是在__ruby 1.8__的版本你不应像下面这样做,
1 | def f1(...) |
# lambda
lambda 是匿名方法,lambda 和 proc 也是两种不同的东西,但是在 ruby 中 lambda 只能依附 proc 而存在,这点和 block 不同,block 并不依赖 proc。
1 | l = lambda {} |
在__ruby1.8 中输出的信息类似#Proc:0x0000000000000000@:1(irb) 在 ruby1.9 及以上版本输出的信息类似 #<Proc:0x007f85548109d0@(irb):1 (lambda)>,注意 1.9 及以上版本的输出多了 (lambda),从这里可以理解 ruby 的设计者们确实在有意的区分 lambda 和 proc,并不想把 lambda 和 proc 混在一起,如同 ruby 中没有叫 Block 的类,除非你自己定义一个,ruby 中也没有叫 Lambda 的类,于是将 lambda 对象化的活儿就交给了 Proc,所以出现了这种情况, 当你用 lambda 弄出了一个匿名方法时,发现它是一个 proc 对象,并且这个匿名方法能干的活,proc 对象都能做,于是我们这些码农不淡定了, Proc.new {} 这样可以,proc {} 这样也没有问题,lambda {} 这样做也不错,->{} 这个还是能行,我平时吃个饭都为吃什么左右为难,现在一下子多出了四种差不多的方案来实现同一件事情, 确实让人不好选择,特别是有些码农还有点小洁癖,如果在代码里一会儿看到 proc {}, 一会儿看到 lambda {}, 这多不整洁啊,让人心情不畅快。 在这里我们认定 lambda 或者 -> 弄出的就是一个匿名方法,记做 l, 即使它披着 proc 的外衣,proc 或者 Proc.new 创建的就是一个 proc 对象,记做 p__ 在 ruby 各个版本中,l__和 p__是有一些差别的。
定义一些方法,
1 | def f0() |
# retrun 的几个试验
1 | def f0() |
如果你能够理解 proc 在行为上更像 block,lambda 其实就是方法只不过是匿名的,那么你对上面的结果不会感到惊讶。
如果把 f0,f1 做一些修改,就更容易理解上面的结果了。
1 | def f0() |
return 只能在方法体里执行,
1 | p = Proc.new { return 0 } |
构造 p 的时候我没有使用 proc {return 0},因为在__ruby1.8 中,proc {return 0} 的行为和 lambda 一样,比如在 ruby1.8__中,
1 | def f(p) |
1 | def f(p) |