Ruby 中的闭包:Blocks、Procs 和 Lambdas
在《Ruby Magic》中,我们热衷于深入探究日常用品背后的奥秘,了解它们的工作原理。在本期,我们将探讨块、过程和 lambda 表达式之间的区别。
在具有“一等函数”的编程语言中,函数可以存储在变量中,并作为参数传递给其他函数。函数甚至可以使用其他函数作为其返回值。
闭包是具有环境的一等函数。环境是指向闭包创建时存在的变量的映射。闭包将保留对这些变量的访问权限,即使它们定义在另一个作用域中。
Ruby 没有“一等函数”,但它有闭包,形式包括块、过程 (proc) 和 lambda。块用于将代码块传递给方法,而过程和 lambda 允许将代码块存储在变量中。
区块
在 Ruby 中,代码块do
是可以创建并稍后执行的代码片段。代码块通过和关键字传递给生成代码块的方法end
。其中一个例子就是#each
,它循环遍历可枚举对象。
[1,2,3].each do |n|
puts "#{n}!"
end
[1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.
在此示例中,一个块被传递给Array#each
方法,该方法为数组中的每个项目运行该块并将其打印到控制台。
def each
i = 0
while i < size
yield at(i)
i += 1
end
end
在这个简化的示例中Array#each
,循环中调用while
,yield
对数组中的每个项执行传递的块。请注意,此方法没有参数,因为块是隐式传递给该方法的。
隐式块和yield
关键字
yield
在 Ruby 中,方法可以隐式或显式地接收块。隐式块传递通过在方法中调用关键字来实现。yield
关键字比较特殊,它会查找并调用传入的块,因此您无需将该块添加到方法接受的参数列表中。
由于 Ruby 允许隐式传递块,因此您可以使用块调用所有方法。如果没有调用yield
,则该块将被忽略。
irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]
如果被调用的方法确实产生结果,则会找到传递的块并使用传递给yield
关键字的任何参数进行调用。
def each
return to_enum(:each) unless block_given?
i = 0
while i < size
yield at(i)
i += 1
end
end
此示例返回一个Enumerator
除非给出块的实例。
yield
和关键字block_given?
用于在当前作用域内查找块。这允许隐式传递块,但由于块未存储在变量中,因此代码无法直接访问该块。
明确传递块
我们可以在方法中显式地接受一个块,方法是使用 & 符号参数(通常称为&block
)将其作为参数添加。由于该块现在是显式的,我们可以#call
直接在结果对象上调用该方法,而不必依赖于yield
。
该&block
参数不是适当的参数,因此使用块以外的任何其他方式调用此方法都会产生ArgumentError
。
def each_explicit(&block)
return to_enum(:each) unless block
i = 0
while i < size
block.call at(i)
i += 1
end
end
当一个块像这样传递并存储在变量中时,它会自动转换为proc。
进程
“proc” 是Proc
类的一个实例,它包含要执行的代码块,并且可以存储在变量中。要创建 proc,您需要调用Proc.new
并传递一个代码块。
proc = Proc.new { |n| puts "#{n}!" }
由于 proc 可以存储在变量中,因此它也可以像普通参数一样传递给方法。在这种情况下,我们不使用 & 符号,因为 proc 是显式传递的。
def run_proc_with_random_number(proc)
proc.call(random)
end
proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)
您无需创建一个 proc 并将其传递给方法,而是可以使用我们之前看到的 Ruby 的 & 符号参数语法并使用块。
def run_proc_with_random_number(&proc)
proc.call(random)
end
run_proc_with_random_number { |n| puts "#{n}!" }
注意方法中参数后面添加了“&”符号。这会将传递的块转换为 proc 对象,并将其存储在方法作用域内的变量中。
提示:虽然在某些情况下在方法中使用 proc 很有用,但将 block 转换为 proc 会对性能造成影响。请尽可能使用隐式 block。
#to_proc
符号、哈希值和方法可以通过使用它们的#to_proc
方法转换为 proc。最常见的用法是将由符号创建的 proc 传递给方法。
[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }
此示例展示了三种等效的调用#to_s
数组元素的方法。第一种方法传递一个以 & 符号为前缀的符号,通过调用其#to_proc
方法自动将其转换为过程。后两种方法展示了该过程的具体实现。
class Symbol
def to_proc
Proc.new { |i| i.send(self) }
end
end
虽然这是一个简化的示例,但 的实现Symbol#to_proc
展示了其底层实现。该方法返回一个 proc,该 proc 接受一个参数并将其发送self
给它。由于self
是此上下文中的符号,因此它调用该Integer#to_s
方法。
Lambda 表达式
Lambda 本质上是带有一些独特特征的 proc。它们在两个方面更像“常规”方法:它们强制在调用时传递参数的数量,并且使用“常规”返回值。
当调用一个没有参数的 lambda 时,或者将一个参数传递给一个不期望该参数的 lambda 时,Ruby 会抛出一个ArgumentError
.
irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
from (irb):8:in `block in irb_binding'
from (irb):8
from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'
此外,lambda 处理 return 关键字的方式与方法相同。调用 proc 时,程序会将控制权交给 proc 中的代码块。因此,如果 proc 返回,则当前作用域也会返回。如果在函数内部调用 proc 并调用return
,则该函数也会立即返回。
def return_from_proc
a = Proc.new { return 10 }.call
puts "This will never be printed."
end
此函数会将控制权交给 proc,因此当 proc 返回时,函数也会返回。在此示例中调用该函数不会打印输出,并且返回 10。
def return_from_lambda
a = lambda { return 10 }.call
puts "The lambda returned #{a}, and this will be printed."
end
使用 lambda 表达式时,它会被打印出来。调用return
lambda 表达式的行为类似于调用return
方法,因此a
变量会被填充10
,并且该行会被打印到控制台。
块、过程和 lambda
现在我们已经深入了解了这两个块、procs 和 lambda,让我们缩小范围并总结一下比较。
- 在 Ruby 中,块被广泛用于将代码片段传递给函数。通过使用
yield
关键字,可以隐式传递块,而无需将其转换为 proc。 - 当使用以 & 符号为前缀的参数时,将块传递给方法会在方法的上下文中生成一个 proc。proc 的行为类似于块,但可以存储在变量中。
- Lambdas 是行为类似于方法的 procs,这意味着它们强制执行元数并作为方法返回,而不是在其父范围内。
我们对 Ruby 中闭包的了解到此结束。关于闭包,还有很多内容需要学习,比如词法作用域和绑定,但我们会留到以后的文章中再讲。同时,请告诉我们您希望在以后的 Ruby Magic 文章中阅读哪些内容,是闭包还是其他内容。
文章来源:https://dev.to/appsignal/closures-in-ruby-blocks-procs-and-lambdas-2hk9