Ruby 中的闭包-代码块

在许多编程语言中都会有闭包这个概念。今天主要来谈谈Ruby中的闭包,它在这门语言中地位如何,以什么形式存在,主要用途有哪些?node

闭包概要

维基百科里对闭包地解释以下git

In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.
复制代码

看起来很复杂是吧?其实我也看不太懂,建议英文很差的人仍是学我去看中文版。通俗来说,闭包就是一个函数,它能够跟外部做用域打交道,访问并修改外部做用域的变量。在咱们所熟悉的JavaScript这门语言中,全部的函数/方法都是闭包。github

let a = 1;
function add_a() {
  return a += 1;
}

console.log(a)
console.log(add_a())
console.log(add_a())
console.log(add_a())
console.log(a)
复制代码

结果以下编程

1
2
3
4
4
复制代码

可见,函数add_a能够自由访问外部做用域的变量a,而且可以在函数调用过程当中持续维护着变量的值。这种特性为函数的柯里化带来了可能。c#

function add(a, b) {
 return a + b
}

console.log(add(1,2)) // => 3
复制代码

柯里化以后可得api

function add(a) {
  return function(b) {
    return a + b
  }
}

console.log(add(1)(2)) // => 3
复制代码

得益于闭包的特性,上述两个函数虽然调用方式不一样,不过它们所完成的工做是等价的,咱们能够借此写出许多有趣的代码。然而闭包若是使用不当,或许会不当心修改了不该该修改的外部变量,特别是这些外部变量被多个程序单元共享的时候,可能会引起意想不到的系统问题。Ruby在设计的时候也考虑到了这种问题,因而只要是经过def定义的方法,它都会建立一个封闭的词法做用域,在该做用域内不可访问外部的局部变量,外部信息只可以经过参数的形式传入到函数中。好比,下面这个代码片断是会报错的ruby

a = 100

def add_a
  a = a + 1
end

puts add_a

复制代码
Traceback (most recent call last):
	1: from a.rb:7:in `<main>' a.rb:4:in `add_a': undefined local variable or method `a' for main:Object (NameError) 复制代码

然而,若是没有闭包,Ruby这门语言所可以提供的灵活性就颇有限了。Matz也考虑到了这点,Ruby中并非没有闭包,它只是以另外一种方式来展示--代码块。代码块也是Ruby元编程的重点内容,接下来咱们以代码块的形式来从新定义add_a方法。bash

a = 100

define_method :add_a do
  a = a + 1
end

puts add_a # => 101
puts add_a # => 102
puts add_a # => 103
复制代码

该例子中采用了define_method搭配代码块来定义方法,使得咱们能够在函数体的中访问外部做用域的局部变量a,使得该函数可以达到咱们预期的效果。一个方法的定义,是否要造成封闭的做用域,不一样的语言可能会有不一样的权衡,Ruby特意采用了代码块来表示闭包,有别于通常的方法定义,为这门语言增添了很多色彩。闭包

PS: 固然形如@xxxx的实例变量便不受这种封闭做用域的制约。由于实例变量自己就是实例上下文共享的。app

回调

在编程世界中,咱们简单地称可以做为某个函数的参数,而且可以在该函数内部被调用的函数为回调函数。许多人听到回调函数就会想到回调地狱,然而我的以为只要设计得当,并非全部回调都会沦为地狱。在Ruby中几乎每个经过def关键字定义地方法都默认接收一个代码块来做为回调函数,一般这个默认的回调函数参数并不须要显式声明,考虑如下代码片断

def print_message(message)
  yield(message) if block_given?
  puts 'The End!!'
end

print_message('Hello World') do |message|
  puts "I will print the message #{message}"
end
复制代码

结果以下

I will print the message Hello World
The End!!
复制代码

好玩吧,咱们能够在调用方法的时候,在末尾以代码块的形式来定义回调逻辑。在被调用的方法的内部,经过block_given?来判断是否有代码块传入,若是有须要则经过关键字yield来运行对应的代码块,并传入相关的参数。这种以代码块做为回调的方式,为编码带来了必定的灵活性。然而或许在一些场景中这种隐式接收代码块的方式并非那么直观,咱们也能够显式地去声明这个参数。

def print_message(message, &block)
  block.call(message) if block_given?
  puts "The End!!"
end

print_message('Hello World') do |message|
  puts "I will print the message #{message}"
end
复制代码

只是在这种场景中对应的参数&block会把咱们传入的代码块转换成Proc对象,因而在这个例子中须要经过Proc#call方法来运行对应的代码块,而再也不用yield关键字了。固然咱们也能够直接往被调用的方法中传入一个Proc的对象

callback = Proc.new do |message|
  puts "I will print the message #{message}"
end

def print_message(message, &block)
  block.call(message) if block_given?
  puts "The End!!"
end

print_message('Hello World', &callback)
复制代码

打印结果都是同样的

