Blocchi e rese in Ruby


275

Sto cercando di capire i blocchi yielde come funzionano in Ruby.

Come si yieldusa Molte delle applicazioni di Rails che ho visto usare yieldin modo strano.

Qualcuno può spiegarmi o mostrarmi dove andare per capirli?


2
Potresti essere interessato alla risposta alla caratteristica di rendimento di Ruby in relazione all'informatica . Sebbene sia una domanda in qualche modo diversa dalla tua, potrebbe far luce sulla questione.
Ken Bloom,

Risposte:


393

Sì, all'inizio è un po 'sconcertante.

In Ruby, i metodi possono ricevere un blocco di codice per eseguire segmenti arbitrari di codice.

Quando un metodo prevede un blocco, lo invoca chiamando la yieldfunzione.

Questo è molto utile, ad esempio, per scorrere su un elenco o per fornire un algoritmo personalizzato.

Prendi il seguente esempio:

Definirò una Personclasse inizializzata con un nome e fornirò un do_with_namemetodo che, se invocato, passerebbe semplicemente l' nameattributo al blocco ricevuto.

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

    def do_with_name 
        yield( @name ) 
    end
end

Questo ci permetterebbe di chiamare quel metodo e passare un blocco di codice arbitrario.

Ad esempio, per stampare il nome dovremmo fare:

person = Person.new("Oscar")

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

Stampa:

Hey, his name is Oscar

Nota, il blocco riceve, come parametro, una variabile chiamata name(NB puoi chiamare questa variabile come preferisci, ma ha senso chiamarla name). Quando il codice invoca yieldriempie questo parametro con il valore di @name.

yield( @name )

Potremmo fornire un altro blocco per eseguire un'azione diversa. Ad esempio, invertire il nome:

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

Abbiamo usato esattamente lo stesso metodo ( do_with_name) - è solo un blocco diverso.

Questo esempio è banale. Usi più interessanti sono filtrare tutti gli elementi in un array:

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

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

=> ["tuesday", "thursday"]

Oppure, possiamo anche fornire un algoritmo di ordinamento personalizzato, ad esempio basato sulla dimensione della stringa:

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

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

Spero che questo ti aiuti a capirlo meglio.

A proposito, se il blocco è opzionale dovresti chiamarlo come:

yield(value) if block_given?

Se non è facoltativo, basta invocarlo.

MODIFICARE

@hmak ha creato un repl.it per questi esempi: https://repl.it/@makstaks/blocksandyieldsrubyexample


come stampa racsOse the_name = ""
Paritosh Piplewar,

2
Siamo spiacenti, il nome è una variabile di istanza inizializzata con "Oscar" (non è molto chiara nella risposta)
OscarRyz

Che dire del codice come questo? person.do_with_name {|string| yield string, something_else }
f.ardelian,

7
Quindi, in termini di Javascripty, è un modo standardizzato di passare un callback a un determinato metodo e chiamarlo. Grazie per la spiegazione!
Yitznewton,

In un modo più generale: un blocco è uno zucchero sintetico "migliorato" rubino per il modello di strategia. perché l'uso tipico è fornire un codice per fare qualcosa nel contesto di altre operazioni. Ma i miglioramenti del rubino aprono la strada a cose interessanti come la scrittura di DSL usando il blocco per passare il contesto
Roman Bulgakov

25

In Ruby, i metodi possono verificare se sono stati chiamati in modo tale che sia stato fornito un blocco in aggiunta ai normali argomenti. In genere questo viene fatto usando il block_given?metodo ma è anche possibile fare riferimento al blocco come Proc esplicito anteponendo una e commerciale ( &) prima del nome dell'argomento finale.

Se un metodo viene invocato con un blocco, il metodo può yieldcontrollare il blocco (chiamare il blocco) con alcuni argomenti, se necessario. Considera questo metodo di esempio che dimostra:

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

Oppure, usando la sintassi dell'argomento blocco speciale:

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

Buono a sapersi modi diversi per attivare un blocco.
LPing

22

È possibile che qualcuno fornisca una risposta davvero dettagliata qui, ma ho sempre trovato questo post di Robert Sosinski come una grande spiegazione delle sottigliezze tra blocchi, proc e lambda.

Dovrei aggiungere che credo che il post a cui mi sto collegando sia specifico per ruby ​​1.8. Alcune cose sono cambiate in ruby ​​1.9, come ad esempio le variabili di blocco che sono locali al blocco. In 1.8, otterrai qualcosa di simile al seguente:

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

