Programming Ruby

The Pragmatic Programmer's Guide

Precedente < Indice ^
Prossimot >

Contenitori, Blocchi e Ripetizioni



Un jukebox con una canzone diventerà difficilmente popolare (eccetto, forse, in qualche bar molto, ma molto spaventoso), così dobbiamo iniziare a pensare di produrre assai rapidamente un catalogo delle canzoni disponibili e una lista di quelle in attesa di essere riprodotte. Entrambi sono contenitori: oggetti che rimandano i riferimenti ad uno o più oggetti.

Sia il catalogo che la lista delle canzoni in attesa necessita di un insieme simile di metodi: aggiungere una canzone, rimuovere una canzone, restituire una lista di canzoni e così via. La lista delle canzoni in attesa necessita di ulteriori caratteristiche, quali l' avvertimento di inserimento ogni tante volte o tenere traccia della somma dei tempi di riproduzione (ma ci preoccuperemo di queste cose più avanti). Nel frattempo, sembrerebbe una buona idea sviluppare alcuni tipi di classi generiche SongList, che potremo specializzare all'interno dei cataloghi e delle liste di riproduzione.

Contenitori

Prima di iniziare l' implementazione, dobbiamo lavorare sul modo di memorizzare le liste di canzoni all' interno di un oggetto SongList. Abbiamo a disposizione tre ovvie possibilità. Potremo utilizzare il tipo di Array di Ruby, il tipo di Hash di Ruby o creare la nostra propria struttura. Essendo pigri, per ora guarderemo gli array e gli hash, e sceglieremo uno di questi per la nostra classe.

Array

La classe Array sostiene una collezione di riferimenti ad oggetti. Ogni riferimento ad un oggetto occupa una posizione nell'array, identificato da un indice intero non negativo.

potete creare gli array usando lettere o creando esplicitamente un oggetto Array. Un array letterale è semplicemente una lista di oggetti tra parentesi quadre.

a = [ 3.14159, "pie", 99 ]
a.type » Array
a.length » 3
a[0] » 3.14159
a[1] » "pie"
a[2] » 99
a[3] » nil
b = Array.new
b.type » Array
b.length » 0
b[0] = "second"
b[1] = "array"
b » ["second", "array"]

Gli array sono indicizzati usando l' operatore []. Come con molti operatori di Ruby, questo è attualmente un metodo (nella classe Array) e da qui può essere sovrascritto in sottoclassi. Come l' esempio mostra, l' indice array inizia da zero. Indicizza un array con un singolo intero, e ritorna l' oggetto corrispondente a quella posizione oppure ritorna un valore nullo se non esistono valori. Indicizzate un array con un valore negativo, e partirà nel conteggio dalla fine. Ci è mostrato nella Figura 4.1 a pagina 37.

Figura non disponibile...

a = [ 1, 3, 5, 7, 9 ]
a[-1] » 9
a[-2] » 7
a[-99] » nil

Potete anche indicizzare gli array con un paio di numeri, [start,| count]. Questo restituisce un nuovo array composto dai riferimenti agli oggetti count che iniziano dalla posizione start.

a = [ 1, 3, 5, 7, 9 ]
a[1, 3] » [3, 5, 7]
a[3, 1] » [7]
a[-3, 2] » [5, 7]

Finalmente, potete indicizzare gli array usando una lista, nella quale la posizione d' inizio e di fine sono separate da due o tre punti. Il modulo con i due punti include la posizione di fine, mentre quello a tre no.

a = [ 1, 3, 5, 7, 9 ]
a[1..3] » [3, 5, 7]
a[1...3] » [3, 5]
a[3..3] » [7]
a[-3..-1] » [5, 7, 9]

L' operatore [] ha un corrispondente operatore []=, che permette di fissare gli elementi nell'array. Se utilizzato con un solo indice intero, l' elemento in quella posizione verra sostituito dal valore sul lato destro dell' assegnamento qualunque esso sia. Qualsiasi varco sarà riempito da un valore nullo.

