Ruby 的块

块是 Ruby 程序员最喜欢的东西之一。它是一项非常强大的功能,使我们能够编写非常灵活的代码,拥有极高的可读性,并且可以在各处使用。本章中我们会详细为您讲解块的使用。

1. 什么是块

Ruby 的块(Block)的概念在其他语言中也被称为闭包。本质上,块与方法是相同的,除了它没有名称并且不属于对象。块是一段接受参数并返回值的代码并总是通过传递给方法来调用。

2. 如何创建一个块

块有两种表达形式,一种是 do...end,用来包含多行代码,另外一种是 {},只包含1行代码。

实例:

5.times do
  puts "Hello, This is from inside of block."
end

#---- 输出结果 ----
Hello, This is from inside of block.
Hello, This is from inside of block.
Hello, This is from inside of block.
Hello, This is from inside of block.
Hello, This is from inside of block.

再看另一种例子:

实例:

5.times { puts "Hello, This is from inside of block." }

#---- 输出结果 ----
Hello, This is from inside of block.
Hello, This is from inside of block.
Hello, This is from inside of block.
Hello, This is from inside of block.
Hello, This is from inside of block.

解释:这两种语句完全相同,它们的含义均是向 5times 实例方法中传递了一个块,而 times方法接收了块,并执行了里面的内容。由此可见,可以简单的将整个块理解成可以作为方法的一个特殊参数。

Tips:在 Ruby 社区中,惯例是块只有单行的时候使用花括号 {},每当块多于一行的时候,使用do..end。虽然花括号也可以接收多行参数。

3. 块如何接收参数

块经常与哈希、数组结合成迭代器Iterators)来使用。这里我们给出了一个块接收参数的例子。

实例:

[1, 2, 3, 4, 5].each do |number|
  puts "#{number} was passed to the block"
end

#---- 输出结果 ----
1 was passed to the block
2 was passed to the block
3 was passed to the block
4 was passed to the block
5 was passed to the block
[1, 2, 3, 4, 5].each { |number| puts "#{number} was passed to the block" }
 
#---- 输出结果 ----
1 was passed to the block
2 was passed to the block
3 was passed to the block
4 was passed to the block
5 was passed to the block

解释:上面两个例子是完全相同的,跟方法不同的是,number参数没有使用括号(())括起来,而是使用竖线(|)的形式列出。

在迭代器(Iterators)中,我们可以通过操作块来改变返回值。

实例:

[1, 2, 3, 4, 5].collect { |number| number + 1 }

#---- 输出结果 ----
[2, 3, 4, 5, 6]

解释:它调用原始数组上的collect方法,该方法为每个元素调用给定的块,并收集该块返回的每个返回值。然后,通过方法collect返回一个新的数组。

4. 创建一个带有块方法

4.1 隐形参数形式

这种形式我们会在方法后面传入一个块,用yield来调用块内的方法:

实例:

def sayHello 
  yield                                  # 块会在这里被调用
end
sayHello {puts 'Hello, This is a block'} # 传入块

#---- 输出结果 ----
Hello, This is a block

它实际上和下面这个代码是一致的:

def sayHello 
  puts 'Hello, This is a block'                      
end
sayHello

#---- 输出结果 ----
Hello, This is a block

块是可以多次调用的:

实例:

def sayHello 
  yield # 块会在这里被调用
  yield # 块会在这里第二次被调用
end
sayHello {puts 'Hello, This is a block'} # 传入块

#---- 输出结果 ----
Hello, This is a block
Hello, This is a block

注意事项:当没有块传入sayHello方法的时候,调用yield会抛出异常:

实例:

def sayHello 
  yield                        # 块会在这里被调用
end
sayHello                       # 不传入块

#---- 输出结果 ----
ruby.rb:2:in `sayHello': no block given (yield) (LocalJumpError)

所以,如果块是一个可选项,我们要使用block_given?方法,仅当块传入的时候调用。

def sayHello 
  yield if block_given?        # 块会在这里被调用
end
sayHello                       # 不传入块

# 不会抛出异常

4.2 显式参数形式

当我们显示声明块参数的时候,要使用&来标注哪个参数是块(&后面的参数名称是任意的。)

实例:

def sayHello &block
  block.call                             # 块会在这里通过call来调用
end
sayHello {puts 'Hello, This is a block'} # 传入块

#---- 输出结果 ----
Hello, This is a block

注意事项:显示声明块参数的时候,如果不传入块,block的值会成为nil,所以,这种时候如果块是可选项,我们可以通过增加判断block来忽略块调用部分代码。

def sayHello &block
  block.call if block           # 块未传入时不会调用block.call
end
sayHello                        # 传入块

# 不会抛出异常

注意事项:当方法有多个参数的时候,block的参数一定要放到最后。

def sayHello name, &block
  ...
end

4.3 如何调用含有参数的代码块œ

当我们使用隐形参数的yield来调用含有参数的代码块的时候我们直接将参数传入yield,类似方法的调用形式。同样,我们定义块的时候,也要设置接收这个参数。

def sayHello
  yield('Andrew')
end
sayHello { |name| puts "Hello, #{name}, This is block" }

#---- 输出结果 ----
Hello, Andrew, This is block

当我们使用显式参数的时候,调用的形态基本一样。

实例:

def sayHello &block
  block.call('Andrew')
end
sayHello { |name| puts "Hello, #{name}, This is block" }

#---- 输出结果 ----
Hello, Andrew, This is block

Tips : block.arity 返回块一共需要接收多少个参数,block.call 用来调用这个块。

5. 小结

本章节中我们了解了块,知道了可以通过{}创建单行的块,也可以通过do...end创建多行的块,我们在块中可以使用|参数|的形式接收传入参数。如果自己要定义带有块的方法的话,有隐形和显式两种执行块的方式,我们同样还可以向块中传入参数,形式和方法传参是一致的。