Blöcke und Ausbeuten in Ruby


275

Ich versuche zu verstehen, yieldwie Blöcke funktionieren und wie sie in Ruby funktionieren.

Wie wird yieldverwendet? Viele der Rails-Anwendungen, die ich mir angesehen habe, werden auf yieldseltsame Weise verwendet.

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


2
Vielleicht interessiert Sie die Antwort auf Rubys Ertragsmerkmal in Bezug auf die Informatik . Obwohl es eine etwas andere Frage ist als Ihre, kann sie etwas Licht in die Sache bringen.
Ken Bloom

Antworten:


393

Ja, es ist zunächst ein bisschen 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 yieldFunktion auf.

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

Nehmen Sie das folgende Beispiel:

Ich werde eine PersonKlasse definieren, die mit einem Namen initialisiert wurde, und eine do_with_nameMethode bereitstellen , die beim Aufrufen nur das nameAttribut an den empfangenen Block übergibt.

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 beispielsweise den Namen zu drucken, würden wir 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 empfängt (Hinweis: nameSie können diese Variable beliebig aufrufen, es ist jedoch sinnvoll, sie aufzurufen name). Wenn der Code aufgerufen wird yield, füllt er diesen Parameter mit dem Wert von @name.

yield( @name )

Wir könnten einen weiteren Block bereitstellen, um eine andere Aktion auszuführen. Kehren Sie beispielsweise den Namen um:

#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 die gleiche Methode ( do_with_name) verwendet - es ist nur ein anderer Block.

Dieses Beispiel ist trivial. Interessantere 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, der beispielsweise auf der Zeichenfolgengröße basiert:

 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, sollten Sie ihn wie folgt aufrufen:

yield(value) if block_given?

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

BEARBEITEN

@hmak hat ein repl.it für diese Beispiele erstellt: https://repl.it/@makstaks/blocksandyieldsrubyexample


wie es druckt racsOwenn the_name = ""
Paritosh Piplewar

2
Entschuldigung, der Name ist eine Instanzvariable, die mit initialisiert wurde "Oscar" (ist in der Antwort nicht sehr klar)
OscarRyz

Was ist mit Code wie diesem? person.do_with_name {|string| yield string, something_else }
f.ardelian

7
In Javascripty-Begriffen ist dies eine standardisierte Methode, um einen Rückruf an eine bestimmte Methode zu übergeben und aufzurufen. Danke für die Erklärung!
Yitznewton

Allgemeiner ausgedrückt - ein Block ist ein rubinroter "erweiterter" Syntaxzucker für das Strategiemuster. weil die typische Verwendung darin besteht, einen Code bereitzustellen, um etwas im Kontext einer anderen Operation zu tun. Aber Ruby-Verbesserungen eröffnen einen Weg zu so coolen Dingen wie dem Schreiben von DSLs mit Block, um den Kontext weiterzugeben
Roman Bulgakov,

25

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

Wenn eine Methode mit einem Block aufgerufen wird, kann die Methode yieldbei Bedarf mit einigen Argumenten den Block steuern (den Block aufrufen). 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 Blockargument-Syntax:

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

Gut zu wissen, wie man einen Block auslöst.
LPing

22

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

Ich sollte hinzufügen, dass ich glaube, dass der Beitrag, auf den ich verlinke, spezifisch für Ruby 1.8 ist. Einige Dinge haben sich in Ruby 1.9 geändert, z. B. Blockvariablen, die lokal für den Block sind. In 1.8 erhalten Sie ungefähr Folgendes:

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

Während 1.9 Ihnen geben würde:

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

Ich habe nicht 1.9 auf diesem Computer, daher kann ein Fehler auftreten.


Tolle Beschreibung in diesem Artikel, ich habe Monate
gebraucht

Genau. Ich glaube nicht, dass ich die Hälfte der erklärten Dinge wusste, bis ich sie gelesen habe.
theIV

Der aktualisierte Link ist jetzt auch 404. Hier ist der Link zur Wayback-Maschine .
Klenwell

@klenwell danke für das Heads Up, ich habe den Link nochmal aktualisiert.
theIV

13

Ich wollte irgendwie hinzufügen, warum Sie die bereits so guten Antworten so machen würden.

Keine Ahnung, aus welcher Sprache Sie kommen, aber wenn es sich um eine statische Sprache handelt, wird Ihnen so etwas 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 ist, die Idee zu ignorieren

  1. Initialisieren Sie die Ressource, die bereinigt werden muss
  2. Ressource verwenden
  3. Stellen Sie sicher, dass Sie es aufräumen

So machst du es in Rubin

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

Ganz anders. Brechen Sie dieses auf

  1. Teilen Sie der File-Klasse mit, wie die Ressource initialisiert werden soll
  2. Sagen Sie der Dateiklasse, was damit zu tun ist
  3. lache über die Java-Jungs, die noch tippen ;-)

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

Nun, es ist nicht so, dass man in Java nichts Ähnliches machen kann, tatsächlich machen es die Leute schon seit Jahrzehnten. Es heißt das Strategiemuster . Der Unterschied besteht darin, dass ohne Blöcke für etwas Einfaches wie das Dateibeispiel die Strategie aufgrund der Anzahl der Klassen und Methoden, die Sie schreiben müssen, übertrieben wird. Mit 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 Möglichkeit, wie Blöcke verwendet werden, aber die anderen (wie das Builder-Muster, das Sie in form_for api in Rails sehen können) sind ähnlich genug, dass es offensichtlich sein sollte, was passiert, wenn Sie Ihren Kopf darum wickeln. 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 ihn ausführen möchten.


5
Lassen Sie uns das ein wenig vereinfachen: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" endund die Java-Leute noch härter auslachen.
Michael Hampton