a = [ 1, 3, 5, 7, 9 ] » [1, 3, 5, 7, 9]
a[1] = 'bat' » [1, "bat", 5, 7, 9]
a[-3] = 'cat' » [1, "bat", "cat", 7, 9]
a[3] = [ 9, 8 ] » [1, "bat", "cat", [9, 8], 9]
a[6] = 99 » [1, "bat", "cat", [9, 8], 9, nil, 99]

Se l' indice []= è composto da due numeri (in cui un rappresenta inizio e l' altro la sua lunghezza) o un' intervallo, allora questi elementi nell'array originale saranno rimpiazzati con qualunque si trovi sul lato destro dell' assegnamento. Se la lunghezza è pari a zero, il lato destro è inserito all' interno dell' array prima della posizione d' inizio; nessun elemento verrà rimosso. Se il lato destro è un array egli stesso, i suoi elementi verranno utilizzati nella sostituzione. La dimensione dell' array è automaticamente adattata se l' indice seleziona un differente numero di elementi di quelli che sono disponibili sul lato destro dell' assegnamento.

a = [ 1, 3, 5, 7, 9 ] » [1, 3, 5, 7, 9]
a[2, 2] = 'cat' » [1, 3, "cat", 9]
a[2, 0] = 'dog' » [1, 3, "dog", "cat", 9]
a[1, 1] = [ 9, 8, 7 ] » [1, 9, 8, 7, "dog", "cat", 9]
a[0..3] = [] » ["dog", "cat", 9]
a[5] = 99 » ["dog", "cat", 9, nil, nil, 99]

Gli array hanno un grande numero di altri metodi utili. Usandoli, potrete trattare gli array come stacks, sets, queues, dequeues, and fifos. Una lista completa dei metodi degli array inizia a pagina 282.

Gli Hashes

