Ruby 匿名函数的使用详解

作者:袖梨 2022-06-25

Ruby 里面主要的可调用的对象是 Proc 对象,Lambdas,方法对象。Proc 是独立的代码序列,你可以创建,存储,可以作为方法的参数,你愿意的话,也可以使用 call 方法执行它。Lambdas 跟 Proc 对象很像,Lambda 其实就是 Proc 对象,不过稍有不同。

Proc 对象

用 Proc.new 创建一个 Proc 实例:

pr = Proc.new { puts "inside a proc's block" }
上面的代码块就是 Proc 的主体,调用 Proc 的时候会执行代码块里的东西:

pr.call
结果是:

inside a proc's block
给 proc 方法一个代码块,它会给你返回一个 Proc 对象。

proc { puts "hi!" }
Procs 与 Blocks

不是所有的代码块都跟 Proc 一样。

[1,2,3].each {|x| puts x * 10 }
上面用了个代码块,但是并没有创建一个 proc。

一个方法可以捕获一个代码块:

def call_a_proc(&block)
  block.call
end

call_a_proc { puts "I'm the block ... or Proc ... or someting." }
输出的是:

I'm the block ... or Proc ... or someting.
用 proc 也行:

p = Proc.new {|x| puts x.upcase}
%w{ Matt Damon }.each(&p)
输入的是:

MATT
DAMON
语法(blocks)与对象(procs)

Ruby 的代码块不是一个对象。

[1,2,3].each {|x| puts x * 10}
接收者是一个对象,代码块不是。代码块是调用方法语法的一部分。把代码块想成是一个参数列表。

puts c2f(100)
上面调用的方法里,参数是对象,但整体的参数列表并不是对象:(100)。没有 ArgumentList 类,也没有 CodeBlock 类。

block 与 proc 转换

def capture_block(&block)
  block.call
end

capture_block { puts "inside the block" }
上面隐式调用了 Proc.new,使用同样的区块。

解释:

1 调用 capture_block 方法,给它提供一个代码块:

capture_block { puts "inside the block" }
2 使用同样的区块创建了一个 Proc 对象:

Proc.new { puts "inside the block" }
3 block 参数绑定了 Proc 对象。

def capture_block(&block)
  puts "got block as proc"
  block.call
end
Proc 作为代码块

p = Proc.new { puts "this proc argument will serve as a code block" }
capture_block(&p)
输出的是:

this proc argument will serve as a code block
用 & 符号标记的 proc 会作为一个代码块,所以你就不能再给同一个方法提供一个代码块了。执行下面的代码会报错:

capture_block(&p) { puts "this is the explicit block" }
提示:“both block arg and actual block given”。Ruby 不能判断你到底想用 proc 还是 block 作为代码块。你只能选择一个。

&p 里的 & 是 to_proc 方法的包装。在 Proc 对象上调用 to_proc 会返回 Proc 对象本身。

你仍然需要 &,如果你想做:

capture_block(p)
或者:

capture_block(p.to_proc)
这样做你传递的只是一般的参数。没有让 proc 参数作为代码块。也就是说在 capture_block(&p) 里面,这个 & 有两个意思,它会触发在 p 上执行 to_proc 方法,还会让 Ruby 把执行了 to_proc 返回的 proc 对象当成是一个代码块。

Symbol#to_proc

>> %w{ a b }.map(&:capitalize)
=> ["A", "B"]
:capitalize 这个符号会被解释成发送给数组里面每个项目的信息。相当于:

%w{ a b }.map {|str| str.capitalize}
也相当于:

%w{ a b }.map {|str| str.send(:capitalize)}
可以去掉括号:

%w{ a b }.map &:capitalize
Procs 作为闭包

在方法主体里用的本地变量,跟调用方法的时候使用的本地变量不一样。

def talk
  a = "hello"
  puts a
end

a = "goodbye"
# 输出的是 hello
talk

# 输出的是 goodbye
puts a
a 这个标识符被分配使用了两次,不过两次分配之间没什么联系。

在代码块里使用已经存在的变量:

>> m = 10
=> 10
>> [1,2,3].each {|x| puts x * m}
10
20
30
=> [1, 2, 3]
multiply_by 返回了一个 proc,调用 multiply_by 方法的时候,传递给这个方法的参数,会保留在 proc 里面:

