在Ruby中块和yield

我试图理解块和yield以及它们如何在Ruby中工作。

yield是如何使用的?我看过的许多Rails应用程序都以一种奇怪的方式使用yield

谁能给我解释一下或者告诉我怎么理解他们?

156314 次浏览

在Ruby中,方法可以检查调用方法时是否在正常参数之外提供了块。通常这是使用block_given?方法完成的,但你也可以通过在最终参数名称前加上一个&号(&)来将块引用为显式Proc。

如果用块调用方法,则该方法可以yield控制块(调用块),如果需要的话,使用一些参数。考虑这个示例方法,它演示了:

def foo(x)
puts "OK: called as foo(#{x.inspect})"
yield("A gift from foo!") if block_given?
end


foo(10)
# OK: called as foo(10)
foo(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as foo(123)
# BLOCK: A gift from foo! How nice =)

或者,使用特殊的块参数语法:

def bar(x, &block)
puts "OK: called as bar(#{x.inspect})"
block.call("A gift from bar!") if block
end


bar(10)
# OK: called as bar(10)
bar(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as bar(123)
# BLOCK: A gift from bar! How nice =)

很有可能有人会在这里提供一个真正详细的答案,但我总是发现Robert Sosinski的这篇文章是对block, procs &之间微妙关系的一个很好的解释;λ。

我应该补充一点,我所链接的这篇文章是专门针对ruby 1.8的。ruby 1.9中有一些变化,比如块变量是块的局部变量。在1.8中,你会得到如下内容:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Goodbye"

而1.9会给你:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Hello"

我在这台机器上没有1.9,所以上面可能有一个错误。

我想补充一下为什么你会这样做在已经很好的答案上。

不知道你来自什么语言,但假设它是静态语言,这类事情看起来会很熟悉。这是在java中读取文件的方法

public class FileInput {


public static void main(String[] args) {


File file = new File("C:\\MyFile.txt");
FileInputStream fis = null;
BufferedInputStream bis = null;
DataInputStream dis = null;


try {
fis = new FileInputStream(file);


// Here BufferedInputStream is added for fast reading.
bis = new BufferedInputStream(fis);
dis = new DataInputStream(bis);


// dis.available() returns 0 if the file does not have more lines.
while (dis.available() != 0) {


// this statement reads the line from the file and print it to
// the console.
System.out.println(dis.readLine());
}


// dispose all the resources after using them.
fis.close();
bis.close();
dis.close();


} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

忽略整个流链接的事情,想法是这样的

  1. 初始化需要清理的资源
  2. 使用资源
  3. 一定要清理干净

这是在ruby中如何做的

File.open("readfile.rb", "r") do |infile|
while (line = infile.gets)
puts "#{counter}: #{line}"
counter = counter + 1
end
end

完全不同。把这个分解一下

  1. 告诉File类如何初始化资源
  2. 告诉文件类如何处理它
  3. 嘲笑那些还在打字的Java家伙;-)

这里,不是处理第一步和第二步,而是将其委托给另一个类。正如您所看到的,这极大地减少了必须编写的代码量,从而使内容更易于阅读,并降低了内存泄漏或文件锁未清除等问题的可能性。

现在,并不是说你不能在java中做类似的事情,事实上,人们已经做了几十年了。它被称为策略模式。不同之处在于,如果没有块,对于像文件这样简单的例子,策略就会因为需要编写的类和方法的数量而变得多余。使用块,这是一种简单而优雅的方式,不以这种方式构建代码是没有任何意义的。

这并不是使用块的唯一方式,但是其他的(比如Builder模式,你可以在rails中的form_for api中看到)都非常相似,一旦你仔细思考,应该就会很清楚发生了什么。当您看到块时,通常可以安全地假设方法调用是您想要做的,并且块描述了您想要如何做。

是的,一开始有点令人费解。

在Ruby中,方法可以接收一个代码块,以便执行任意的代码段。

当一个方法需要一个块时,可以通过调用yield函数来调用它。

例子:

Person为例,它是一个具有name属性和do_with_name方法的类。当方法被调用时,它会将name属性传递给块。

class Person
def initialize( name )
@name = name
end


def do_with_name   # expects a block
yield( @name ) # invoke the block and pass the `@name` attribute
end
end

现在可以调用此方法并传递任意代码块。

person = Person.new("Oscar")


# Invoking the method passing a block to print the value
person.do_with_name do |value|
puts "Got: #{value}"
end

将打印:

Got: Oscar

注意,块接收一个名为value的变量作为参数。当代码调用yield时,它将@name的值作为参数传递。

yield( @name )

相同的方法可以用不同的块调用。

例如反转名称:

reversed_name = ""


# Invoke the method passing a different block
person.do_with_name do |value|
reversed_name = value.reverse
end


puts reversed_name


=> "racsO"

其他更有趣的现实生活例子:

数组中的筛选元素:

 days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]


# Select those which start with 'T'
days.select do | item |
item.match /^T/
end


=> ["Tuesday", "Thursday"]

或按名称长度排序:

 days.sort do |x,y|
x.size <=> y.size
end


=> ["Monday", "Friday", "Tuesday", "Thursday", "Wednesday"]

如果块是可选的,你可以使用:

yield(value) if block_given?

如果不是可选的,只需调用它。

你可以用irb (交互式Ruby Shell)在你的电脑上尝试这些例子。

以下是复制/粘贴表单中的所有示例:

class Person
def initialize( name )
@name = name
end


def do_with_name   # expects a block
yield( @name ) # invoke the block and pass the `@name` attribute
end
end




person = Person.new("Oscar")


# Invoking the method passing a block to print the value
person.do_with_name do |value|
puts "Got: #{value}"
end




reversed_name = ""


# Invoke the method passing a different block
person.do_with_name do |value|
reversed_name = value.reverse
end


puts reversed_name






# Filter elements in an array:
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]


# Select those which start with 'T'
days.select do | item |
item.match /^T/
end






# Sort by name length:
days.sort do |x,y|
x.size <=> y.size
end

简单地说,yield允许您创建的方法获取和调用块。yield关键字是块中'stuff'将被执行的位置。

我发现这篇文章非常有用。特别是下面的例子:

#!/usr/bin/ruby


def test
yield 5
puts "You are in the method test"
yield 100
end


test {|i| puts "You are in the block #{i}"}


test do |i|
puts "You are in the block #{i}"
end

它应该给出以下输出:

You are in the block 5
You are in the method test
You are in the block 100
You are in the block 5
You are in the method test
You are in the block 100

因此,本质上,每次调用yield时,ruby都会在do块或{}内部运行代码。如果一个参数被提供给yield,那么它将被作为参数提供给do块。

对我来说,这是我第一次真正理解do块在做什么。它基本上是函数访问内部数据结构的一种方式,无论是用于迭代还是用于函数配置。

所以在rails中,你可以这样写:

respond_to do |format|
format.html { render template: "my/view", layout: 'my_layout' }
end

这将运行respond_to函数,该函数生成带有(内部)format参数的do块。然后在这个内部变量上调用.html函数,该函数反过来生成运行render命令的代码块。注意,.html只会在请求文件格式时产生。(技术细节:这些函数实际上使用block.call,而不是yield,因为你可以从中看到,但功能本质上是相同的,参见这个问题进行讨论。)这为函数提供了一种方法,可以执行一些初始化,然后从调用代码获取输入,然后在需要时进行处理。

或者换句话说,它类似于一个函数以匿名函数作为参数,然后在javascript中调用它。

我有时会这样使用yield:

def add_to_http
"http://#{yield}"
end


puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}

