wake-up-neo.net

Blöcke und Erträge in Ruby

Ich versuche, Blöcke und yield und deren Funktionsweise in Ruby zu verstehen.

Wie wird yield verwendet? Viele der Rails-Anwendungen, die ich mir angesehen habe, verwenden yield auf seltsame Weise.

Kann mir jemand erklären oder mir zeigen, wohin ich gehen soll, um sie zu verstehen?

251
Matt Elhotiby

Ja, es ist zunächst etwas rätselhaft. 

In Ruby können Methoden einen Codeblock empfangen, um beliebige Codesegmente auszuführen. 

Wenn eine Methode einen Block erwartet, ruft sie ihn durch Aufrufen der Funktion yield auf. 

Dies ist zum Beispiel sehr praktisch, um eine Liste zu durchlaufen oder einen benutzerdefinierten Algorithmus bereitzustellen. 

Nehmen Sie das folgende Beispiel:

Ich werde eine Person-Klasse definieren, die mit einem Namen initialisiert ist, und eine do_with_name-Methode bereitstellen, die beim Aufruf das name-Attribut an den empfangenen Block übergeben würde.

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

    def do_with_name 
        yield( @name ) 
    end
end

Dies würde es uns ermöglichen, diese Methode aufzurufen und einen beliebigen Codeblock zu übergeben.

Um den Namen zu drucken, würden wir beispielsweise Folgendes tun:

person = Person.new("Oscar")

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

Würde drucken:

Hey, his name is Oscar

Beachten Sie, dass der Block als Parameter eine Variable mit dem Namen name empfängt (Sie können diese Variable jedoch beliebig nennen, es ist jedoch sinnvoll, sie als name zu bezeichnen). Wenn der Code yield aufruft, füllt er diesen Parameter mit dem Wert von @name

yield( @name )

Wir könnten einen anderen Block bereitstellen, um eine andere Aktion auszuführen. Zum Beispiel den Namen umkehren:

#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"

Wir haben genau dieselbe Methode (do_with_name) verwendet - es ist nur ein anderer Block.

Dieses Beispiel ist trivial. Weitere interessante Anwendungen sind das Filtern aller Elemente in einem Array:

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

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

=> ["tuesday", "thursday"]

Oder wir können auch einen benutzerdefinierten Sortieralgorithmus bereitstellen, beispielsweise basierend auf der Zeichenfolgengröße:

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

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

Ich hoffe, das hilft Ihnen, es besser zu verstehen.

Übrigens, wenn der Block optional ist, solltest du ihn so nennen:

yield(value) if block_given?

Wenn dies nicht optional ist, rufen Sie es einfach auf. 

354
OscarRyz

In Ruby können Methoden überprüfen, ob sie so aufgerufen wurden, dass zusätzlich zu den normalen Argumenten ein Block bereitgestellt wurde. Normalerweise geschieht dies mit der block_given?-Methode. Sie können den Block jedoch auch als expliziten Proc bezeichnen, indem Sie ein kaufmännisches Und (&) vor den endgültigen Argumentnamen setzen.

Wenn eine Methode mit einem Block aufgerufen wird, kann die Methode yield den Block (Aufruf des Blocks) mit einigen Argumenten steuern, falls erforderlich. Betrachten Sie diese Beispielmethode, die Folgendes demonstriert:

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

Oder verwenden Sie die spezielle Blockargumentensyntax:

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
maerics

Es ist durchaus möglich, dass jemand eine wirklich detaillierte Antwort gibt, aber ich habe immer diesen Beitrag von Robert Sosinski als eine großartige Erklärung für die Feinheiten zwischen Blöcken, Prozessen und Lambdas gefunden.

Ich sollte hinzufügen, dass ich glaube, dass der Beitrag, zu dem ich verlinkt habe, spezifisch für Ruby 1.8 ist. In Ruby 1.9 haben sich einige Dinge geändert, z. B. Blockvariablen, die lokal für den Block sind. In 1.8 würden Sie etwa Folgendes erhalten:

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

Während 1.9 Ihnen folgendes geben würde:

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

Ich habe nicht 1.9 auf diesem Rechner, daher könnte dies einen Fehler enthalten.

22
theIV

Ich wollte irgendwie hinzufügen, warum Sie die Dinge auf diese Weise zu den ohnehin schon guten Antworten machen würden.

Keine Ahnung, aus welcher Sprache Sie kommen, aber wenn man davon ausgeht, dass es sich um eine statische Sprache handelt, werden solche Dinge bekannt vorkommen. So lesen Sie eine Datei in 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();
    }
  }
}

