Ruby中的块和收益


275

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

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

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


2
您可能对Ruby与计算机科学相关的yield特性的答案感兴趣。尽管这是一个与您的问题有所不同的问题,但可能可以使您对此有所了解。
肯·布鲁姆

Answers:


393

是的,起初有点令人困惑。

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

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

例如,这对于遍历列表或提供自定义算法非常方便。

请看以下示例:

我将定义一个Person用名称初始化的类,并提供一个do_with_name方法,该方法在调用时将name属性传递给接收到的块。

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

    def do_with_name 
        yield( @name ) 
    end
end

这将允许我们调用该方法并传递任意代码块。

例如,要打印名称,我们将执行以下操作:

person = Person.new("Oscar")

#invoking the method passing a block
person.do_with_name do |name|
    puts "Hey, his name is #{name}"
end

将打印:

Hey, his name is Oscar

注意,该块接收一个称为变量的变量name(注意,您可以随心所欲地调用此变量,但是调用它很有意义name)。在代码调用时,yield它将用的值填充此参数@name

yield( @name )

我们可以提供另一个块来执行不同的操作。例如,反转名称:

#variable to hold the name reversed
reversed_name = ""

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

puts reversed_name

=> "racsO"

我们使用了完全相同的方法(do_with_name)-这只是一个不同的块。

这个例子很简单。更有趣的用法是过滤数组中的所有元素:

 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?

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

编辑

@hmak创建了这些示例的repl.it:https://repl.it/@makstaks/blocksandyieldsrubyexample


它是如何打印racsO如果 the_name = ""
Paritosh Piplewar

2
抱歉,名称是实例变量初始化的"Oscar" (答案中不是很清楚)
OscarRyz 2012年

像这样的代码呢?person.do_with_name {|string| yield string, something_else }
f.ardelian

7
因此,就Java语言而言,这是将回调传递给给定方法并对其进行调用的一种标准化方法。感谢您的解释!
yitznewton 2013年

从更一般的角度讲,块是用于“策略”模式的红宝石“增强”语法糖。因为典型用法是提供代码以在其他操作的上下文中执行某些操作。但是,红宝石增强功能为诸如通过块传递上下文来编写DSL的DSL之类的凉爽事物提供了一种途径
Roman Bulgakov

25

在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 =)

很高兴知道触发块的不同方法。
平平

22

很有可能有人会在这里提供一个真正详细的答案,但是我一直发现Robert Sosinski的这篇文章很好地解释了块,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,所以上面可能有错误。


在该文章中进行了很好的描述,我花了几个月的时间才弄清楚自己全都=)
maerics 2010年

我同意。在阅读之前,我认为我不了解其中一半的内容。
theIV 2010年

现在,更新的链接也是404。这是Wayback Machine链接
klenwell '16

@klenwell感谢您的注意,我再次更新了链接。
theIV 2016年

13

我想补充一下为什么您会这样处理已经很不错的答案。

不知道您来自哪种语言,但是假设它是静态语言,这种情况看起来会很熟悉。这是您在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. 确保将其清理干净

这就是你在红宝石中做的方式

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中看到)足够相似,一旦将其包裹起来,应该很明显会发生什么。当您看到块时,通常可以放心地假设方法调用是您要执行的操作,而该块正在描述您要执行的操作。


5
让我们简化一下:File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" end更嘲笑Java专家。
迈克尔·汉普顿

1
@MichaelHampton,您读了一个千兆字节的文件后笑了。
akostadinov

@akostadinov不,这让我想哭!
迈克尔·汉普顿

3
@MichaelHampton或者,更好的是:(IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }再加上没有记忆问题)
基金莫妮卡的诉讼

12

我发现这篇文章非常有用。特别是以下示例:

#!/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

因此,基本上每次调用yieldruby都会在do块中或内部运行代码{}。如果提供参数,yield则将其作为参数提供给do块。

对我来说,这是我第一次真正了解do积木的作用。从根本上说,这是函数访问内部数据结构的一种方法,无论是迭代还是配置功能。

因此,在使用rails时,请编写以下代码:

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

这将运行respond_to函数,该函数产生do带有(内部)format参数的块。然后,您.html可以在此内部变量上调用函数,从而产生代码块以运行render命令。请注意,.html只有在它是请求的文件格式时,它才会产生。(技术性:这些功能实际上block.call并不yield像您从源代码中看到的那样使用,但功能本质上是相同的,请参阅此问题进行讨论。)这为该功能执行一些初始化然后从调用代码中获取输入和然后根据需要进行处理。

换一种说法,它类似于将匿名函数作为参数然后在javascript中调用它的函数。


8

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

块广泛用于Ruby gem(包括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


这是(唯一的)答案,真正使我了解什么是区块和收益,以及如何使用它们。
埃里克·王

5

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

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

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

好吧,为什么呢?原因很多,例如,Logger如果用户不需要,则不必执行某些任务。不过,您应该向自己解释...
Ulysse BN18年

4

简而言之,Yields允许您创建的方法采用和调用块。yield关键字特别是将在块中执行“填充”的位置。


1

关于产量,我想提出两点。首先,尽管这里有很多答案都讨论了将块传递给使用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

调用每个方法时,它将逐行执行。现在,当我们到达3.times块时,该块将被调用3次。每次调用yield时。该产量链接到与调用each方法的方法相关联的块。重要的是要注意,每次调用yield时,它都会将控制权返回给客户端代码中每个方法的块。一旦该块完成执行,它将返回到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调用某些对象的情况下,必须明确说明枚举方法是什么。


0

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

Def Up(anarg)
  yield(anarg)
end

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

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

当Up方法使用参数调用yield时,它将传递到block变量以处理请求。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.