|
|||
Precedente < | Indice ^ |
Prossimot > |
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.
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.
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 (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.
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.
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.
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 []
).
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"] |
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.
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 |
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.