Die ganze Sache der Stream-Verkettung ignorieren, Die Idee ist diese

  1. Initialisieren Sie die Ressource, die bereinigt werden muss
  2. ressource verwenden
  3. stellen Sie sicher, es aufzuräumen

So machen Sie es in Ruby

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

Wild anders Brechen Sie dieses hier ab

  1. teilen Sie der File-Klasse mit, wie die Ressource zu initialisieren ist
  2. sagen Sie der Dateiklasse, was Sie damit machen sollen
  3. lache über die Java-Leute, die immer noch tippen ;-)

Anstatt die Schritte eins und zwei zu bearbeiten, delegieren Sie dies im Grunde in eine andere Klasse. Wie Sie sehen, wird dadurch die Menge an Code, die Sie schreiben müssen, drastisch reduziert, was das Lesen einfacher macht und die Wahrscheinlichkeit verringert, dass Speicherlecks oder Dateisperren nicht gelöscht werden. 

Nun, es ist nicht so, dass Sie in Java nichts Ähnliches tun können. Tatsächlich tun die Leute das schon seit Jahrzehnten. Es wird das Strategie Muster genannt. Der Unterschied besteht darin, dass die Strategie ohne Blöcke für ein einfaches Beispiel wie das Dateibeispiel aufgrund der Menge an Klassen und Methoden, die Sie schreiben müssen, zu viel wird. Bei Blöcken ist dies eine so einfache und elegante Methode, dass es keinen Sinn macht, Ihren Code NICHT so zu strukturieren.

Dies ist nicht die einzige Art, wie Blöcke verwendet werden, aber die anderen (wie das Builder-Muster, das Sie in der form_for api in Rails sehen) sind ähnlich genug, dass es offensichtlich sein sollte, was los ist, sobald Sie Ihren Kopf darum legen. Wenn Sie Blöcke sehen, können Sie normalerweise davon ausgehen, dass der Methodenaufruf genau das ist, was Sie tun möchten, und der Block beschreibt, wie Sie dies tun möchten.

13
Matt Briggs

Ich fand diesen Artikel sehr nützlich zu sein. Insbesondere das folgende Beispiel:

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

was folgende Ausgabe ergeben sollte:

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

Im Wesentlichen wird also jedes Mal, wenn ein Aufruf an yield erfolgt, der Code im do-Block oder in {} ausgeführt. Wenn ein Parameter für yield bereitgestellt wird, wird dieser als Parameter für den do-Block bereitgestellt.

Für mich war dies das erste Mal, dass ich wirklich verstand, was die do-Blöcke taten. Es ist im Wesentlichen eine Möglichkeit für die Funktion, Zugriff auf interne Datenstrukturen zu gewähren, sei es für die Iteration oder für die Konfiguration der Funktion.

Wenn Sie in Rails schreiben, schreiben Sie Folgendes:

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

Dadurch wird die respond_to-Funktion ausgeführt, die den do-Block mit dem (internen) format-Parameter liefert. Sie rufen dann die Funktion .html für diese interne Variable auf, die wiederum den Codeblock für die Ausführung des Befehls render liefert. Beachten Sie, dass .html nur dann nachgibt, wenn es sich um das angeforderte Dateiformat handelt. (Technisch: Diese Funktionen verwenden tatsächlich block.call nicht yield, wie Sie an der source sehen können, aber die Funktionalität ist im Wesentlichen die gleiche, siehe diese Frage für eine Diskussion.) Dies bietet eine Möglichkeit für die Funktion Bei einigen Initialisierungen werden dann Eingaben vom aufrufenden Code übernommen und die Verarbeitung ggf. fortgesetzt.

Oder anders ausgedrückt, sie ähnelt einer Funktion, die eine anonyme Funktion als Argument verwendet und dann in Javascript aufruft.

12
zelanix

In Ruby ist ein Block im Wesentlichen ein Teil des Codes, der an eine beliebige Methode übergeben und ausgeführt werden kann. Blöcke werden immer mit Methoden verwendet, die normalerweise Daten als Argumente liefern. 

Blöcke werden häufig in Ruby-Edelsteinen (einschließlich Rails) und in gut geschriebenem Ruby-Code verwendet. Sie sind keine Objekte und können daher nicht Variablen zugewiesen werden. 

Grundlegende Syntax

Ein Block ist ein von {} oder do..end eingeschlossener Code. Konventionell sollte die geschweifte Klammer-Syntax für einzeilige Blöcke und die do..end-Syntax für mehrzeilige Blöcke verwendet werden. 