在Ruby中,块基本上是可以传递给任何方法并由任何方法执行的代码块。块总是与方法一起使用,方法通常向它们提供数据(作为参数)。

块在Ruby宝石(包括Rails)和编写良好的Ruby代码中被广泛使用。它们不是对象,因此不能赋值给变量。

基本语法

block是由{}或do. end括起来的一段代码。按照惯例,花括号语法应该用于单行块,do. end语法应该用于多行块。

{ # This is a single line block }


do
# This is a multi-line block
end

任何方法都可以接收块作为隐式参数。块由方法中的yield语句执行。基本语法是:

def meditate
print "Today we will practice zazen"
yield # This indicates the method is expecting a block
end


# We are passing a block as an argument to the meditate method
meditate { print " for 40 minutes." }


Output:
Today we will practice zazen for 40 minutes.

当到达yield语句时,冥想方法将控制权交给块,执行块中的代码并将控制权返回给方法,该方法在yield语句之后立即恢复执行。

当一个方法包含yield语句时,它期望在调用时接收一个块。如果没有提供块,一旦到达yield语句,就会抛出异常。我们可以将block设置为可选的,并避免引发异常:

def meditate
puts "Today we will practice zazen."
yield if block_given?
end meditate


Output:
Today we will practice zazen.

不可能将多个块传递给一个方法。每个方法只能接收一个块。

详见:http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html

收益率可以用作在方法中返回值的无名块。考虑下面的代码:

Def Up(anarg)
yield(anarg)
end

您可以创建一个方法“Up”,它被分配了一个参数。你现在可以将这个参数赋值给yield,它将调用并执行一个相关的块。您可以在参数列表之后分配块。

Up("Here is a string"){|x| x.reverse!; puts(x)}

当Up方法调用带有参数的yield时,它被传递给块变量以处理请求。

关于收益率,我想说两点。首先,虽然这里有很多答案讨论了将块传递给使用yield的方法的不同方法,但我们也来讨论一下控制流。这是特别相关的,因为你可以多次屈服于一个块。让我们来看一个例子:

class Fruit
attr_accessor :kinds


def initialize
@kinds = %w(orange apple pear banana)
end


def each
puts 'inside each'
3.times { yield (@kinds.tap {|kinds| puts "selecting from #{kinds}"} ).sample }
end
end


f = Fruit.new
f.each do |kind|
puts 'inside block'
end


=> inside each
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block

当调用each方法时,它逐行执行。现在到3。Times块,该块将被调用3次。每次它都要求收益率。该yield链接到与调用each方法的方法相关联的块。需要注意的是,每次调用yield时,它都会将控制返回到客户端代码中each方法的块。一旦块完成执行,它就返回到3。*块。这发生了3次。因此,由于yield被显式地分别调用了3次,所以客户端代码中的块将在3个不同的场合被调用。

我的第二点是关于enum_for和yield。enum_for实例化Enumerator类,这个Enumerator对象也响应yield。

class Fruit
def initialize
@kinds = %w(orange apple)
end


def kinds
yield @kinds.shift
yield @kinds.shift
end
end


f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
=> "orange"
enum.next
=> "apple"

请注意,每次我们使用外部迭代器调用types时,它只会调用yield一次。下次我们调用它时,它会调用下一次收益率,以此类推。

关于enum_for有一个有趣的小细节。网上的文档说明如下:

enum_for(method = :each, *args) → enum
Creates a new Enumerator which will enumerate by calling method on obj, passing args if any.


str = "xyz"
enum = str.enum_for(:each_byte)
enum.each { |b| puts b }
# => 120
# => 121
# => 122

如果你没有指定一个符号作为enum_for的参数,ruby会将枚举器挂接到接收方的each方法上。有些类没有each方法,比如String类。

str = "I like fruit"
enum = str.to_enum
enum.next
=> NoMethodError: undefined method `each' for "I like fruit":String

因此,对于使用enum_for调用的某些对象,必须显式说明枚举方法是什么。