Gli hashes (talvolta conosciuti come un' associazione di array o di dizionari) sono simili agli array, per il motivo che sono indicizzati gli insieme dei riferimenti dell' oggetto.

Comunque, finchè indicizzerete gli array con interi, potrete indicizzare un hash con un oggetto di qualsiasi tipo. L' esempio che segue usa hash letterali: una lista di chiave| =>| valore tra un paio di apici.

h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
h.length » 3
h['dog'] » "canine"
h['cow'] = 'bovine'
h[12]    = 'dodecine'
h['cat'] = 99
h » {"cow"=>"bovine", 12=>"dodecine", "dog"=>"canine", "donkey"=>"asinine", "cat"=>99}

Comparati con gli array, gli hashes hanno un significativo vantaggio: possono utilizzare qualsiasi oggetto come indice. Nonostante cio, hanno anche un significativo svantaggio: i loro elementi non sono ordinati, così non potete utilizzare un hash come uno stack o una queue.

troverete che gli hash sono uno delle strutture dati più comunemente utilizzate in Ruby. Una lista completa delle implementazioni dei metodi per la classe Hash inizia a pagina 321.

Implementazione di un contenitore di liste di canzoni

Dopo questa piccola digressione negli array e hash, siamo pronti ad implementare la SongList del jukebox. Inventiamo una lista di base dei metodi di cui avremo bisogno nella nostra SongList. Vorremo aggiungere ad essa delle implementazioni strada facendo, ma per ora ci limiteremo a queste.

append( aSong ) » list
Aggiunge la canzone selezionata alla lista.
deleteFirst() » aSong
Rimuove la prima canzone dalla lista, restituendo quella canzone
deleteLast() » aSong
Rimuove l' ultima canzone dalla lista, restituendo quella canzone.
[ anIndex } » aSong
Restituisce la canzone identificata da anIndex, la quale potrebbe essere un indice intero o un titolo di canzone.

Questa lista ci fornisce un bandolo dell' implementazione. L' abilità di aggiungere canzoni alla fine, e rimuoverle sia dalla cima che dal fondo, ci suggerisce una dequeue---a double-ended queue--- che noi sappiamo di poter implementare utilizzando un Array. Similmente, l' abilità di restituire una canzone come una posizione nell'intera lista è supportato dagli array.

Comunque, esiste la necessità di essere in grado di organizzare le canzoni per titolo, che potrebbe suggerire di usare un hash, con il titolo in qualità di chiave e la canzone come valore. Possiamo usare un hash? Bè, possiamo, ma esistono problemi. Dapprima gli hash non posso essere ordinati, così probabilmente potremo usare un array sussidiario per tenere traccia della lista. Un problema maggiore riguarda il fatto che gli hash non supportino chiavi multiple con lo stesso valore. Questo potrebbe rappresentare un problema per la nostra lista di canzoni, dove la stessa canzone potrebbe essere messa in coda per essere suonata un numero di volte. Così, per ora ci dedicheremo ad un array di canzoni, cercandolo i titoli di cui abbiamo bisogno. Se diverrà un collo di bottiglia per le performance, potremo sempre in seguito aggiungere alcuni tipi basati sugli hash.

Inizieremo la nostra classe con un metodo base initialize, il quale crea l' Array che useremo per mantere unite le canzoni e salvare un riferimento ad esso nella variabile istanza @songs.

class SongList
  def initialize
    @songs = Array.new
  end
end

Il metodo SongList#append aggiunge la canzone data alla fine dell' array @songs. Ciò restituisce anche self, un riferimento all' oggetto corrente SongList. Questa è una convenzione utile, poichè ci permette di legare insieme chiamate multiple da aggiungere. Osserveremo più avanti un esempio di ciò.

class SongList
  def append(aSong)
    @songs.push(aSong)
    self
  end
end

Poi aggiungeremo i metodi deleteFirst ed il deleteLast, banalmente implementato utilizzando rispettivamente Array#shift e Array#pop.

class SongList
  def deleteFirst
    @songs.shift
  end
  def deleteLast
    @songs.pop
  end
end

A questo punto, potrebbe essere il caso di eseguire un rapido test. Inizialmente aggiungeremo quattro canzoni alla lista. Giusto da mettere in risalto, utilizzeremo la cosa provata che append restituisce l' oggetto SongList per tenere unite insieme queste chiamate ai metodi.

list = SongList.new
list.
  append(Song.new('title1', 'artist1', 1)).
  append(Song.new('title2', 'artist2', 2)).
  append(Song.new('title3', 'artist3', 3)).
  append(Song.new('title4', 'artist4', 4))

Poi controlleremo che le canzoni siano raccolte correttamente dall' inizio e dalla fine della lista, e che il valore nil venga restituito quando la lista diventa vuota.

list.deleteFirst » Song: title1--artist1 (1)
list.deleteFirst » Song: title2--artist2 (2)
list.deleteLast » Song: title4--artist4 (4)
list.deleteLast » Song: title3--artist3 (3)
list.deleteLast » nil

Sembra funzionare. Il nostro prossimo metodo è [], il quale accede agli elementi in base all' indice. Se l' indice è un numero (che noi controlliamo utilizzando Object#kind_of?), restituiamo l' elemento corrispondente a quella posizione.

class SongList
  def [](key)
    if key.kind_of?(Integer)
      @songs[key]
    else
      # ...
    end
  end
end

Nuovamente, testare ciò è alquanto banale.

list[0] » Song: title1--artist1 (1)
list[2] » Song: title3--artist3 (3)
list[9] » nil

Ora abbiamo bisogno di aggiungere la funzionalità che ci permette di cercare una canzone in base al titolo. Ciò implica di scorrere attraverso le canzoni nella lista, confrontando il titolo di ciascuna. Per fare ciò, dapprima abbiamo bisogno di spendere un paio di pagine osservando una delle più interessanti caratteristiche di Ruby: gli iteratori.

Blocchi e Iteratori

Cosi', il nostro prossimo problema con SongList e' di implementare il codice nel metodo [] che prende una stringa e cerca una canzone con quel titolo. Questo sembra chiaro: abbiamo una matrice di canzoni, cosi' noi possiamo scorrerle uno alla volta cercando una corrispondenza valida.

class SongList
  def [](key)
    if key.kind_of?(Integer)
      return @songs[key]
    else
      for i in 0...@songs.length
        return @songs[i] if key == @songs[i].name
      end
    end
    return nil
  end
end

Cio' funziona e sembra confortevolmente familiare: un ciclo for scorre gli elementi del vettore. Cosa potrebbe essere piu' semplice?

Cio' che viene rivelato e' qualcosa di piu' naturale. In un certo senso il nostro ciclo for e' qualcosa di troppo intrinseco con l'array; cerca una lunghezza, quindi riceve valori finche' non trova una corrispondenza. Perche' non ricerca il vettore per applicare un test per ognuno dei suoi membri? Questo e' esattamente cio' che il metodo find fa nel Array.

class SongList
  def [](key)
    if key.kind_of?(Integer)
      result = @songs[key]
    else
      result = @songs.find { |aSong| key == aSong.name }
    end
    return result
  end
end

Potremmo usare if come una dichiarazione modifier per abbreviare il codice ulteriormente.

class SongList
  def [](key)
    return @songs[key] if key.kind_of?(Integer)
    return @songs.find { |aSong| aSong.name == key }
  end
end

Il metodo find e' un iteratore--un metodo che invoca un blocco di codice ripetitivamente. Gli iteratori e i blocchi di codice sono tra le piu' interessanti caratteristiche di Ruby:' soffermiamoci per osservarli ( e cosi' nel processo scopriremo che cosa fa quella linea di codice nel nostro metodo []).

Implementare gli iteratori

Un iteratore di Ruby e' semplicemente un metodo che puo' invocare un blocco di codice. A prima vista un blocco in Ruby assomiglia a un blocco in C, Java o Perl. Sfortunatamente, in questo caso sembra che siano stati ingannati--un blocco di Ruby e' un metodo per raggruppare asserzioni, ma non nel modo convenzionale.

Primo, un blocco puo' apparire solamente nel sorgente adiacente al metodo chiamato; il blocco e' scritto iniziando sulla stessa linea come l'ultimo parametro del metodo. Secondo, il codice del blocco non e' eseguito mentre ci si imbatte nello stesso. Invece, Ruby ricorda il contesto nel quale il blocco appare ( la variabile locale, l'oggetto corrente, e cosi' via), e poi esegue il metodo. Quì e' dove inizia la magia.

All'interno del metodo, il blocco puo' essere invocato, quasi come se fosse un metodo egli stesso, utilizzando il costrutto yield. Ogni qual volta un yield e' eseguito, esso invoca il codice nel blocco. Quando il blocco e' terminato, il controllo ritorna immediatamente dopo yield.[Programming-language buffs will be pleased to know that the keyword yield was chosen to echo the yield function in Liskov's language CLU, a language that is over 20 years old and yet contains features that still haven't been widely exploited by the CLU-less.] Incominciamo con un semplice esempio.

def threeTimes
  yield
  yield
  yield
end
threeTimes { puts "Hello" }

produce:

Hello
Hello
Hello

Il blocco (il codice tra le graffe) e' associato con la chiamata al metodo threeTimes. All'interno di questo metodo, yield e' chiamato 3 volte in una riga. Ogni volta richiama il codice nel blocco, e un simpatico saluto e' stampato. Cio' che rende interessanti i blocchi, comunque e' che tu potete passargli i parametri e riceverne i valori. Per esempio si puo' scrivere una semplice funzione che restituisce i membri della serie di Fibonacci fino ad un certo valore.[La serie di Fibonacci e' una sequenza di interi che iniziano con due 1 nei quali ogni termine seguente e' la somma dei due precedenti. La serie e' usata talvolta per gli algoritmi di ordinamento e nell'analisi dei fenomeni naturali.]

def fibUpTo(max)
  i1, i2 = 1, 1        # assegnamento parallelo
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fibUpTo(1000) { |f| print f, " " }

produce:

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

In questo esempio il costrutto yield ha un parametro. Questo valore e' passato al blocco associato. Nella definizione di blocco la lista di argomenti appare tra barre verticali. In questa istanza, la variabile f, riceve il valore passato a yield, cosi' che il blocco stampa i successivi membri della serie. (Questo esempio mostra anche gli assegnamenti paralleli in azione, Torneremo indietro per questo a pagina 77). Sebbene sia comune passare un unico valore al blocco, cio' non e' richiesto; un blocco puo' avere un qualsiasi numero di elementi. Cosa succede se un blocco ha un numero differenti di parametri rispetto a quelli che sono dati a yield? Per una coincidenza vacillante, le regole di cui abbiamo discusso al riguardo delle assegnamenti paralleli torna a fagiolo ( con una sottile alterazione: i parametri multipli passati a yield sono convertiti in una vettore se il blocco ha gia' un argomento).

