Ruby 中 Block、Proc 和 Lambda 的区别是什么?

2013年8月5日

Block、Proc 和 Lambda 是什么?

程序员的说法: Ruby 中 closures 的例子。 通俗易懂的说法: 对我们想要运行的代码进行分组的方式。

# Block 示例
[1,2,3].each { |x| puts x*2 }  # block 在花括号之间
[1,2,3].each do |x|
 puts x*2          # block 是 do 和 end 之间的所有内容
end

# Proc 示例       
p = Proc.new { |x| puts x*2 }  
[1,2,3].each(&p)       # '&' 告诉 Ruby 将 proc 转换为 block
proc = Proc.new { puts "Hello World" }
proc.call           # Proc 对象的代码体在被调用时执行

# Lambda 示例      
lam = lambda { |x| puts x*2 }  
[1,2,3].each(&lam) 
lam = lambda { puts "Hello World" }
lam.call

虽然它们看起来非常相似,但存在一些细微的差异,我将在下面介绍。

Block 和 Proc 之间的区别

1. Proc 是对象,而 Block 不是

Proc (注意小写的 p) 是 Proc 类的一个实例。

p = Proc.new { puts "Hello World" }

这使我们可以在它上面调用方法并将其分配给变量。 Proc 也可以返回自身。

p.call # 打印 'Hello World'
p.class # 返回 'Proc'
a = p  # a 现在等于 p,一个 Proc 实例
p    # 返回一个 proc 对象 '#<Proc:0x007f96b1a60eb0@(irb):46>'

相比之下,block 只是方法调用 语法 的一部分。它本身没有任何意义,只能出现在参数列表中。

{ puts "Hello World"}    # 语法错误 
a = { puts "Hello World"}  # 语法错误
[1,2,3].each {|x| puts x*2} # 仅作为方法调用语法的一部分有效

2. 参数列表中最多只能出现一个 Block

相反,您可以将多个 Proc 传递给方法。

def multiple_procs(proc1, proc2)
 proc1.call
 proc2.call
end

a = Proc.new { puts "First proc" }
b = Proc.new { puts "Second proc" }
multiple_procs(a,b)

Proc 和 Lambda 之间的区别

在深入探讨 Proc 和 Lambda 之间的区别之前,重要的是要提到它们都是 Proc 对象。

proc = Proc.new { puts "Hello world" }
lam = lambda { puts "Hello World" }
proc.class # 返回 'Proc'
lam.class # 返回 'Proc'

但是,Lambda 是 Proc 的一种不同的“风格”。当返回对象时,这种细微的差异会显示出来。

proc  # 返回 '#<Proc:0x007f96b1032d30@(irb):75>'
lam  # 返回 '<Proc:0x007f96b1b41938@(irb):76 (lambda)>'

(lambda) 符号提醒我们,虽然 Proc 和 Lambda 非常相似,甚至是 Proc 类的实例,但它们也略有不同。 以下是主要区别。

1. Lambda 检查参数的数量,而 Proc 不检查

lam = lambda { |x| puts x }  # 创建一个接受 1 个参数的 lambda
lam.call(2)          # 打印 2
lam.call            # ArgumentError: wrong number of arguments (0 for 1)
lam.call(1,2,3)        # ArgumentError: wrong number of arguments (3 for 1)

相比之下,如果传递了错误的参数数量,Proc 并不在意。

proc = Proc.new { |x| puts x } # 创建一个接受 1 个参数的 proc
proc.call(2)          # 打印 2
proc.call           # 返回 nil
proc.call(1,2,3)        # 打印 1 并忽略额外的参数

如上所示,如果传递了错误的参数数量,Proc 不会出错并引发错误。 如果 proc 需要一个参数但没有传递参数,则 proc 返回 nil。 如果传递了太多的参数,则它会忽略额外的参数。

2. Lambda 和 Proc 对 'return' 关键字的处理方式不同

Lambda 中的 'return' 触发 lambda 代码之外的代码

def lambda_test
 lam = lambda { return }
 lam.call
 puts "Hello world"
end

lambda_test         # 调用 lambda_test 打印 'Hello World'

Proc 中的 'return' 触发执行 Proc 的方法之外的代码

def proc_test
 proc = Proc.new { return }
 proc.call
 puts "Hello world"
end

proc_test         # 调用 proc_test 不打印任何内容

那么什么是 Closure?

程序员的说法: '一个函数或对一个函数的引用,以及一个引用环境。 与普通函数不同,即使在词法作用域之外调用函数,Closure 允许函数访问非本地变量。' - Wikipedia

通俗易懂的说法: 类似于手提箱,它是一组代码,当打开它时(即调用),包含打包它时(即创建它时)其中的任何东西。

# Proc 对象保留本地上下文的示例
def counter
 n = 0
 return Proc.new { n+= 1 }
end

a = counter         
a.call      # 返回 1
a.call      # 返回 2

b = counter 
b.call      # 返回 1
a.call      # 返回 3

背景知识 第 1 部分:Lambda 演算和匿名函数

Lambda 的名称来自 1930 年代引入的一种演算,旨在帮助研究数学的基础。 Lambda 演算通过简化其语义来帮助使可计算函数更易于研究。 其中最相关的简化是它以“匿名”方式处理函数,这意味着没有为函数提供显式名称。

sqsum(x,y) = x*x + y*y #<-- 普通函数
(x,y) -> x*x + y*y #<-- 匿名函数

一般来说,在编程中,术语 lambda 指的是匿名函数。 这些匿名函数在某些语言(即 JavaScript)中非常常见且明确,而在其他语言(即 Ruby)中则隐式。

背景知识 第 2 部分:Proc 名称的由来

Proc 是 procedure(过程)的缩写,procedure 是一组打包在一起的指令,用于执行特定任务。 在不同的语言中,这些指令可能被称为函数、例程、方法或通用术语“可调用单元”。 它们通常被设计为多次调用和从程序中的多个位置调用。

总结差异

  1. Proc 是对象,而 Block 不是
  2. 参数列表中最多只能出现一个 Block
  3. Lambda 检查参数的数量,而 Proc 不检查
  4. Lambda 和 Proc 对 'return' 关键字的处理方式不同