了解Ruby中的闭包

116 阅读5分钟

了解Ruby中的闭包

计算机科学中的闭包是指一段代码,它可以随身携带其创建环境。在Ruby中,闭包包括拥有与范围环境相关的变量的代码块或方法。这对所有的开发者来说都是一个敏感的话题,尤其是那些正在适应函数式范式的开发者。

前提条件

要继续学习本教程,必须具备以下条件。

  • 在你的电脑上安装了Ruby
  • 对Ruby编程有基本了解。
  • 使用交互式Ruby控制台的一些知识。

闭包

为了清楚地了解什么是闭包,我们需要了解first-class functionsfree variables ,和lexical environment

A first-class function 是一个方法,可以被当作一个 ,并作为一个 ,传递给另一个函数。object parameter

A free variable 是不在函数父级范围内声明的,但仍然可以在函数内被访问。

Lexical scoping 是指变量的可见性。 ,也称为 ,是指通过阅读代码来识别程序中的某个变量的能力。Lexical scope eyeball_scoping

在你的交互式控制台中试试这个。

parent_scope = "I'm available everywhere"

3.times do
  inner_scope = "Only accessed in the scope above: -"
  puts "#{inner_scope} #{parent_scope}}"
end

上面的代码演示了lexical scopinginner_scope 只在它被定义的块中可见。当你试图在该块之外访问它时,Ruby会抛出一个异常。

因此,我们可以将closures 定义为以后可以使用的代码块,并在创建它的环境中存储变量。

在确定一个闭包时,我们使用以下规则。

  • 它需要是一个函数。
  • 函数主体应该引用某个变量。
  • 该变量应该在父级范围内声明。

闭包的用例

  • 闭包可以被用来模拟 Ruby 中的类。
  • 闭包也有助于在Ruby中实现回调。

为了更好地理解我们的主题,让我们来看看Ruby块和可调用对象。

红宝石区块

Blocks 是用来捕获可以接受参数并在以后执行的代码。

在Ruby中,blocks 可以由curly bracesdo/end 关键字对划定。它们也可以作为匿名函数。

让我们来探讨一下yield 关键字和block_given?() 方法。了解这两个概念与闭包的关系是很重要的。

将行为封装到块中并将其传递到方法中是一种强大的编程技术。

yield 当定义在一个 内时,仅仅意味着 。block execute the block

def do_it
  yield
end

do_it { puts "I'm doing it" }

当你试图调用没有块的do_it 方法时,控制台会显示一个错误。

我们可以使用block_given?() 方法来捕获exception 。在这种情况下,只有当提供了一个块时,该函数才会被执行。

def do_it
  yield if block_given?
end

do_it

闭包和块之间的关系

在Ruby中,blocks ,作为匿名函数。

区块包含局部变量,消除了变量的串通。当一个人给全局变量的名字与块范围内的变量相同时,就会发生这种情况。

x = "Global variable"

1.times { x = "Block variable... conflicts and is modified" }
puts x #Block variable... conflicts and is modified

当你运行上面的片段时,你会得到一个意外的输出。这是因为我们在global scope 中分配了一个与block scope 中相同名称的变量。

我们可以在声明的块中提供一个参数来避免这个问题,如下所示。

x = "Global variable"

1.times { |;x| x = "Block parameter prevents variable overriding" }

puts x #Global variable

Procs

在介绍中,我们讨论了第一类函数。这些方法通常由procs 支持。

Procs是简单的可调用对象。一个块,你可以创建、存储并作为方法参数传递。它也像一个方法一样被执行。

Procs可以使用Proc#call(args),(args)(), 和lambdas 访问。

一个Proc对象在Proc 类的实例化时被创建。

pr = Proc.new { puts "Inside a Proc's block" }

当你使用pr.call 调用上述proc 时,该代码块成为proc 的主体,因此被执行。

Proc与lambdas有很多相似之处。然而,请注意,proc不一定是lambda。

我们使用lambda 关键字创建一个lambda函数,如下图所示。

lambda { |x, y| x + y }.call(x, y)

在Ruby中,lambdas也可以被定义为stabby lambdas。这一点在下面有说明。

->(x, y) { x + y }.call(x, y)

lambda和proc之间的区别

Arity

lambdas,不像proc,期望传递精确数量的参数。

l = lambda { |a, b| puts "x: #{a}, y: #{b}" } # number of args

p = proc { |a, b| puts "x: #{a}, y: #{b}" }

l.call("Ruby", "closures") #invoking object

p.call("Ruby", "closures")

当我们向proc 提供一个参数时,它将不会抛出一个异常,因为对参数没有任何限制。

不像procs,当参数没有正确插入时,lambdas会抛出一个异常。

返回语义

Procs 总是从它们的创建环境中返回,这可能是个问题。

Lambdas比procs更受欢迎,因为它们有和普通方法一样的行为模式。这一点在下面得到证明。

class ReturnSemantic
  def method_that_calls_proc_or_lambda(callable_object)
    puts "Calling #{proc_or_lambda(callable_object)}"
    callable_object.call
    puts "#{proc_or_lambda(callable_object)} gets called"
  end

  def proc_or_lambda(proc_like_thing)
    proc_like_thing.lambda? ? "Lambda" : "Proc"
  end
end

在上面的例子中,method_that_calls_proc_or_lambda() 负责传递一个可调用对象作为参数。它调用了可调用对象,因此,期望得到返回值。如果可调用对象是proc或lambda,从该方法返回的结果将有所不同。

proc_or_lambda() 使用一个三元操作符来识别传入的参数是proc还是lambda。

c = ReturnSemantic.new
c.method_that_calls_proc_or_lambda lambda { return }

当人们提供一个lambda作为参数时,该方法将返回puts 语句的最后一次执行。

当使用proc时,它会返回一个LocalJumpError ,如下图所示。

c = ReturnSemantic.new
c.method_that_calls_proc_or_lambda proc { return }

结论

闭包在开发者的手中确实很强大。人们可以使用这些组件在Ruby中编写功能性和可读的代码。