I parametri al blocco potrebbero essere variabili locali gia' esistenti; se fosse cosi' il nuovo valore della variabile sara' mantenuto dopo che il blocco terminera'. Cio' puo' condurre ad un comportamento inatteso-, ma c'e' anche un guadagno prestazionale d'aggiungere utilizzando variabili che gia' esistono. [ Per maggiori informazioni su queste ed altre ``gotchas,'' osserva la lista che incomincia a pagina 129; maggiori informazioni sulle prestazioni incominciano a pagina 130.]

Un blocco puo' anche restituire un valore al metodo. Il valore dell'ultima espressione presa in considerazione nel blocco e' restituita al metodo come valore di yield. Questo e' il motivo per cui il metodo find usato da una classe Array funziona.[Il metodo find e' attualmente definito nel modulo Enumerable, che e' mischiato nella classe Array.] La sua implementazione potrebbe assomigliare a qualcosa di simile al codice seguente.

class Array
  def find
    for i in 0...size
      value = self[i]
      return value if yield(value)
    end
    return nil
  end
end
[1, 3, 5, 7, 9].find {|v| v*v > 30 } » 7

Questo passa gli elementi successivi di un vettore al blocco associato. Se il blocco restituisce true, il metodo restituisce l'elemento corrispondente. Se nessun elemento e' stato trovato il metodo restituisce nil. L'esempio mostra i benefici di questo approccio agli iteratori. La classe Array fa quello che fa meglio, accedendo agli elementi del vettore lasciando il codice dell'applicazione per concentrarsi nella suo particolare requisito (in questo caso, trovando un valore che soddisfi alcuni criteri matematici).

