除非你从其他语言中理解了 "闭包 "的概念,否则在Ruby中,它最初可能会让你感到困惑。"闭包",除了Ruby(和一些函数式编程语言)外,是一个不合时宜的概念。
下面是Merriam-Webster对闭包的定义。
closure名词 clo-sure \ ˈklō-zhər \ 古代:包围的手段:包围物
这里也是一样。一个Ruby闭包 "包围了一堆要执行的代码"。这是一个 "封套"。根据维基百科,闭包是一种技术,用于在具有一级函数的语言中实现词义范围的名称绑定。
考虑一下这个片段:
2.times do
topic = 'Ruby closures'
puts "An adventure with #{topic}."
end
#=> An adventure with Ruby closures.
#=> An adventure with Ruby closures.
#=> 2
我们在do 和end 之间 "封闭 "了topic 和puts 。该区域外的任何东西都不能访问topic,但puts 方法可以,因为它与变量赋值(名称绑定)处于同一环境中(词义范围)。这种结合方法和变量的技术我们称之为闭合。注意: { ... } 和do ... end 在传递给一个方法时是等价的。我将会交替使用它们。
这段代码如何呢?
lam = lambda do |topic|
puts "An adventure with #{topic}."
end
lam.call('Ruby closures')
#=> An adventure with Ruby closures
如果你仔细看,它做的事情和前面的片段类似。现在,最后一段呢?
prok = Proc.new do |topic|
puts "An adventure with #{topic}."
end
prok.call('Ruby closures')
#=> An adventure with Ruby closures.
这些形式代表了在Ruby中封闭代码的方法。我们已经看到一个块、lambda和一个Proc分别表示为。{},lambda {} 和Proc.new {} 在前面的代码片断中。这些都是Ruby中的闭包。在这三个代码中,我们都设置了一个局部变量topic ,并将其传递给一个局部方法puts 。你可能会想,我们在lam 和prok 中设置了topic 这个变量。我们没有像在第一个代码段中那样明确地将其设置为topic = "Ruby closures" 。原因是;Proc 和lambda 的#call 方法为我们做了这个。如果你看一下Proc#call 的文档,我们有。
call(params, ...) → obj 调用块,将块的参数设置为params中的值。
关于Ruby块(作为闭包),一个值得注意的细微差别是,在Ruby中,块不能被表示为对象,它们只是提供了一个简单的方法来分组代码,但你不能将它们绑定到变量。就其本身而言,它们是非常无用的。另一方面,Procs和Lambdas则是一些人所说的一等公民。
区块
我同意被纠正,但正如我之前提到的,块本身并没有什么意义。它们通常作为参数传递给方法,但它们也能帮助我们对代码进行分组以修改其行为。这里有一个例子:
(1..10).each { |v| puts v if v.even? }
# Output: #
# 2
# 4
# 6
# 8
# 10
很容易看出,我们在迭代一个Range并打印出所有的偶数。现在,如果我们想同时打印出所有偶数和质数的数字呢?这时,区块就显示出它们的用处了。我们只需要修改区块中的代码就可以做到这一点了:
require 'prime'
(1..20).each { |v| puts v if v.even? && v.prime? }
# => 2
我们只是通过对块中的内容进行分组来改变代码的行为。我们不得不要求Prime 标准库来利用prime? 方法。如果我们愿意的话,我们也可以实现我们挑选素数的方法。我们已经看到了我们如何使用Ruby块来分组我们的代码来列举。但在这篇文章中,我们要讨论的是闭包。你已经看到了上面的代码块是如何通过将1-20范围内的每个数字分配给变量v ,从而起到封闭作用的吗?如果没有,那么这里的代码如何呢?
def shout(word, position)
position.times { |x| puts "#{ x }: #{ word }"}
end
shout("SHOUTING!", 2)
# Output: #
# 0: SHOUTING!
# 1: SHOUTING!
在这个代码中,我们将word 变量分配给字符串SHOUTING! ,同时将第二个参数,一个整数,分配给变量position ,并使用x 代表从0到position 的每个数字,以表示我们希望字符串打印的次数。请牢记闭合的定义。最后的代码是否满足它?
现在你对块有了一些了解,但在目前展示的例子中,我们只是用Ruby块来封装行为,并将它们传递给Ruby的方法。我们也可以写我们的方法,接受块。
在Ruby中,我们被赋予了yield 和block_given? 方法。这些方法也能帮助我们创建接受块的方法。只要你在Ruby中看到yield ,就把它翻译成 "嘿,做传来的块说的任何事情"。block_given? 是非常直观的。这个方法简单地返回一个布尔值,表示是否有一个块被传递。这里有一个简单的例子来说明这个概念:
def shout
yield
end
# Call the #shout method with a block.
shout { puts "SHOUTING!" }
# => SHOUTING!
试试上面的方法,调用yield 两次或三次,看看会发生什么。这有什么意义吗?如果你在调用这个方法时没有给它传递一个块,你就会看到
没有给定块(yield)(LocalJumpError)。
你可以试试。而这正是block_given? 的用武之地。这里是你如何利用block_given? 方法的:
def shout
block_given? ? yield : puts("Sir, you didn't pass any block.")
end
# Call the #shout method without a block.
shout
# => Sir, you didn't pass any block.
这几乎就是它的全部内容了!但是你可能想知道这些东西的实际用途是什么。漂亮的对象初始化!就像这样:
module Genus
class Species
attr_accessor :name, :height, :weight, :ethnicity
def initialize
yield self if block_given?
end
end
end
human = Genus::Species.new do |characteristics|
characteristics.name = "Emmanuel Hayford"
characteristics.height = 188 # cm
characteristics.weight = 104 # kg
characteristics.ethnicity = "black"
end
p human.ethnicity
# => "black"
我们正在将self (这是一个实例化的物种类的对象)设置为characteristics 变量。
Procs
尽管Ruby中的所有东西都是一个对象,但诸如块和方法这样的结构体却不是。然而,我们可以用Procs将它们变成对象。所以大部分Procs是表示为对象的块,你可以调用方法或分配给内存。
让我们重构我们的第一个例子,使用Proc
所以我们将这段代码:
2.times do
topic = 'Ruby closures'
puts "An adventure with #{topic}."
end
改为这样:
prok = Proc.new {
topic = 'Ruby closures'
puts "An adventure with #{topic}."
}
# `Proc.new` is equivalent to `proc`
2.times do
prok.call
end
# => An adventure with Ruby closures.
# => An adventure with Ruby closures.
因为proc 现在是一个对象,你可以对它调用call 方法来返回块。我们可以进一步重构,使块或Proc对象接受一个参数,使其更加动态:
prok = Proc.new { |topic|
puts "An adventure with #{topic}."
}
2.times do
prok.call('Ruby closures')
end
# => An adventure with Ruby closures.
# => An adventure with Ruby closures.
这里有一些有趣的东西;对于一个需要一个参数的Proc对象,比如说,你可以用几种不同的方式来调用它。Ruby允许我们省略对call 的调用。因此上面代码的第6行相当于下面的内容。
prok.('Ruby closures') 和 。prok === 'Ruby closures'
然而,如果你想处理多个参数,你可以将你的参数作为一个数组的元素传递给Proc对象。因此,让我们假设这是我们的代码:
prok = Proc.new { |x, y| puts x + y }
下面所有调用prok 的方法都是有效的:
prok.call [7,3]
prok === [3, 7]
prok[6, 4]
# => 10
# => 10
# => 10
以上所有的方法对lambdas都是有效的。
Lambdas。
兰姆达是尊重arity的Proc对象,对待return 关键字的方式有点不同。
下面是值得注意的关于Proc和Lambdas的行为。请考虑以下情况:
cool_lambda = lambda { |first_name, nationality| puts "#{first_name} is #{nationality}."}
cool_proc = proc { |first_name, nationality| puts "#{first_name} is #{nationality}."}
cool_lambda.call("Matz", "Japanese")
cool_proc.call("Aaron", "American")
一切按预期进行。cool_lambda'的签名接受两个参数,它对此很严格,如果你多传一个或少传一个参数,它就会爆炸,比如:
错误的参数数(给定3个,预期2个)(ArgumentError)。
cool_proc 另一方面,它将只做区块指定的事情,默默地忽略额外的参数或没有参数。
下面是关于return 关键字与闭包的情况:
class ClosureExperiment
def call_closure(closure)
puts "Calling #{ check_closure_type(closure) }."
closure.call
puts "#{ check_closure_type(closure) } got called!"
end
def check_closure_type(unknown_closure)
unknown_closure.lambda? ? "Lambda" : "Proc"
end
end
closure = ClosureExperiment.new.call_closure(lambda { return })
# => Calling Lambda.
# => Lambda got called!
closure = ClosureExperiment.new.call_closure(proc { return })
# => Calling Proc.
从上面可以看出,当Procs看到return ,就会立即弹出,而Lambda则犹豫不决,直到其上下文中的所有内容都被执行。
这两个闭包之间还有一个区别是,当Lambda定义了一个默认参数并传递了一个数组时,它总是返回传递给它的默认参数,而Proc会忽略它,除非没有额外的参数,那么默认参数就会像下面这样实现:
lam = lambda { |a, b = :foo| p a, b }.call([1,3,5])
# => [[1, 3, 5], :foo]
prok = proc { |a, b = :foo| p a, b }.call([1,2,3])
# => [1, 2]
prok = proc { |a, b = :foo| p a, b }.call([1])
# => [1, :foo]
这就是Proc和Lambdas的不同之处了。
最后,我喜欢把Procs看作是以对象形式表示的块,而Lambdas是Procs的一个变体,当你传入错误的参数数量时,会引发错误。