I will print the message Hello World
The End!!
复制代码

以上两种代码块充当回调的方式,是Ruby中编码的经常使用手段。回调逻辑始终做为“最后一个参数”传入到被调用的方法中去,这或许也是一种约定优于配置的表现吧。

代码块的“意义”

刚开始接触Ruby的时候,我总以为代码块是一个反人类的设计,明明就是一个闭包,为什么要设计得这么异类。更奇怪的是,许多业内人士都以为代码块是Ruby最伟大的发明之一。后来接触多了渐渐也就习惯了,代码块的优雅配合上其闭包的特性,再加上上面所说的一些回调的相关约定,为Ruby这门语言增色很多。代码块有两种表达方式{ .... }do ... end。通常对于单行的代码块会采用第一种形式,对于多行的代码块会采用第二种形式。在Ruby的开源世界中,代码块几乎无处不在,下面咱们来看一些常见的案例

1. 容错设计

Ruby承袭于Lisp,代码块的运行会自动返回最后一条语句或者表达式的值,因而有些库也考虑到了用代码块来进行容错处理。就拿Hash的实例来做作个例子,咱们但愿当Hash实例对应的键值对不存在的时候给它一个默认值,常见的作法是

> hash = {}
> value = hash['a'] ? 'default value' : hash['a']
 => "default value"
复制代码

熟悉JavaScript的人应该对上面这种代码不陌生,真所谓是啰嗦至极。为了使代码更加优雅咱们能够采用Hash#fetch接口来取值,当Hash#fetch接口找不到对应的键值对的时候就会触发异常

> hash = {}
> hash.fetch('a')
Traceback (most recent call last):
        3: from /Users/lan/.rvm/rubies/ruby-2.5.3/bin/irb:11:in `<main>' 2: from (irb):12 1: from (irb):12:in `fetch' KeyError (key not found: "a") 复制代码

这个时候咱们能够采用代码块来作容错,当找不到对应键的时候为取值操做提供一个默认值

> value = hash.fetch('a') { 'default value' }
 => "default value"
复制代码

相对于第一种方式第二种方式更加优雅,也更有Ruby味一些。虽然说计算机世界是由0,1组成的,非此即彼。可是Ruby社区并不崇尚Python社区的绝对正确,每一个人的偏向不一样,咱们能够选择本身喜欢的方式去完成工做。

2. DSL

另一个代码块用得比较普遍的地方应该就是DSL了,许多优秀的Ruby开源项目都会有相应的DSL。下面是Ruby模板渲染库RABL的配置代码

Rabl.configure do |config|
  # Enabling cache_all_output will cause an orders cache entry to be used in all templates
  # matching orders.cache_key, which results in unexpected behavior on Spree api response.
  # For more about this option, see https://github.com/nesquena/rabl/issues/281#issuecomment-6780104
  config.cache_all_output = false
  config.cache_sources = !Rails.env.development?
  config.view_paths = [Rails.root.join('app/views/')]
end
复制代码

这是一个简单的DSL,经过暴露模块内部的config实例,而后在调用者的上下文中去配置实例相关的属性。这里的代码块其实也充当了回调函数的角色,它让咱们的配置逻辑能够被统一规划到一个区间当中,不然的话可能你得写出相似这样的配置代码

config = Rabl::ConfigureXXXX.new
config.cache_all_output = false
config.cache_sources = !Rails.env.development?
config.view_paths = [Rails.root.join('app/views/')]
复制代码

不管怎么看都是DSL的方式比较优雅对吧?相似的DSL还有不少,这里不一一举例了,这些DSL如何去实现也不在本篇文章的讨论范围内。

3. 蹩脚的函数

代码块能够被当作是一个“蹩脚”的函数,虽然说通常状况下它能够做为某个方法的回调,可是它不像JavaScript中的函数那样能够独立存在,它必需要依赖其余的机制。当咱们要用代码块去定义一个匿名函数时,须要搭配lambda关键字或者Proc类来实现

> c = lambda() {}
 => #<Proc:0x00007ff47b8546a8@(irb):6 (lambda)>
> c.class
 => Proc

> (lambda() { 'hello' }).call
 => "hello"
> (lambda() { 'hello' })[]
 => "hello"
> (Proc.new { 'hello' }).call
 => "hello"
> (Proc.new { 'hello' })[]
 => "hello"
复制代码

以上都是经常使用的定义匿名函数的方式,本质上它们都是Proc类的实例,须要显式地利用Proc#call方法或者语法糖[]来调用它们。

尾声

这篇文章简单地介绍了一下闭包的概念,闭包跟通常封闭做用域的方法有何不一样之处。区别于通常的方法,闭包在Ruby中以代码块的形式出现,它在Ruby世界中几乎无处不在,充当了一等公民。这种区分,不只使咱们的Ruby代码更加优雅,增添了可读性,还使得咱们的编码过程更加简单。

相关文章
相关标签/搜索