Alcuni iteratori sono comuni a molti tipo di collezioni di Ruby. Abbiamo gia' inoltrato find. Altri due sono each e collect. Each e' probabilmente l'iteratore pu' semplice cio' che fa e' proporre i successivi elementi della sua collezione.

[ 1, 3, 5 ].each { |i| puts i }

produce:

1
3
5

L' iteratore each ha un posizione particolare in Ruby; a pagina 87 descriveremo come e' usato alla base del ciclo for del linguaggio, e iniziando da pagina 104 vedremo come definendo un metodo each si possa aggiungere una mole di funzionalita' alla vostra classe senza fatica.

Un altro iteratore comune e' collect, il quale prende ciascun elemento della collezione e lo passa al blocco. I risultati ritornati al blocco sono utilizzati per costruire un nuovo vettore. Per esempio:

["H", "A", "L"].collect { |x| x.succ } » ["I", "B", "M"]

Ruby messo a confronto con il C++ ed il Java

Vale la pena spendere un paragrafo per mettere a confronto l' approccio di Ruby agli iteratori rispetto al C++ ed il Java. Nell'approccio a Ruby, l' iteratore è semplicemente un metodo, identico a tutti gli altri, che succede a chiamare yield ogniqualvolta genera un nuovo valore. La cosa che usa l' iteratore è semplicemente un blocco di codice associato a questo metodo. Non è necessario generare una classe assistente per sostenere lo stato dell' iteratore, esattamente come nel Java e nel C++. In questo, come in molte altre cose, Ruby è un linguaggio trasparente. Quando scrivi un programma in Ruby, ti concentri nella realizzazione del lavoro, non nel realizzare punteggi che supportino il linguaggio stesso.

Gli Iteratori non si limitano ad accedere ai dati negli arrays e hashes. Come abbiamo visto nell'esempio di Fibonacci, un iteratore può restituire valori derivati. Questa capacità è utilizzata dalle classi di input/output di Ruby, le quali implementano un' interfaccia dell' iteratore restituendo linee ( o byte) successive in un flusso I/O.

f = File.open("testfile")
f.each do |line|
  print line
end
f.close

produce:

This is line one
This is line two
This is line three

E così avanti...

