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

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

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


当前回答

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

不知道你来自什么语言,但假设它是静态语言,这类事情看起来会很熟悉。这是在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中做类似的事情,事实上,人们已经做了几十年了。这被称为策略模式。不同之处在于,如果没有块,对于像文件这样简单的例子,策略就会因为需要编写的类和方法的数量而变得多余。使用块,这是一种简单而优雅的方式,不以这种方式构建代码是没有任何意义的。

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

其他回答

关于收益率,我想说两点。首先,虽然这里有很多答案讨论了将块传递给使用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

When the each method is invoked, it executes line by line. Now when we get to the 3.times block, this block will be invoked 3 times. Each time it invokes yield. That yield is linked to the block associated with the method that called the each method. It is important to notice that each time yield is invoked, it returns control back to the block of the each method in client code. Once the block is finished executing, it returns back to the 3.times block. And this happens 3 times. So that block in client code is invoked on 3 separate occasions since yield is explicitly called 3 separate times.

我的第二点是关于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调用的某些对象,必须显式说明枚举方法是什么。

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

如果一个方法是用块调用的,那么如果需要的话,该方法可以通过一些参数将控制权交给块(调用块)。考虑这个示例方法,它演示了:

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

在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

很可能有人会在这里提供一个真正详细的答案,但我一直认为Robert Sosinski的这篇文章对block, procs和lambdas之间的微妙之处进行了很好的解释。

我应该补充一点,我所链接的这篇文章是专门针对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();
    }
  }
}

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

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

这是在ruby中如何做的

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

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

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

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

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

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