{ # This is a single line block }

do
  # This is a multi-line block
end 

Jede Methode kann einen Block als implizites Argument erhalten. Ein Block wird von der Yield-Anweisung innerhalb einer Methode ausgeführt. Die grundlegende Syntax lautet: 

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.

Wenn die Yield-Anweisung erreicht ist, gibt die Meditation-Methode die Kontrolle an den Block, der Code innerhalb des Blocks wird ausgeführt und die Kontrolle an die Methode zurückgegeben, die die Ausführung unmittelbar nach der Yield-Anweisung fortsetzt. 

Wenn eine Methode eine Yield-Anweisung enthält, erwartet sie zum Zeitpunkt des Aufrufs einen Block. Wenn kein Block bereitgestellt wird, wird eine Ausnahme ausgelöst, sobald die Ertragsaussage erreicht ist. Wir können den Block optional machen und verhindern, dass eine Ausnahme ausgelöst wird: 

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

Output:
Today we will practice zazen. 

Es ist nicht möglich, mehrere Blöcke an eine Methode zu übergeben. Jede Methode kann nur einen Block empfangen. 

Weitere Informationen finden Sie unter: http://www.zenruby.info/2016/04/introduction-to-blocks-in-Ruby.html

7
user3847220

Ich benutze manchmal "Ertrag" wie folgt:

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

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
5
Samet Sazak

Um es einfach auszudrücken: Erlauben Sie der Methode, die Sie erstellen, Blöcke zu nehmen und aufzurufen. Das Ergebnis-Schlüsselwort ist insbesondere die Stelle, an der das "Zeug" im Block ausgeführt wird.

4
ntarpey

Ich möchte hier auf zwei Punkte eingehen. Erstens, während viele Antworten hier über verschiedene Wege sprechen, um einen Block an eine Methode zu übergeben, die den Ertrag verwendet, wollen wir auch über den Kontrollfluss sprechen. Dies ist besonders wichtig, da Sie einem Block MULTIPLE-Zeiten geben können. Schauen wir uns ein Beispiel an:

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

Wenn jede Methode aufgerufen wird, wird sie zeilenweise ausgeführt. Wenn wir nun zum 3.x-Block kommen, wird dieser Block dreimal aufgerufen. Jedes Mal, wenn es den Ertrag aufruft. Diese Ausbeute ist mit dem Block verknüpft, der der Methode zugeordnet ist, die die jeweilige Methode aufgerufen hat. Es ist wichtig zu wissen, dass bei jedem Aufruf von Yield die Kontrolle an den Block jeder Methode im Clientcode zurückgegeben wird. Sobald die Ausführung des Blocks abgeschlossen ist, kehrt er zum 3x-Block zurück. Und das passiert 3 mal. Dieser Block im Client-Code wird also 3 Mal aufgerufen, da Yield explizit 3 Mal aufgerufen wird. 

Mein zweiter Punkt betrifft enum_for und Yield. enum_for instanziiert die Enumerator-Klasse, und dieses Enumerator-Objekt reagiert auch auf das Ergebnis. 

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" 

Beachten Sie also jedes Mal, wenn wir Arten mit dem externen Iterator aufrufen, der Ertrag wird nur einmal aufgerufen. Wenn wir es das nächste Mal aufrufen, ruft es den nächsten Ertrag auf und so weiter. 

Es gibt ein interessantes Leckerbissen in Bezug auf enum_for. In der Online-Dokumentation heißt es:

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

Wenn Sie kein Symbol als Argument für enum_for angeben, bindet Ruby den Enumerator an die jeweilige Methode des Empfängers. Einige Klassen verfügen nicht über jede Methode, wie die String-Klasse. 

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

Bei einigen mit enum_for aufgerufenen Objekten müssen Sie daher explizit angeben, welche Aufzählmethode verwendet wird. 

1
Donato

Yield kann als namenloser Block verwendet werden, um einen Wert in der Methode zurückzugeben. Betrachten Sie den folgenden Code:

Def Up(anarg)
  yield(anarg)
end

Sie können eine Methode "Up" erstellen, der ein Argument zugewiesen ist. Sie können dieses Argument jetzt nachgeben, um einen zugehörigen Block aufzurufen und auszuführen. Sie können den Baustein nach der Parameterliste zuordnen.

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

Wenn die Up-Methode mit einem Argument aufruft, wird sie zur Verarbeitung der Anforderung an die Blockvariable übergeben.

0
gkstr1