Osserviamo ancora una implementazione di un iteratore. Il linguaggio Smalltalk supporta anche gli iteratori al di sopra delle collezioni. Se chiedete ad un programmatore Smalltalk di sommare gli elementi di un array, è facile che possa usare una funzione inject .

sumOfValues              "metodo Smalltalk"
    ^self values
          inject: 0
          into: [ :sum :element | sum + element value]

inject lavora in questo modo. ll blocco associato è chiamato una prima volta, sum è settato ai parametri di inject (zero in questo caso), ed element è settato al primo elemento dell' array. La seconda e successiva volta in cui il blocco è chiamato, sum è fissato al valore di ritorno dalla precedente chiamata. In questo modo, sum può essere usato per un funzionamento completo. Il valore finale di inject è il valore restituito dall' ultima chiamata al blocco.

Ruby non ha un metodo inject, ma è semplice scriverlo. In questo caso lo aggiungeremo alla classe Array , in tanto che a pagina 102 vedremo come renderlo disponibile in senso generale.

class Array
  def inject(n)
     each { |value| n = yield(n, value) }
     n
  end
  def sum
    inject(0) { |n, value| n + value }
  end
  def product
    inject(1) { |n, value| n * value }
  end
end
[ 1, 2, 3, 4, 5 ].sum » 15
[ 1, 2, 3, 4, 5 ].product » 120

Sebbene i blocchi sono spesso l' obiettivo di un iteratore, hanno anche degli altri usi. Ne osserviamo alcuni.

Blocchi per Transazioni

I blocchi possono essere usati per definire una grossa porzione di codice che deve funzionare sotto qualche tipo di controllo transazionale. Per esempio, aprirete spesso un file, operando sui suoi contenuti, e per finire vorrete essere sicuro che verrà chiuso. Per quanto possiate fare ciò usando del codice convenzionale, esiste un argomento per rendere il file responsabile della propria chiusura. Possiamo realizzarlo con i blocchi. Una semplice implementazione (ignorando gli errori di manipolazione) potrebbe assomigliare a qualcosa che segue.