1
@ MichaelHampton, lache, nachdem du eine Datei gelesen hast, die ein paar Gigabyte lang ist.
Akostadinov

@akostadinov Nein ... das bringt mich zum Weinen!
Michael Hampton

3
@ MichaelHampton Oder noch besser: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(plus keine Speicherprobleme)
Fund Monica Klage

12

Ich fand diesen Artikel sehr nützlich. 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

welches die folgende Ausgabe geben 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 yieldwird der Code bei jedem Aufruf von Ruby im doBlock oder im Inneren ausgeführt {}. Wenn ein Parameter für bereitgestellt wird, yieldwird dieser als Parameter für den doBlock bereitgestellt .

Für mich war es das erste Mal, dass ich wirklich verstand, was die doBlöcke taten. Es ist im Grunde 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 also in Schienen sind, schreiben Sie Folgendes:

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

Dadurch wird die respond_toFunktion ausgeführt, die den doBlock mit dem (internen) formatParameter ergibt . Sie rufen dann die .htmlFunktion für diese interne Variable auf, die wiederum den Codeblock zum Ausführen des renderBefehls ergibt . Beachten Sie, dass dies .htmlnur dann der Fall ist, wenn es sich um das angeforderte Dateiformat handelt. (Technische Daten: Diese Funktionen werden tatsächlich block.callnicht verwendet, yieldwie Sie aus der Quelle ersehen können, aber die Funktionalität ist im Wesentlichen dieselbe . Weitere Informationen finden Sie in dieser Frage .) Auf diese Weise kann die Funktion eine Initialisierung durchführen und dann Eingaben aus dem aufrufenden Code und vornehmen dann bei Bedarf weiter verarbeiten.

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


8

In Ruby ist ein Block im Grunde ein Codeabschnitt, der an jede Methode übergeben und von dieser ausgeführt werden kann. Blöcke werden immer mit Methoden verwendet, die ihnen normalerweise Daten (als Argumente) zuführen.

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 keinen Variablen zugewiesen werden.

Grundlegende Syntax

Ein Block ist ein Code, der von {} oder do..end eingeschlossen ist. 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 empfangen. 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 Meditationsmethode dem Block die Kontrolle, der Code innerhalb des Blocks wird ausgeführt und die Kontrolle wird an die Methode zurückgegeben, die die Ausführung unmittelbar nach der Yield-Anweisung wieder aufnimmt.

Wenn eine Methode eine Yield-Anweisung enthält, erwartet sie beim Aufruf einen Block. Wenn kein Block bereitgestellt wird, wird eine Ausnahme ausgelöst, sobald die Yield-Anweisung erreicht ist. Wir können den Block optional machen und vermeiden, 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


Dies ist die (einzige) Antwort, die mich wirklich verstehen lässt, was Block und Ausbeute ist und wie man sie verwendet.
Eric Wang

5

Ich benutze manchmal "Ausbeute" wie folgt:

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

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

OK, aber warum ? Es gibt viele Gründe, zum Beispiel, dass einer Loggerkeine Aufgabe ausführen muss, wenn der Benutzer dies nicht muss. Sie sollten Ihre jedoch erklären ...
Ulysse BN

4

Erträge, um es einfach auszudrücken, erlauben der von Ihnen erstellten Methode, Blöcke zu nehmen und aufzurufen. Das Yield-Schlüsselwort ist speziell die Stelle, an der das 'Zeug' im Block ausgeführt wird.


1

Es gibt zwei Punkte, die ich hier zum Ertrag ansprechen möchte. Während viele Antworten hier über verschiedene Möglichkeiten sprechen, einen Block an eine Methode zu übergeben, die Yield verwendet, sprechen wir zunächst auch über den Kontrollfluss. Dies ist besonders relevant, da Sie einem Block MEHRERE 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.times-Block kommen, wird dieser Block dreimal aufgerufen. Jedes Mal, wenn es Ertrag aufruft. Diese Ausbeute ist mit dem Block verknüpft, der der Methode zugeordnet ist, die die einzelnen Methoden aufgerufen hat. Es ist wichtig zu beachten, dass jedes Mal, wenn Yield aufgerufen wird, die Kontrolle an den Block jeder Methode im Clientcode zurückgegeben wird. Sobald die Ausführung des Blocks abgeschlossen ist, kehrt er zum 3.times-Block zurück. Und das passiert dreimal. Dieser Block im Clientcode wird also dreimal aufgerufen, da Yield explizit dreimal hintereinander aufgerufen wird.

Mein zweiter Punkt betrifft enum_for und Yield. enum_for instanziiert die Enumerator-Klasse und dieses Enumerator-Objekt reagiert auch auf 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" 

Beachten Sie also, dass jedes Mal, wenn wir Arten mit dem externen Iterator aufrufen, die Ausbeute nur einmal aufgerufen wird. Wenn wir es das nächste Mal aufrufen, wird die nächste Ausbeute aufgerufen und so weiter.

Es gibt einen interessanten 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 für enum_for kein Symbol als Argument angeben, verknüpft ruby ​​den Enumerator mit der jeweiligen Methode des Empfängers. Einige Klassen haben nicht 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 Objekten, die mit enum_for aufgerufen werden, müssen Sie daher explizit angeben, wie Ihre Aufzählungsmethode aussehen soll.


0

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 dem Ertrag zuweisen, der einen zugeordneten Block aufruft und ausführt. Sie können den Block nach der Parameterliste zuweisen.

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

Wenn die Up-Methode Ausbeute mit einem Argument aufruft, wird sie an die Blockvariable übergeben, um die Anforderung zu verarbeiten.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.