9.  Le Classi

 

Finora abbiamo visto diversi tipi, o classi, di oggetti: stringhe, numeri interi (integers), numeri in virgola mobile (floats), array, e alcuni oggetti speciali (true, false, e nil) dei quali parleremo più avanti. In Ruby, queste classi si scrivono sempre con l'iniziale maiuscola: String, Integer, Float, Array... etc. In generale, se vogliamo creare un oggetto di una determinata classe, usiamo il comando new:

a = Array.new  + [12345]  # Somma di Array.
b = String.new + 'ciao'  #  Somma di Stringhe.
c = Time.new

puts 'a = '+a.to_s
puts 'b = '+b.to_s
puts 'c = '+c.to_s
a = 12345
b = ciao
c = Tue Apr 14 16:29:20 GMT 2009

Siccome possiamo creare array e stringhe scrivendo rispettivamente [...] and '...', raramente li creiamo usando new. (Sebbene non sia proprio ovvio dall'esempio qui sopra, String.new crea una stringa vuota, mentre Array.new crea un array vuoto.) Inoltre, i numeri costituiscono un'eccezione: non puoi creare un integer con Integer.new. Per creare un numero intero, devi semplicemente scriverlo.

La Classe Time

Allora, che storia è questa classe Time? Gli oggetti Time rappresentano momenti nel tempo. Puoi aggiungere (o sottrarre) numeri ai (o dai) tempi per ottenere nuovi tempi: aggiungere 1.5 a un tempo crea un tempo di un secondo e mezzo dopo:

time  = Time.new   #  Il momento in cui ho generato questa pagina web.
time2 = time + 60  #  Un minuto dopo.

puts time
puts time2
Tue Apr 14 16:29:20 GMT 2009
Tue Apr 14 16:30:20 GMT 2009

Puoi anche creare un tempo per uno specifico momento usando Time.mktime (NdT: 'mk' sta per 'make' che in italiano, tra le altre cose, si può tradurre in 'crea'):

puts Time.mktime(2000, 1, 1)          #  Y2K.
puts Time.mktime(1976, 8, 3, 10, 11)  #  Quando sono nato.
Sat Jan 01 00:00:00 GMT 2000
Tue Aug 03 10:11:00 GMT 1976

NB: quello è il tempo in cui sono nato secondo il fuso orario "Pacific Daylight Savings Time" (PDT). Quando invece incombette l'Y2K, era secondo il fuso orario "Pacific Standard Time (PST)", perlomeno per noi della West Coast. Le parentesi servono a raggruppare i paramentri da passare a mktime. Più parametri specifichi, più accurato è il tempo che ottieni.

Puoi comparare questi tempi usando i metodi di comparazione (un tempo precedente è minore di un tempo successivo), e se sottrai un tempo da un altro, otterrai il numero di secondi fra di essi (in altre parole, l'intervallo compreso in secondi). Giochiamoci un po'!

Un Po' di Cose da Provare

  • Un miliardo di secondi... Trova l'esatto secondo in cui sei nato (se puoi). Quindi scopri quando compirai (o magari quando hai già compiuto) il tuo miliardesimo secondo. E poi vallo a segnare sul tuo calendario!
  • Buon compleanno! Chiedi in che anno una persona è nata, poi il mese e poi il giorno. Calcola quanto è anziana e quindi dagli un bel SPANK! per ogni compleanno che hanno passato [NdT: il programma deve stampare a video la scritta "SPANK!" (che in inglese è il suono onomatopeico per un buffetto, ma anche per una sculacciata) per ogni compleanno passato].

La Classe Hash

Un'altra classe utilissima è la classe degli Hash. Gli hash (che in italiano si traduce, tra le altre cose, in 'mucchio disordinato') sono molto simili agli array: hanno un mucchio di slot o posizioni che possono puntare a diversi oggetti. Tuttavia, in un array, gli slot sono in fila e ciascuna posizione è numerata (a partire da zero). In un hash, invece, gli slot non sono in fila (sono piuttosto mescolati insieme), ed è possibile utilizzare qualsiasi oggetto per riferirsi a una posizione, non solo un numero. E' buona norma usare gli hash quando si hanno parecchie cose di cui si vuole tenere traccia, ma che non rientrano in un elenco ordinato. Per esempio, i colori che uso per le diverse parti del codice che ha generato questo tutorial:

colorArray = []  #  un array come Array.new
colorHash  = {}  #  un hash come Hash.new

colorArray[0]          = 'rosso'
colorArray[1]          = 'verde'
colorArray[2]          = 'blu'
colorHash['stringhe']  = 'rosso'
colorHash['numberi']   = 'verde'
colorHash['keywords']  = 'blu'

colorArray.each do |colore|
  puts colore
end
colorHash.each do |tipoColore, colore|
  puts tipoColore + ':  ' + colore
end
rosso
verde
blu
stringhe:  rosso
keywords:  blu
numeri:  verde

[NdT: "keywords" significa "parole chiave".]
Se avessi utilizzato un array, mi sarei dovuto ricordare che la posizione 0 è per le stringhe, che la posizione 1 è per i numeri, etc. Ma se invece uso un hash, è facile! La posizione 'stringhe' contiene il colore delle stringhe, ovviamente. Niente da ricordare. Potresti aver notato che quando ho usato each, gli oggetti nell'hash non sono saltati fuori nello stesso ordine in cui li avevamo inseriti. Gli array servono a tenere le cose in ordine, gli hash no.

Sebbene i programmatori usano solitamente delle stringhe per riferirsi agli slot in un hash, si può davvero utilizzare ogni tipo di oggetto, perfino array e altri hash (anche se proprio non mi viene in mente perché vorresti mai fare una cosa del genere...):

# encoding: utf-8
weirdHash = Hash.new

stranoHash[12] = 'scimmiette'
stranoHash[[]] = 'senso di vacuità'
stranoHash[Time.new] = 'il tuo presente è qui, adesso!'

Gli hash e gli array sono buoni per cose diverse; sta a te decidere quale dei due rappresenta la soluzione migliore per il problema che stai in quel momento affrontando.

Estendere le Classi

Alla fine dello scorso capitolo hai scritto un metodo per ottenere la scrittura in inglese di un dato numero intero. Non si trattava però di un metodo degli interi, era un metodo solo un metodo generico del programma. Non sarebbe molto più bello poter scrivere semplicemente qualcosa tipo 22.to_eng al posto di englishNumber 22? Ed ecco come ci si riesce (ricorda che in Ruby la classe dei "Numeri Interi" è la classe "Integer", NdT):

# encoding: utf-8
class Integer
  
  def to_eng
    if self == 5
      english = 'five'
    else
      english = 'fifty-eight'
    end
    
    english
  end

end

#  Meglio fare un test con un paio di numeri "a caso"...
puts 5.to_eng
puts 58.to_eng
five
fifty-eight

Bene, sembra funzionare! ;)

Quindi abbiamo definito un metodo degli interi saltando dentro la loro classe Integer, definendo lì dentro il metodo, e poi saltando di nuovo fuori. Ora tutti gli interi hanno questo metodo (sebbene ancora incompleto). In effetti, se non ti piace il modo in cui si comporta un metodo incorporato nel linguaggio Ruby come to_s, potresti semplicemente ridefinirlo esattamente nella stessa maniera di come abbiamo definito to_eng... ma questa cosa non te la raccomando! E' meglio lasciare in pace i vecchi metodi e piuttosto definirne di nuovi quando ti serve qualche nuova funzionalità.

Allora... già confuso/a? Lasciami tornare su quest'ultimo programma un altro po'. Finora, ogni volta che abbiamo eseguito del codice o definito qualche metodo, lo abbiamo fatto nell'oggetto "programma". Nel nostro ultimo programma invece, abbiamo lasciato quell'oggetto per la prima volta e siamo andati nella classe Integer. Lì abbiamo definito un metodo (il che lo rende un metodo dei "Numeri Interi" o "Integers") che quindi ora tutti gli integer possono usare. All'interno di quel metodo usiamo la parola chiave self per riferirci all'oggetto (l'integer) che sta eseguendo il metodo. (NdT: "self" in inglese significa, tra le altre cose, "se stesso").

Creare Classi

Abbiamo visto un certo numero di diverse classi di oggetti. Tuttavia, è facile ottenere tipi di oggetti che Ruby nativamente non ha. Fortunatamente, creare una nuova classe è facile come estenderne una già esistente. Immaginiamo di voler ottenere dei dadi da gioco in Ruby. Ecco come potremmo definire la classe Dado:

class Dado
  
  def lancia
    1 + rand(6)
  end
  
end

#  Creiamo una coppia di dadi...
dadi = [Dado.new, Dado.new]

#  ...e ora lanciamoli!
dadi.each do |dado|
  puts dado.lancia
end
3
4

(Se hai saltato la parte sui numeri random (o casuali) sappi che rand(6) semplicemente restituisce un numero random compreso tra 0 e 5.)

E questo è tutto! Ora sai creare oggetti del tutto personali.

Possiamo definire ogni tipo di metodi per i nostri oggetti... ma non è tutta la storia. Lavorare con questi oggetti assomiglia molto al programmare prima di aver imparato cosa siano le variabili. Consideriamo i nostri nuovi dadi, per esempio. Possiamo lanciarli, e ogni volta che lo facciamo ci danno dei risultati diversi. Ma se volessimo tenere un numero, dovremmo creare una variabile che punti a quel numero. Eppure un dado decente dovrebbe avere un numero, e il lancio dovrebbe solo cambiarlo. Se teniamo traccia del dado, non dovremmo avere bisogno di tenere traccia anche del numero che in quel momento sta mostrando.

Tuttavia, se proviamo a salvare il numero ottenuto dal lancio in una variabile (locale) in lancia, sarà persa non appena lancia ha finito. Abbiamo bisogno di salvare il numero in un diverso tipo di variabile:

Variabili d'Istanza

Normalmente quando vogliamo parlare di una stringa la possiamo semplicemente chiamare stringa. Tuttavia, potremmo anche chiamarla oggetto stringa. Alle volte i programmatori potrebbero chiamarla un'istanza della classe String, ma questo è solo un modo pignolo (e abbastanza prolisso) di dire stringa. Un'istanza di una classe è semplicemente un oggetto di quella classe.

Quindi le variabili d'istanza sono semplicemente le variabili di un oggetto. Le variabili locali di un metodo, come abbiamo visto, esistono solo finché il metodo è in esecuzione. Le variabili d'istanza di un oggetto, d'altro canto, esistano finché esiste l'oggetto. Per distinguere le variabili d'istanza dalle variabili locali, il nome delle prime comincia sempre con una @:

class Dado
  
  def lancia
    @facciaInAlto = 1 + rand(6)
  end
  
  def punteggio
    @facciaInAlto
  end
  
end

die = Dado.new
die.lancia
puts dado.punteggio
puts dado.punteggio
die.lancia
puts dado.punteggio
puts dado.punteggio
1
1
5
5

Molto bene! Quindi lancia lancia il dado e punteggio ci dice quale faccia sta mostrando il dado. Ma che accadrebbe se ci chiedessimo quale faccia sta mostrando un dado che non abbiamo ancora lanciato (prima cioè di assegnare un valore a @facciaInAlto)?

# encoding: utf-8
class Dado
  
  def lancia
    @facciaInAlto = 1 + rand(6)
  end
  
  def punteggio
    @facciaInAlto
  end
  
end

#  Siccome non riutilizzerò questo dado,
#  Non ho bisogno di salvarlo in una variabile.
puts Dado.new.punteggio
nil

Uhm... beh, almeno non ci ha dato un errore. Eppure non ha proprio senso per un dato il non mostrare una faccia prima di essere stato lanciato. Un vero dado, quando lo guardiamo, non risponderebbe mai nil. Sarebbe bello se potessimo inizializzare il nostro nuovo dato in modo tale che mostri comunque una faccia anche se non è stato ancora lanciato. E il comando initialize serve proprio a questo:

# encoding: utf-8
class Dado
  
  def initialize
    #  Mi limiterò a lanciare il dado, ma potremmo
    #  fare qualcosa di diverso se volessimo
    #  come settare come faccia in alto il 6.
    lancia
  end
  
  def lancia
    @facciaInAlto = 1 + rand(6)
  end
  
  def punteggio
    @facciaInAlto
  end
  
end

puts Dado.new.punteggio
3

Appena un oggetto viene creato, il suo metodo initialize (se ne ha uno definito) viene subito chiamato.

I nostri dadi sono quasi perfetti. L'unica cosa che potrebbe mancare è un metodo per impostare manualmente il valore della faccia che sta mostrando... potresti scrivere un metodo imbroglia che faccia proprio questo prendendo un numero come parametro! Torna qui quando hai finito (e quando hai testato che funziona, ovviamente). Assicurati che nessuno possa imbrogliare in modo che il dado mostri come punteggio il numero 7!

Abbiamo visto un bel po' di cosette interessanti. Si tratta però di argomenti un po' difficili, quindi lascia che ti scriva un altro e più interessante esempio. Diciamo di voler creare un animaletto domestico virtuale, un cucciolo di drago. Come la tutti i cuccioli, dev'essere in grado di mangiare, dormire e fare la cacca, il che significa che dovremmo essere in grado di anche di dargli da mangiare, metterlo a nanna e portarlo a passeggio. Internamente, il nostro drago, avrà bisogno di tener traccia del fatto che sia o meno affamato, stanco o che abbia o meno bisogno di andare in bagno. Ma noi non saremo in grado di vederlo esternamente, esattamente come non puoi chiedere a un neonato umano "Hai fame?". Aggiungeremo anche un altro paio di modi divertenti di interagire col nostro dragoncino, e sarà nostra cura dargli un nome appena nato (Qualsiasi cosa passi come parametro al metodo new viene passata per te al metodo initialize). Ok, è tempo di programmare:

# encoding: utf-8
class Dragone
  
  def initialize nome
    @nome = nome
    @addormentato = false
    @robaInPancia     = 10  #  E' pieno.
    @robaNellIntestino =  0  #  Non ha bisogno di sedersi sul vasino.
    
    puts @nome + ' è nato.'
  end
  
  def daiDaMangiare
    puts 'Hai dato da mangiare a ' + @nome + '.'
    @robaInPancia = 10
    passaIlTempo
  end
  
  def portaAPasseggio
    puts 'Hai portato a passeggio ' + @nome + '.'
    @robaNellIntestino = 0
    passaIlTempo
  end
  
  def mettiALetto
    puts 'Hai messo ' + @nome + ' a letto.'
    @addormentato = true
    3.times do
      if @addormentato
        passaIlTempo
      end
      if @addormentato
        puts @nome + ' sta russando, riempiendo la stanza di fumo.'
      end
    end
    if @addormentato
      @addormentato = false
      puts @nome + ' si sveglia lentamente.'
    end
  end
  
  def lanciaInAria
    puts 'Fai volare ' + @nome + ' su nell\'aria.'
    puts @nome + ' fa una risatina, che ti brucia le sopracciglia.'
    passaIlTempo
  end
  
  def dondola
    puts 'Fai dondolare ' + @nome + ' dolcemente.'
    @addormentato = true
    puts 'In un attimo si assopisce...'
    passaIlTempo
    if @addormentato
      @addormentato = false
      puts '…ma si sveglia appena smetti.'
    end
  end
  
  private
  
  #  "private" significa che i metodi definiti qui
  #  sono interni all'oggetto.  (Puoi dare da mangiare
  #  al tuo dragone, ma non puoi chiedergli se è affamato.)
  
  def affamato?
    #  I nomi dei metodi possono terminare con un "?".
    #  Di solito però chiamiamo così solo i metodi
    #  che devono restituire true o false, come questo:
    @robaInPancia <= 2
  end
  
  def scappa?
    @robaNellIntestino >= 8
  end
  
  def passaIlTempo
    if @robaInPancia > 0
      #  Sposta cibo dalla pancia all'intestino.
      @robaInPancia     = @robaInPancia     - 1
      @robaNellIntestino = @robaNellIntestino + 1
    else  #  Il nostro dragone sta morendo di fame!
      if @addormentato
        @addormentato = false
        puts 'Si sveglia di colpo!'
      end
      puts @nome + ' sta morendo di fame! Disperato, mangia TE!'
      exit  #  Questo termina il programma.
    end
    
    if @robaNellIntestino >= 10
      @robaNellIntestino = 0
      puts 'Ooops!  ' + @nome + ' ha avuto un incidente...'
    end
    
    if affamato?
      if @addormentato
        @addormentato = false
        puts 'Si sveglia di colpo!'
      end
      puts 'Lo stomaco di ' + @nome + ' brontola...'
    end
    
    if scappa?
      if @addormentato
        @addormentato = false
        puts 'Si sveglia di colpo!'
      end
      puts @nome + ' fa la danza del vasino...'
    end
  end
  
end

pet = Dragone.new 'Norbert'
pet.daiDaMangiare
pet.lanciaInAria
pet.portaAPasseggio
pet.mettiALetto
pet.dondola
pet.mettiALetto
pet.mettiALetto
pet.mettiALetto
pet.mettiALetto
Norbert è nato.
Hai dato da mangiare a Norbert.
Fai volare Norbert su nell'aria.
Norbert fa una risatina, che ti brucia le sopracciglia.
Hai portato a passeggio Norbert.
Hai messo Norbert a letto.
Norbert sta russando, riempiendo la stanza di fumo.
Norbert sta russando, riempiendo la stanza di fumo.
Norbert sta russando, riempiendo la stanza di fumo.
Norbert si sveglia lentamente.
Fai dondolare Norbert dolcemente.
In un attimo si assopisce...
…ma si sveglia appena smetti.
Hai messo Norbert a letto.
Si sveglia di colpo!
Lo stomaco di Norbert brontola...
Hai messo Norbert a letto.
Si sveglia di colpo!
Lo stomaco di Norbert brontola...
Hai messo Norbert a letto.
Si sveglia di colpo!
Lo stomaco di Norbert brontola...
Norbert fa la danza del vasino...
Hai messo Norbert a letto.
Si sveglia di colpo!
Norbert sta morendo di fame! Disperato, mangia TE!

Caspita! Ovviamente, sarebbe più bello se fosse un programma interattivo, ma puoi svilupparlo in tal senso più tardi. Stavo solo cercando di mostrarti le parti direttamente correlate alla definizione di una nuova classe Dragone.

Abbiamo visto alcune cose nuove in questo esempio. La prima è semplice: exit termina il programma su due piedi. La seconda è la parola private che abbiamo piazzato proprio nel mezzo della definizione della nostra classe. Avrei potuto ometterla, ma ho voluto rafforzare l'idea per cui alcuni metodi sono cose che puoi fare a un dragone mentre altri sono cose che semplicemente accadono al suo interno. Puoi pensare a questi metodi "privati" come a un "dietro le quinte": a meno che tu non sia un meccanico automobilistico, tutto ciò con cui ti serve interagire per guidare una macchina sono il suo acceleratore, freno e volante. Un programmatore chiamerebbe queste cose l'interfaccia pubblica della tua macchina. Come fa il tuo airbag a sapere quando esplodere, tuttavia, è un qualcosa interno alla macchina; l'utente tipico (il conducente) non ha bisogno di sapere come funziona.

Nondimeno, per avere un'idea un po' più concreta, vediamo come potresti rappresentare una macchina in un videogioco (cosa che risulta essere il mio tipo di lavoro). Per prima cosa, dovresti decidere come dovrebbe essere la tua interfaccia pubblica; in altre parole, quali metodi devono poter chiamare gli utenti sui tuoi oggetti di classe macchina? Beh, devono essere in grado di premere l'acceleratore e il freno, ma dovrebbero poter anche specificare quanto forte stanno premendo il pedale. (C'è una gran differenza fra il premerlo a tavoletta e il dare un colpetto). Dovrebbero anche essere in grado di sterzare, e ancora, dovrebbero essere in grado di quantificare quanto forte lo stanno facendo. Suppongo che ci si potrebbe spingere oltre e considerare la frizione, il cambio, le frecce, il turbo, il sedile eiettabile, il canalizzatore di flusso, etc... dipende dal tipo di gioco che stai programmando.

All'interno di un oggetto macchina, comunque, ci sarebbe molto di più; altre cose di cui una macchina avrebbe bisogno sono una velocità, una direzione, una posizione (come minimo). Questi attributi sarebbero modificati agendo sui pedali e sullo sterzo, ovviamente, ma l'utente non sarebbe capace di impostare direttamente la posizione (il che assomiglierebbe a un teletrasporto). Potresti anche voler tener traccia di sbandamento e danni, del livello del carburante, usura delle gomme e così via. Tutto questo sarebbe interno (privato) rispetto al tuo oggetto macchina.

Un Po' di Cose da Provare

  • Definisci una classe albero Arancio. Dovrebbe avere un metodo altezza che restituisca la sua altezza, e un metodo passaUnAnno che, quando chiamato, faccia invecchiare l'albero di un anno. Ogni anno l'albero diventa più alto (di quanto pesi che un arancio debba crescere in un anno), e dopo un certo numero di anni (ancora, una tua scelta) l'albero dovrebbe morire. Per i primissimi anni, non dovrebbe produrre frutta, ma dopo qualche anno dovrebbe, e scommetto che gli alberi tendono a produrre sempre più frutta mano a mano che crescono... ma fai pure la cosa che ritieni abbia più senso. E, ovviamente, dovresti essere in grado di countaLeArance (che deve restituire il numero di arance sull'albero), e prendiUnArancia (che riduce la variabile d'istanza @orangeCount di un'unità e restituisce una stringa che dica quanto fosse buona l'arancia appena colta, oppure che quest'anno le arance sono finite). Assicurati che ogni arancia che non prendi un anno cada prima dell'anno seguente.
  • Scrivi un programma che ti consenta di interagire col tuo cucciolo di drago. Dovresti essere in grado di inserire comandi come daiDaMangiare e portaAPasseggio, e far sì che questi metodi siano chiamati sul tuo Dragone. Ovviamente, siccome quello che stai inserendo sono solo stringhe, avrai bisogno di una parte di programma che effettui una selezione del metodo da chiamare effettivamente sul dragone, sulla base della stringa che hai inserito.

E questo è quasi tutto quello che c'è da fare! Ma, aspetta un secondo... Non ti ho ancora detto niente su quelle classi per fare cose tipo mandare email, caricare e salvare file del computer, o come creare finestre e pulsanti, o mondi 3D o.. qualsiasi cosa! Beh, esistono talmente tante classi che potresti utilizzare che proprio non riuscirei a mostrartele tutte; La maggior parte non le conosco nemmeno io! Quello che posso dirti è dove saperne di più, così puoi studiare quelle con cui vuoi programmare. Prima lasciarti andare, però, c'è un'ultima funzionalità di Ruby di cui dovresti proprio fare la conoscenza, un qualcosa che la maggior parte dei linguaggi di programmazione non ha, ma cui non saprei proprio fare a meno: i blocks e i procs.

Le soluzioni di tutti gli esercizi proposti sono disponibili anche nel Manuale delle Soluzioni in Italiano.


Commenti

La sezione commenti è messa a disposizione per consentirti di scambiare idee e consigli con gli altri studenti, fanne buon uso!
Nota bene: se vuoi condividere del codice puoi utilizzare i tag <pre> e <code>, così:

<pre><code>
saluto = 'ciao'
puts saluto
</code></pre>