class File
  def File.openAndProcess(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end

File.openAndProcess("testfile", "r") do |aFile|
  print while aFile.gets
end

produce:

This is line one
This is line two
This is line three

E così avanti...

Questo piccolo esempio illustra un numero di tecniche. Il metodo openAndProcess è un class method---potrebbe essere chiamato indipendentemente dal particolare oggetto di un File . Vogliamo che prenda gli stessi argomenti come il metodo convenzionale File::open , ma non ci importa realmente quali siano questi argomenti. In altre parole, specifichiamo l' argomento come *args, che significa ``colleziona gli attuali parametri passati al metodo dentro un array.'' Di seguito chiamiamo File.open, passandogli *args come un parametro. Questo espande l' array indietro fino ai parametri individuali. Il vantaggio è che openAndProcess passa trasparentemente qualsiasi parametro riceva da File::open.

Una volta che il file è stato aperto, openAndProcess chiama yield, passando l' oggetto del file aperto al blocco. Quando il blocco ritorna, il file è chiuso. In questo modo, la responsabilità di chiudere un file aperto è passato dall' utente degli oggetti del file al file stesso.

Alla fine, questo esempio utilizza do...end per definire un blocco. L' unica differenza tra questa notazione e l' utilizzo delle graffe per definire un blocco è la precedenza: do...end lega ad un livello più basso rispetto a ``{...}''. Ne riparleremo a pagina 236.

La tecnica di avere i files che si controllano da sè è così utile che la classe File è fornita direttamente di supporti. Se File::open ha un blocco associato, allora quel blocco sarà richiamato con un oggetto file, ed il file verrà chiuso quando il blocco termina. Ciò è interessante, come voler dire che File::open ha due differenti comportamenti: quando è chiamato con un blocco, esegue il blocco e chiude il file. Quando è chiamato senza un blocco, restituisce l' oggetto del file. Questo è reso possibile dal metodo Kernel::block_given?, che restituisce true se un blocco è associato al metodo corrente. Usandolo, potreste implementare File::open (nuovamente ignorando gli errori di manipolazione) usando qualche cosa come il seguente.

class File
  def File.myOpen(*args)
    aFile = File.new(*args)
    # se esiste un blocco, passa nel file e lo chiude
    # quando ritorna
    if block_given?
      yield aFile
      aFile.close
      aFile = nil
    end
    return aFile
  end
end

I blocchi possono essere chiusi

Torniamo indietro al nostro jukebox per un momento (ricordate il jukebox?). Ad un certo punto staremo lavorando sul codice che tratta l' interfaccia utente---i bottoni che le persone schiacciano per selezionare le canzoni e controllare il jukebox. Vorremo associare le azioni a quei bottoni: schiacci STOP e la musica si ferma. Ciò produce che i blocchi di Ruby sono un modo conveniente per realizzarlo. Partiamo dal presupposto che la persona che ha fatto l' hardware ha implementato una estensione di Ruby che ci fornisce una classe di base per un bottone. (Parliamo delle estensioni di Ruby da pagina 171)

bStart = Button.new("Start")
bPause = Button.new("Pause")
# ...

Che cosa accade quando l' utente schiaccia uno dei nostri bottoni? Nella classe Button , la gente dell' hardware ha provveduto affinchè un metodo di chiamata di ritorno, buttonPressed, sarà invocato. La strada ovvia per aggiungere funzionalità a questi bottoni è di creare una sottoclasse di Button e avere per ogni implementazione della sottoclasse il suo proprio metodo buttonPressed.

class StartButton < Button
  def initialize
    super("Start")       # invoca l' inizializzazione di Button
  end
  def buttonPressed
    # do start actions...
  end
end

bStart = StartButton.new

A questo punto esistono due problemi. Il primo, questo condurrà ad un grande numero di sottoclassi. Se l' interfaccia di Button cambia, questo ci impegolerà in un grande dispendio di manutenzione. Il secondo, le azioni eseguite quando un bottone è schiacciato sono espresse ad un livello sbagliato; esse non sono caratteristiche del bottone, ma sono caratteristiche del jukebox che usa il bottone. Si possono fissare entrambi i problemi utilizzando i blocchi.

class JukeboxButton < Button
  def initialize(label, &action)
    super(label)
    @action = action
  end
  def buttonPressed
    @action.call(self)
  end
end

bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }

La chiave a tutto questo è il secondo parametro di JukeboxButton#initialize. Se l' ultimo parametro nella definizione del metodo è prefissato da una e commerciale (come nel nostro caso da &action), Ruby cerca un blocco di codice ovunque sia il metodo chiamato. Quel blocco di codice è trasformato in un oggetto della classe Proc ed assegnato al parametro. A questo punto si può trattare il parametro come qualsiasi altra variabile. Nel nostro esempio, l' abbiamo assegnata alla variabile istanza @action. Quando il metodo di chiamata di ritorno buttonPressed è invocato, utilizziamo il metodo Proc#call su quell'oggetto per invocare il blocco.

Ora, che cosa dobbiamo realmente fare per creare un oggetto Proc ? La cosa interessante è che è di più di un grosso pezzo di codice. Associato ad un blocco (e di quì un oggetto Proc ) è tutto il contesto in cui il blocco è stato definito: il valore di self, il metodo, le variabili, ed il contesto in proposito. Parte della magia di Ruby risulta nel fatto che il blocco può usare ancora tutti queste informazioni per lo scopo originale anche se la variabile nella quale è stata definita fosse stata in altro modo eliminata, questa caratteristica è denominata una closure.

Osserviamo questo esempio inventato. Questo esempio utilizza il metodo proc, che trasforma un blocco in un oggetto Proc .

def nTimes(aThing)
  return proc { |n| aThing * n }
end
p1 = nTimes(23)
p1.call(3) » 69
p1.call(4) » 92
p2 = nTimes("Hello ")
p2.call(3) » "Hello Hello Hello "

Il metodo nTimes restituisce un oggetto Proc a cui si riferiscono i parametri del metodo, aThing. Anche se quel parametro sia al di fuori dello scopo nel momento in cui è chiamato, il parametro rimane accessibile al blocco.


Precedente < Indice ^
Prossimo >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0.
This reference is available for download.