Ruby闭包: Block, Proc, lambda

Ruby中的闭包非常强大,也非常重要,所以决定系统的学习下。

闭包(Closure),是指未绑定到任何对象的自由代码,闭包中的代码与任何对象和全局变量无关,只与执行此段代码的上下文相关。

Ruby中闭包的实现有:Block,Proc,Lambada。在学习它们的区别的时候,网上讲的都不是很全面,突然记起vincent在以前的一篇开发周记中专门讲到这个主题,遂又仔细的通读了一遍,讲的非常详细全面,顿时就彻底弄懂了,于是也记录在这里,以便分享与以后查阅。

首先,我们来看Block。

arr = [1,2,3,4,5]  
arr.each { |item| puts item }

这段代码,我们使用了Array对象的block方法,将ary中的每个元素打印出来。从例子中我们可以看到block使用起来很方便,想必传统的Java以及C编码,省略掉了冗余的循环,让你更加专注业务代码,这也是ruby的好处之一。

使用block的最大好处就是可以省略重复的冗余的无用的代码,我们再来看一个读文件的例子:

File.open(__FILE__) do |f|
  puts f.readlines
end

对比用Java代码来读文件,每次都很反感那个冗长的try-catch-finally。从上面的ruby代码中我么可以看到,ruby语言给你做了处理,通过block你可以不用写无用的系统代码了。

从上面的例子我们可以看到,Ruby中的Block对开发者来说,不用再写那些冗余无用的系统代码,而是更加专注业务代码,提升开发效率。

其次,我们再看Proc。

如果你经常使用Block,你会发现一个问题:代码中会有很多重复的Block,比如好多地方需要打印文件内容。怎么解决呢?你第一个想到的是写一个公共函数,不错,可以解决。但是记住你使用的是ruby语言,不要重新造轮子。Ruby提供了函数化的Block:Proc。

我们看一个简单的Proc例子:

p = Proc.new{|f| puts f.readlines}
File.open(__FILE__, &p)

上面例子可以看到,将Block代码定义为一个Proc对象,然后在使用Block的地方用Proc替换,这样就做到了完美的代码复用。

最后我们看看lambda。

Lambda:拉姆达表达式,ruby中可以通过lambda表达式创建Proc对象,可以把它理解成一种匿名函数。我们先看例子:

new_p = lambda{|f| puts f.readlines}
File.open(__FILE__, &new_p)

上面例子中,我们使用系统的lambda方法创建了一个Proc对象,其效果与Proc.new创建出来的是一样的。其实使用lambda与使用Proc.new效果基本是一样的。下面就来讲下他们的区别:

yield 和 block call 的区别

yield 和 block call 两种都能实现运行闭包,从实际运行效果来说,区别不大。其区别主要在于:

1. 闭包的传递和保存

因为闭包已经传递到参数里,所以可以继续传递或保存起来,例如:

class A
  def foo1(&b)
    @proc = b
  end

  def foo2
    @proc.call if @proc
  end
end
   
a = A.new
a.foo1 { puts "proc from foo1" }
a.foo2

2. 性能

yield不是方法调用,而是 Ruby 的关键字,yield 要比 block call 比快 1 倍左右。

block 和 proc, lambda 的区别

很简单直接,引入 proc 和 lambda 就是为了复用,避免重复定义,例如在上例中,使用 proc 变量存储闭包,避免重复定义两个 block 。

proc 和 lambda 的区别

这大概是最让人困惑的地方,从行为上看,他们的效果是一致的,为什么要用两种不同的表达方式。

proc = Proc.new { puts "foo in proc" }
lambda_proc = lambda { puts "foo in lambda" }

确实,对于简单的情况,他们的行为是一致的,但是主要在两个地方有明显差异:

1. 参数检查,lambdas检查参数的个数,Procs不会。

还是例子说话

def foo
  x = 100
  yield x
end

proc = Proc.new { |a, b| puts "a is #{a.inspect} b is #{b.inspect}" }
foo(&proc)           #=> a is 100 b is nil

lambda_proc1 = lambda { |a| puts "a is #{a.inspect}" }
foo(&lambda_proc1)   #=> a is 100
lambda_proc2 = lambda { |a, b| puts "a is #{a.inspect} b is #{b.inspect}" }
foo(&lambda_proc2)   #=> ArgumentError: wrong number of arguments (1 for 2)

可见,proc 不会对参数进行个数匹配检查,而 lambda 会,如果不匹配还会报异常,所以安全起见,建议优先用 lambda。

2. return不同。lambdas的return是返回值给方法,方法会继续执行。 Proc的return会终止方法并返回得到的值。

还是例子说话

def foo
	f = Proc.new { return "return from foo from inside proc" }
	f.call # control leaves foo here
	return "return from foo"
end

def bar
  f = lambda { return "return from lambda" }
  puts f.call # control does not leave bar here
  return "return from bar"
end

puts foo  #=> return from foo from inside proc

puts bar  #=> return from lambda
          #=> return from bar

可见,proc 中,return 相当于执行了闭包环境里的 return,而 lambda 只是返回call 闭包所在环境。

为什么会有这样的不同?

答案在于procedures和methods概念上的不同。 Ruby中的Procs是代码片段(code snippets),不是方法。因此,Proc的return就是整个方法的return。 但lambdas就像是单独的methods(只不过是匿名的),所以它要检查参数个数,且不会覆盖整个方法的返回。 因此,最好把lambdas当作另一种methods的写法,一种匿名的方式。

总结

闭包是 Ruby 的强大特性,它的几种表达方式block,proc 和 lambda有细微差别。blocks和Procs看起来像在代码中插入代码片段。而lambdas和Methods看起来像方法。通过几个例子和比较,希望能了解如何灵活运用闭包,游刃有余!