def multiply_by(m)
  Proc.new {|x| puts x * m}
end
mult = multiply_by(10)

# 输出 120
mult.call(12)
再做个实验,注意下面两个变量 a:

def call_some_proc(pr)
  a = "在方法作用域里的 'a'"
  puts a
  pr.call
end

a = "在 Proc 区块里使用的 'a'"
pr = Proc.new { puts a }
pr.call
call_some_proc(pr)
输出的结果是:

在 Proc 区块里使用的 'a'
在方法作用域里的 'a'
在 Proc 区块里使用的 'a'
Proc 对象会带着它的上下文。在上面的例子里,a 就是这个上下文里的一个部分,这个 a 会一直在 Proc 里存在。像这样的一块代码,一直带着它创建时的上下文,就是一个闭包(closure)。调用闭包,就会打开闭包,它里面会包含你创建它的时候放进去的东西。闭包会保留程序运行的部分状态。

创建一个闭包,有点像是打包行李,不管你在哪里打开这个行李包,它里面都会包含你打包的时候放进去的东西。
看一下计数器的例子,每次调用 proc 的时候它的变量的值都会增加一:

def make_counter
  n = 0
  return Proc.new { n += 1 }
end

c = make_counter
puts c.call
puts c.call
d = make_counter
puts d.call
puts c.call
输出的是:

1
2
1
3
Proc 参数

一个 Proc,带个区块,区块里有个参数:

pr = Proc.new {|x| puts "调用时的参数:#{x}" }
pr.call(100)
输出的是:

调用时的参数:100
Proc 的参数与方法处理参数的方式不一样。它不乎调用时使用的参数的数量是否正确。支持一个参数:

>> pr = Proc.new {|x| p x }
=> #
调用时不使用参数:

>> pr.call
nil
调用时使用了多个参数,只取第一个参数,扔掉剩下的:

>> pr.call(1,2,3)
1
用 lambda 与 -> 创建函数

lambda 方法返回一个 Proc 对象。 提供的代码块会成为函数的主体:

>> lam = lambda { puts "a lambda!" }
=> #
>> lam.call
a lambda!
=> nil
lambda 口味的 proc 与一般的 proc 有三个不一样的地方。lambda 需要显式创建,隐式创建的不会是 lambda,比如像这样:

def m(&block)
lambda 对待 return 关键词与普通的 proc 也不一样。

def return_test
  l = lambda { return }
  l.call
  puts "still here!"
  p = Proc.new { return }
  p.call
  puts "you won't see this message!"
end

return_test
输出的是  “still here! ”,不会看到第二条信息。因为调用 Proc 对象会触发从 return_test 那里返回。不过调用 lambda 触发的是从 lambda 主体的返回(退出),方法的执行仍会继续。

lambda 口味的 proc ,调用的时候要使用正常数量的参数:

>> lam = lambda {|x| p x}
=> #
>> lam.call(1)
1
=> 1
>> lam.call
ArgumentError: wrong number of arguments (given 0, expected 1)
 from (irb):105:in `block in irb_binding'
 from (irb):107
 from /usr/local/bin/irb:11:in `

'
>> lam.call(1,2,3)
ArgumentError: wrong number of arguments (given 3, expected 1)
 from (irb):105:in `block in irb_binding'
 from (irb):108
 from /usr/local/bin/irb:11:in `
'
->

>> lam = -> { puts "hi" }
=> #
>> lam.call
hi
=> nil
参数放到括号里:

>> mult = ->(x,y) { x * y }
=> #
>> mult.call(3,4)
=> 12
作为对象的方法

方法不是对象,不过你可以对象化(objectify)它们,让它们成为对象。

捕获方法对象

使用 method 方法,把方法的名字作为它的参数。

class C
  def talk
    puts "获取方法对象,self 是 #{self}。"
  end
end

c = C.new
meth = c.method(:talk)
meth 是方法对象,绑定了 c 对象的 talk 方法。调用它:

>> meth.call
获取方法对象,self 是 #
重新绑定:

class D < C
end

d = D.new
unbound = meth.unbind
unbound.bind(d).call
结果是:

获取方法对象,self 是 #
也可以这样重新绑定:

unbound = C.instance_method(:talk)

相关文章

精彩推荐