Ruby 中的块和产量

Posted

技术标签:

【中文标题】Ruby 中的块和产量【英文标题】:Blocks and yields in Ruby 【发布时间】:2011-03-05 05:56:33 【问题描述】:

我正在尝试了解块和 yield 以及它们在 Ruby 中的工作方式。

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

有人可以向我解释或告诉我去哪里了解它们吗?

【问题讨论】:

您可能对Ruby’s yield feature in relation to computer science 的答案感兴趣。虽然这个问题与你的问题有些不同,但它可能会对此事有所启发。 【参考方案1】:

是的,一开始有点疑惑。

在 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 (Interactive 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

【讨论】:

如果the_name = "",它如何打印racsO 对不起,名字是用"Oscar"初始化的实例变量(答案不是很清楚) 这样的代码呢? person.do_with_name |string| yield string, something_else 所以在 javascripty 术语中,它是一种将回调传递给给定方法并调用它的标准化方式。谢谢你的解释! @NickM 我删除了损坏的链接,并将所有示例放在答案底部的复制/粘贴准备好的表格中【参考方案2】:

关于产量,我想说明两点。首先,虽然这里的很多答案都讨论了将块传递给使用 yield 的方法的不同方法,但我们也来谈谈控制流。这一点尤其重要,因为您可以为一个块产生 MULTIPLE 时间。我们来看一个例子:

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.times 块。这发生了3次。因此,客户端代码中的该块在 3 次不同的情况下被调用,因为 yield 被显式地调用了 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" 

所以请注意,每次我们使用外部迭代器调用种类时,它只会调用一次 yield。下次我们调用它时,它会调用下一个 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 调用的某些对象,您必须明确说明您的枚举方法将是什么。

【讨论】:

【参考方案3】:

Yield 可以用作无名块以在方法中返回值。考虑以下代码:

Def Up(anarg)
  yield(anarg)
end

您可以创建一个分配了一个参数的方法“Up”。您现在可以将此参数分配给将调用并执行关联块的 yield。您可以在参数列表之后分配块。

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

Up方法调用yield时,带有参数,传递给block变量处理请求。

【讨论】:

【参考方案4】:

很可能有人会在这里提供真正详细的答案,但我一直发现 Robert Sosinski 的 this post 很好地解释了块、proc 和 lambda 之间的微妙之处。

我应该补充一点,我相信我链接到的帖子是特定于 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,所以上面可能有错误。

【讨论】:

那篇文章的描述很好,我花了几个月的时间才自己弄清楚 =) 我同意。在我阅读之前,我认为我不知道所解释的内容的一半。 更新后的链接现在也是404了。这是Wayback Machine link。 @klenwell 感谢提醒,我再次更新了链接。【参考方案5】:

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

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

基本语法

块是由 或 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 语句时,meditate 方法将控制权交给块,​​块内的代码被执行并将控制权返回给该方法,该方法在 yield 语句之后立即恢复执行。

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

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

【讨论】:

这是(唯一)让我真正了解什么是块和产量以及如何使用它们的答案。【参考方案6】:

在 Ruby 中,方法可以检查它们是否以除了普通参数之外还提供块的方式被调用。通常这是使用 block_given? 方法完成的,但您也可以通过在最终参数名称前添加一个 & 符号 (&amp;) 来将块称为显式 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 =)

【讨论】:

很高兴知道触发块的不同方法。【参考方案7】:

我有时会这样使用“yield”:

def add_to_http
   "http://#yield"
end

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

【讨论】:

好的,但是为什么呢?有很多原因,例如如果用户不需要,Logger 必须不执行某些任务。你应该解释你的...【参考方案8】:

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

不知道你来自什么语言,但假设它是一种静态语言,这种事情看起来很熟悉。这就是你在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();
    
  

忽略整个流链接的东西,想法是这样的

    初始化需要清理的资源 使用资源 一定要把它清理干净

这就是你在 ruby​​ 中的做法

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

完全不同。打破这个

    告诉 File 类如何初始化资源 告诉文件类如何处理它 笑那些还在打字的java人 ;-)

在这里,您基本上将其委托给另一个类,而不是处理第一步和第二步。如您所见,这极大地减少了您必须编写的代码量,这使事情更容易阅读,并减少了内存泄漏或文件锁未清除等事情的可能性。

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

这不是使用块的唯一方式,但其他方式(如 Builder 模式,您可以在 rails 中的 form_for api 中看到)非常相似,以至于一旦你把头包起来,应该很明显会发生什么围绕这个。当您看到块时,通常可以安全地假设方法调用是您想要执行的操作,并且块正在描述您想要如何执行它。

【讨论】:

让我们简化一下:File.readlines("readfile.rb").each_with_index do |line, index| puts "#index + 1: #line" end,然后对 Java 家伙笑得更厉害。 @MichaelHampton,在你阅读了几 GB 长的文件后大笑。 @akostadinov 不……这让我想哭! @MichaelHampton 或者,更好的是:IO.foreach('readfile.rb').each_with_index |line, index| puts "#index: #line" (加上没有内存问题)【参考方案9】:

我发现this article 非常有用。特别是下面的例子:

#!/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,正如您从source 中看到的那样,但功能基本相同,请参阅this question 进行讨论。)这为函数提供了一种执行某些操作的方法初始化然后从调用代码中获取输入,然后在需要时进行处理。

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

【讨论】:

【参考方案10】:

Yields,简单地说,就是允许你创建的方法来获取和调用块。 yield 关键字特别是块中的“东西”将被执行的地方。

【讨论】:

以上是关于Ruby 中的块和产量的主要内容,如果未能解决你的问题,请参考以下文章

帮助理解 Ruby 中的产量和枚举器

超级账本Fabric的块和交易大小

我可以将一个本身期望一个块的块传递给 ruby​​ 中的 instance_exec 吗?

如何在 CUDA 中自动计算 2D 图像的块和网格大小?

浅谈ruby中的block及yield

为啥我不能在 Ruby 中将块传递给 proc?