Considerando che 1.9 ti darebbe:

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

Non ho 1.9 su questa macchina, quindi sopra potrebbe esserci un errore.


Grande descrizione in quell'articolo, mi ci sono voluti mesi per capire che tutto da solo =)
maerics

Sono d'accordo. Non credo di conoscere metà delle cose spiegate fino a quando non l'ho letto.
theIV

Anche il link aggiornato ora è 404. Ecco il link Wayback Machine .
Klenwell,

@klenwell grazie per l'heads-up, ho aggiornato di nuovo il link.
theIV

13

Volevo aggiungere il motivo per cui avresti fatto le cose in quel modo alle già ottime risposte.

Non ho idea di quale lingua tu stia provando, ma supponendo che sia un linguaggio statico, questo genere di cose sembrerà familiare. Ecco come leggere un file 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();
    }
  }
}

Ignorando l'intera cosa del concatenamento del flusso, l'idea è questa

  1. Inizializza la risorsa che deve essere ripulita
  2. usa la risorsa
  3. assicurati di ripulirlo

Ecco come lo fai in rubino

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

Selvaggiamente diversa. Abbattere questo

  1. dire alla classe File come inizializzare la risorsa
  2. dire alla classe di file cosa farne
  3. ridere dei ragazzi di Java che stanno ancora scrivendo ;-)

Qui, invece di gestire i passaggi uno e due, in pratica lo deleghi in un'altra classe. Come puoi vedere, questo riduce drasticamente la quantità di codice che devi scrivere, il che rende le cose più facili da leggere e riduce le possibilità di cose come perdite di memoria o blocchi di file che non vengono cancellati.

Ora, non è che non puoi fare qualcosa di simile in Java, infatti, la gente lo fa da decenni. Si chiama modello di strategia . La differenza è che senza blocchi, per qualcosa di semplice come l'esempio di file, la strategia diventa eccessiva a causa della quantità di classi e metodi che è necessario scrivere. Con i blocchi, è un modo così semplice ed elegante di farlo, che non ha senso NON strutturare il codice in quel modo.

Questo non è l'unico modo in cui vengono utilizzati i blocchi, ma gli altri (come il modello Builder, che puoi vedere nel form_per api in rotaie) sono abbastanza simili che dovrebbe essere ovvio cosa sta succedendo una volta avvolto la testa attorno a questo. Quando vedi i blocchi, di solito è sicuro supporre che la chiamata al metodo sia ciò che vuoi fare, e il blocco sta descrivendo come lo vuoi fare.


5
Semplificiamo un po ': File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" ende ridiamo ancora di più ai ragazzi di Java.
Michael Hampton,

1
@MichaelHampton, ridi dopo aver letto un file lungo un paio di gigabyte.
Akostadinov,

@akostadinov No ... questo mi fa venire voglia di piangere!
Michael Hampton,

3
@MichaelHampton O, meglio ancora: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" }(più nessun problema di memoria)
Finisci la causa di Monica

12

Ho trovato questo articolo molto utile. In particolare, il seguente esempio:

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

che dovrebbe dare il seguente risultato:

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

Quindi essenzialmente ogni volta che viene effettuata una chiamata a yieldruby verrà eseguito il codice nel doblocco o all'interno {}. Se viene fornito un parametro, yieldquesto verrà fornito come parametro al doblocco.

Per me, questa è stata la prima volta che ho capito davvero cosa dostavano facendo i blocchi. È fondamentalmente un modo per la funzione di dare accesso a strutture dati interne, sia per iterazione che per configurazione della funzione.

Quindi quando su rotaie scrivi quanto segue:

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

Questo eseguirà la respond_tofunzione che produce il doblocco con il formatparametro (interno) . Quindi si chiama la .htmlfunzione su questa variabile interna che a sua volta produce il blocco di codice per eseguire il rendercomando. Si noti che .htmlprodurrà solo se è il formato di file richiesto. (tecnicità: queste funzioni in realtà block.callnon usano yieldcome si può vedere dalla fonte ma la funzionalità è essenzialmente la stessa, vedere questa domanda per una discussione.) Questo fornisce un modo per la funzione di eseguire alcune inizializzazioni quindi prendere input dal codice chiamante e quindi continuare l'elaborazione se necessario.

O in altri termini, è simile a una funzione che prende una funzione anonima come argomento e poi la chiama in javascript.


8

