Ruby中的块和产量

Blocks and yields in Ruby

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

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

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


是的,一开始有点困惑。

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

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

例如,迭代列表或提供自定义算法非常方便。

举个例子:

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

1
2
3
4
5
6
7
8
9
class Person
    def initialize( name )
         @name = name
    end

    def do_with_name
        yield( @name )
    end
end

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

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

1
2
3
4
5
6
person = Person.new("Oscar")

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

将打印:

1
Hey, his name is Oscar

注意,块作为一个参数接收一个名为name的变量(注意,您可以随意调用这个变量,但调用它name是有意义的)。当代码调用yield时,它用@name的值填充这个参数。

1
yield( @name )

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

1
2
3
4
5
6
7
8
9
10
11
#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)—它只是一个不同的块。

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

1
2
3
4
5
6
7
8
 days = ["monday","tuesday","wednesday","thursday","friday"]  

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

=> ["tuesday","thursday"]

或者,我们也可以提供自定义排序算法,例如基于字符串大小:

1
2
3
4
5
 days.sort do |x,y|
    x.size <=> y.size
 end

=> ["monday","friday","tuesday","thursday","wednesday"]

我希望这能帮助你更好地理解它。

顺便说一句,如果块是可选的,您应该这样调用它:

1
yield(value) if block_given?

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


很有可能有人会在这里提供一个真正详细的答案,但我一直认为罗伯特·索辛斯基的这篇文章是对街区、procs&lambdas之间微妙之处的一个很好的解释。

我应该补充一点,我相信我链接到的帖子特定于Ruby1.8。Ruby1.9中的一些内容发生了变化,比如块变量是块的局部变量。在1.8中,您将得到如下内容:

1
2
3
4
5
6
>> a ="Hello"
=>"Hello"
>> 1.times { |a| a ="Goodbye" }
=> 1
>> a
=>"Goodbye"

鉴于1.9将给您:

1
2
3
4
5
6
>> a ="Hello"
=>"Hello"
>> 1.times { |a| a ="Goodbye" }
=> 1
>> a
=>"Hello"

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


在Ruby中,方法可以检查是否以这样的方式调用它们,即除了常规参数之外,还提供了一个块。通常,这是使用block_given?方法完成的,但是您也可以通过在最后的参数名前面加上一个与号(&号)来将块作为显式过程来引用。

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

1
2
3
4
5
6
7
8
9
10
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 =)

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

1
2
3
4
5
6
7
8
9
10
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 =)


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

不知道你来自哪种语言,但是假设它是一种静态语言,这类事情看起来会很熟悉。这就是如何在Java中读取文件的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
    4
    5
    6
    File.open("readfile.rb","r") do |infile|
        while (line = infile.gets)
            puts"#{counter}: #{line}"
            counter = counter + 1
        end
    end

    完全不同。把这个拆下来

  • 告诉文件类如何初始化资源
  • 告诉文件类如何处理它
  • 嘲笑仍在打字的爪哇人;
  • 这里,不是处理第一步和第二步,而是将其委托给另一个类。如您所见,这显著降低了您必须编写的代码量,从而使事情更容易读取,并减少了内存泄漏或文件锁未被清除的可能性。

    现在,它不像你不能用Java做类似的事情,事实上,人们已经做了几十年了。这就是所谓的战略模式。不同之处在于,如果没有块,对于像文件示例这样简单的东西,由于需要编写的类和方法的数量,策略就会变得过多。对于块来说,这是一种简单而优雅的方法,因此不以这种方式构造代码没有任何意义。

    这并不是块的唯一使用方式,但是其他的(比如builder模式,您可以在rails中的api形式中看到)非常相似,一旦您将头绕在这个上面,就会很明显地看到发生了什么。当您看到块时,通常可以安全地假定方法调用是您想要做的,并且块描述了您想要如何做。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #!/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

    应给出以下输出:

    1
    2
    3
    4
    5
    6
    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中,您可以编写以下内容:

    1
    2
    3
    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调用它的函数。


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

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

    基本语法

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

    1
    2
    3
    4
    5
    { # This is a single line block }

    do
      # This is a multi-line block
    end

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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语句时,medite方法将对块产生控制权,执行块内的代码并将控制权返回给方法,该方法将在yield语句之后立即恢复执行。

    当一个方法包含yield语句时,它期望在调用时接收一个块。如果未提供块,则在到达yield语句后将引发异常。我们可以使块成为可选的,并避免引发异常:

    1
    2
    3
    4
    5
    6
    7
    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


    我有时用"屈服"这个词:

    1
    2
    3
    4
    5
    6
    def add_to_http
      "http://#{yield}"
    end

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


    简单地说,生成允许您创建的方法接受并调用块。yield关键字是块中执行"stuff"的位置。


    关于收益率,我想说两点。首先,虽然这里有很多答案讨论了将块传递给使用yield的方法的不同方法,但我们还是来讨论控制流。这一点尤其重要,因为您可以对一个块进行多次让步。让我们来看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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。该yield链接到与调用每个方法的方法关联的块。需要注意的是,每次调用yield时,它都会将控制权返回到客户机代码中每个方法的块中。一旦块执行完毕,它将返回到3.times块。这种情况会发生3次。因此,客户机代码中的块在3个不同的场合被调用,因为yield被显式地调用了3个不同的时间。

    我的第二点是关于枚举和收益。用于实例化枚举器类的枚举器,此枚举器对象也响应yield。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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。下次我们调用它时,它将调用下一个收益率等等。

    关于枚举有一个有趣的消息。联机文档说明如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    如果不指定符号作为枚举的参数,Ruby将把枚举器挂接到接收器的每个方法上。有些类没有每个方法,比如字符串类。

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

    因此,在使用枚举调用某些对象的情况下,必须明确说明枚举方法是什么。


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

    1
    2
    3
    Def Up(anarg)
      yield(anarg)
    end

    您可以创建一个方法"up",它被分配一个参数。现在可以将此参数赋给yield,yield将调用并执行关联的块。可以在参数列表之后指定块。

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

    当up方法使用参数调用yield时,它将传递给块变量以处理请求。