In Ruby, un blocco è sostanzialmente un pezzo di codice che può essere passato ed eseguito con qualsiasi metodo. I blocchi vengono sempre utilizzati con i metodi, che in genere forniscono loro dati (come argomenti).

I blocchi sono ampiamente utilizzati nelle gemme di Ruby (incluso Rails) e nel codice Ruby ben scritto. Non sono oggetti, quindi non possono essere assegnati a variabili.

Sintassi di base

Un blocco è un pezzo di codice racchiuso tra {} o do..end. Per convenzione, la sintassi parentesi graffa deve essere utilizzata per i blocchi a linea singola e la sintassi do..end per i blocchi a più linee.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Qualsiasi metodo può ricevere un blocco come argomento implicito. Un blocco viene eseguito dall'istruzione yield in un metodo. La sintassi di base è:

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.

Quando viene raggiunta l'istruzione yield, il metodo meditate fornisce il controllo al blocco, il codice all'interno del blocco viene eseguito e il controllo viene restituito al metodo, che riprende l'esecuzione immediatamente dopo l'istruzione yield.

Quando un metodo contiene una dichiarazione di rendimento, si aspetta di ricevere un blocco al momento della chiamata. Se non viene fornito un blocco, verrà generata un'eccezione una volta raggiunta la dichiarazione di rendimento. Possiamo rendere facoltativo il blocco ed evitare che venga sollevata un'eccezione:

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

Output:
Today we will practice zazen. 

Non è possibile passare più blocchi a un metodo. Ogni metodo può ricevere solo un blocco.

Vedi di più su: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html


Questa è la (sola) risposta che mi fa davvero capire cos'è il blocco e il rendimento e come usarli.
Eric Wang,

5

A volte uso "yield" in questo modo:

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

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

Ok ma perche ? Ci sono molte ragioni, come quella Loggerche non deve svolgere alcuna attività se l'utente non ne ha bisogno. Dovresti spiegare il tuo però ...
Ulysse BN,

4

I rendimenti, per dirla semplicemente, consentono al metodo creato di prendere e chiamare blocchi. La parola chiave yield è specificamente il punto in cui verranno eseguite le "cose" nel blocco.


1

Ci sono due punti che voglio fare riguardo alla resa qui. Innanzitutto, mentre molte risposte qui parlano di diversi modi per passare un blocco a un metodo che utilizza il rendimento, parliamo anche del flusso di controllo. Ciò è particolarmente rilevante poiché è possibile assegnare MOLTIPLE volte a un blocco. Diamo un'occhiata a un esempio:

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

Quando viene invocato ogni metodo, viene eseguito riga per riga. Ora quando arriviamo al blocco 3.times, questo blocco verrà invocato 3 volte. Ogni volta che invoca la resa. Tale rendimento è collegato al blocco associato al metodo che ha chiamato ciascun metodo. È importante notare che ogni volta che viene invocato il rendimento, si restituisce il controllo al blocco di ciascun metodo nel codice client. Al termine dell'esecuzione, il blocco ritorna al blocco 3.times. E questo succede 3 volte. Quindi quel blocco nel codice client viene invocato in 3 occasioni separate poiché il rendimento viene esplicitamente chiamato 3 volte separate.

Il mio secondo punto riguarda enum_for e yield. enum_for crea un'istanza della classe Enumerator e anche questo oggetto Enumerator risponde al rendimento.

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" 

Quindi nota ogni volta che invochiamo i tipi con l'iteratore esterno, invocherà il rendimento solo una volta. La prossima volta che lo chiameremo, invocherà il prossimo rendimento e così via.

C'è un bocconcino interessante per quanto riguarda enum_for. La documentazione online afferma quanto segue:

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

Se non specifichi un simbolo come argomento su enum_for, ruby ​​aggancerà l'enumeratore a ciascun metodo del destinatario. Alcune classi non hanno un metodo ogni, come la classe String.

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

Pertanto, nel caso di alcuni oggetti invocati con enum_for, devi essere esplicito su quale sarà il tuo metodo di enumerazione.


0

La resa può essere utilizzata come blocco senza nome per restituire un valore nel metodo. Considera il seguente codice:

Def Up(anarg)
  yield(anarg)
end

È possibile creare un metodo "Su" a cui è assegnato un argomento. Ora puoi assegnare questo argomento a yield che chiamerà ed eseguirà un blocco associato. È possibile assegnare il blocco dopo l'elenco dei parametri.

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

Quando il metodo Up chiama yield, con un argomento, viene passato alla variabile di blocco per elaborare la richiesta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.