Home Rhyylen
Contatto
 
 
 
Crystal Language
Capitolo 10
Gli array

Incontriamo la prima struttura dati composta da altri elementi. In Crystal gli array sono sequenze di elementi (non necessariamente dello stesso tipo) indicizzati tramite numeri interi crescenti con indice iniziale 0. Se n sono gli elementi dell'array n-1 sarà l'ultimo indice. Si tratta di una stuttura ben nota nel mondo della programmazione e in Crystal è dinamica, molto flessibile e molto ben supportata. E' importante sottolineare il punto che gli elementi possono non essere tutti dello stesso tipo, come invece è obbligatorio in altri linguaggi. Questo capitolo si presta bene per i vostri esperimenti,
Inizializzare un array può essere fatto tipicamente in vari modi:

tramite new
Array(Int32).new - che origina un array vuoto (è necessario specificare il tipo degli elementi e se ne può specificare uno solo). Evidenziamo che new è una importante keyword usata sostanzialmente per creare istanze di tipo e la incontreremo molto frequentemente durante il nostro percorso.

inserendo direttamente gli elementi
[1,2,3]  - abbiamo un array di interi
[1.1, 'a', "ciao"] - abbiamo un array che contiene un float, un carattere ed una stringa

inizializzando più elementi
Array.new(4, 0) - crea un array inizializzato con 4 "zeri". O anche
Array(Int32).new(5, 1)

ancora usando la keyword Array
Array{1,2,3,4}

In pratica:

ar1 = [1,2,3]
ar2 = [1.2, 0, 'a', "cc"]
ar3 = Array(Int32).new
ar4 = Array.new(4, 0)
ar5 = [] of Int32 ar6 = Array{1,2,3,4}

la penultima scrittura è equivalente alla terza mentre, per quanto detto, scrivere ar5 = [] e basta origina un errore perchè il compilatore non sa come inferire. Questa definizione si può anche modificare includendo più tipi:

ar5 = [] of Int32 | String

Esiste anche un metodo specifico ovvero to_a che serve per riversare le sequenze in un array creandolo quindi a partire dalle sequenze utilizzata. Lo vedrete in azione nel capitolo dedicato ai range. Si tratta di un sistema comodo per gestire ad esempio lunghe sequenze di numeri.
Da ultimo, utilizzando concetti un po' più avanzati che reincontreremo possiamo a nche fare cose del genere:

array_con_blocco = Array(Int32).new(5) { |i| i * 2 }
Crea un array di 5 elementi, con valori 0, 2, 4, 6, 8

Dal punto di vista, diciamo, della presentazione all'utente, come si evince dagli esempi che vedremo, un array è composto da una coppia di parentesi quadre all'interno delle quali troviamo da 0 a n elementi, con n limitato solo dalla memoria disponibile. Se gli elementi di un array sono tutti dello stesso tipo allora anche l'array è di quel tipo altrimenti sarà una unione di tutti quelli presenti al suo interno.


array -> [elemento-0, elemento-1, elemento-n]

la virgola, avrete notato, è il carattere che funge da separatore tra i singoli elementi.

Parlando invece di come possiamo immaginare un array da un punto di vista "fisico", esso può essere rappresentato come segue, prendiamo l'esempio di ar1 nell'elenco precedente:

ar1 - elementi  1  2  3
ar1 - indici  0  1  2
ar1-indici negativi -3 -2 -1

Avrete ovviamente notato la presenza di indici negativi. Crystal permette l'uso anche di questi, come in Ruby, l'indice avente valore assoluto più elevato corrisponde all'elemento 0 mentre l'indice avente valore assoluto più basso coincide con quello positivo n-1, con n come al solito, pari al numero di elementi dell'array.  Gli indici negativi si possono usare come quelli positivi, noi useremo per comodità solo questi ultimi nel corso dei nostri esempi. Detto tra noi, non mi piace molto l'uso di indici negativi che peraltro non ho trovato molto diffuso nella pratica e pochi linguaggi supportano questo tipo di indicizzazione.

Gli array di stringhe e di simboli, cose delle quali ci occuperemo in seguito, possono essere inizializzati in maniera peculiare, ovvero utilizzando
%w per le stringhe e %i per i simboli:

ar1 = %w(uno due tre)
ar2 = %i(uno due tre)
puts typeof(ar1)
puts typeof(ar2)

output:

Array(String)
Array(Symbol)

Come è facile intuire, ci sono parecchie operazioni predefinite per manipolare questa importantissima struttura dati, vediamo quelle, a mio avviso, più importanti e che vi capiteranno più di frequente.

Innanzitutto stampare un array, almeno a livello "complessivo" il nostro puts o print sono più che sufficienti:

ar1 = [1,2,3]
puts ar1

trovare la lunghezza, ovvero il numero di element, di un array - size

ar1 = [1,2,3,4]
puts ar1.size

estrarre un elemento da un array è possibile tramite l'indicazione del suo indice, questo può essere indicato attraverso l'operatore []

ar1 = [1,2,3,4]
puts ar1[3]

cercare di manipolare un valore oltre gli indici definiti per l'array che si sta usando dà origine ad un errore.
NB Alcuni dei metodi o degli operatori che vedremo in questo paragrafo come in quelli che riguardano altre stritture, come le stringhe o gli hash, hanno una contro parte che termina col ?. Ad esempio l'operatore
[] se usato con un indice non valido dà origine ad un errore ma se utilizziamo al suo posto []? allora il risultato può essere o una corretta estrazione dall'array oppure nil se qualcosa va storto, evento questo che è comunque gestibile. Ovvero

  Esempio 10.1
1
2
3
4
5
ar1 = [1,2,3]
x = ar1[6]?
if x == nil
  puts "out of range"
end

Questo programma stampa la stringa "out of range" a video ma se voi toglieste il punto interrogativo alla riga 2 ne uscirebbe un crash. Questo vale anche per altri casi e quindi vi conviene guardare la documentazione ufficiale per vedere se esista l'aternativa col ? ai metodi che state usando. In alcuni casi ve la segnalerò direttamente.

sostituire un elemento di un array è possibile sempre tramite l'operatore [] stando attenti a cosa si mette al posto dell'elemento che si va a rimpiazzare:

ar1 = [1,2,3,4]
puts typeof(ar1)
ar1[2] = 8

Questo codice va bene ma se al posto del numero 8 provaste a mettere ad esempio il carattere 'a' ne avreste un crash. Questo perchè l'array, come si potrebbe notare richiamando typeof(ar1) è composto da soli interi e un altro tipo non ci va. Analogamente, se invece definiamo un array così:

ar1 = [1, 'a', "ciao"]

il suo tipo è: Array(Char | Int32 | String) e quindi potete insere qualsiasi elemento in qualsiasi posizione purchè appartenente ad uno dei tre tipi indicati, mentre ad esempio non potete immettere un numero con virgola. Quindi ad esempio

ar1[0] = "aa" cioè una stringa invece dell'intero all'indice 0 va bene
mentre
ar1[0] = 1.1 dà origine ad errore.

Attenzione quindi perchè errori in questi casi capitano. La libertà (di operare) è bella ma può avere un costo. Se indicate un indice fuori range, diversamente da Ruby, viene fuori un errore.
E' possibile sostituire più elementi con uno specifico utlizzando due formati di sostituzione:

[indice di partenza .. indice finale]
[indice di partenza, numero di elementi]


Vediamo all'opera il primo metodo

ar1 = [0,1,2,3,4,5]
ar1[1..3] = 0
puts ar1

che ci fornisce come output:

[0, 0, 4, 5]

in quanto gli elementi aventi indice 1, 2 e 3 sono stati sostituiti da un unico valore, lo 0 indicato nella seconda riga.
Ora vediamo il secondo sistema

ar1 = [1,2,3,4,5,6,7,8,9]
ar1[2,4] = 0
puts ar1

e qui abbiamo:

[1, 2, 0, 7, 8, 9]

E' possibile anche sostituire un blocco di un array con il contenuto di un altro:

ar1 = [1,2,3,4,5]
ar1[0..3] =[99,88,77,66,55]
puts ar1

ed ecco qua:

[99, 88, 77, 66, 55, 5]

Se invece volete aggiungere un elemento in coda all'array potete usare l'operatore <<

ar1 = [1,2,3,4,5]
ar1 << 6
puts ar1

e avrete:

[1, 2, 3, 4, 5, 6]

Con questo operatore potete quindi riempire un array vuoto:

ar1 = Array(Int32).new
ar1 << 0
ar1 << 2
ar1 << 7
puts ar1

da cui risulta:

[0, 2, 7]

La sequenza precedente può anche essere riscritta:

ar1 << 0 << 2 << 7

per eliminare un elemento dalla coda di un array invece funziona bene pop:

ar1.pop

Se volete invece creare una catena di array potete usare concat, a condizione però che vi sia coerenza nei tipi tra i dati degli array che andrete a concatenare

ar1 = [1,2]
ar2 = [3,4]
ar3 = [5,6]
ar4 = ar1.concat(ar2).concat(ar3)
puts ar4

il cui output è:

[1, 2, 3, 4, 5, 6]

oppure:

ar1 = [1, 'a']
ar2 = ['b', 2]
ar1.concat(ar2)
puts ar1

da cui

[1, 'a', 'b', 2]

Vedremo più avanti un ulteriore metodo

Inserire un elemento in una data posizione è compito di insert(indice, elemento) stando ovviamente attenti a non andare fuori range con l'indice ed alla compatibilità dell'elemento con i tipi ammessi per l'array

ar1 = [1,2,3,4,5]
ar1.insert(3, 99)
puts ar1

Ovviamente potete inserire un elemento all'inizio dell'array
insert(0, elemento)
o alla fine
insert (arraysize-1, elemento)

Insert è generico se volete un metodo per inserire qualcosa esattamente all'inizio dell'array potete usare unshift(elemento) mentre shift lo elimina

ar1 = [1,2,3]
ar1.unshift(0)
puts ar1
ar1.shift
puts ar1

A proposito di cancellazione, potreste volere un metodo rapido per cancellare uno o più elementi da un array. Ci sono vari modi, abbiamo già visto pop che elimina un elemento dalla coda e shift che lo elimina dalla prima posizione. Ora vediamo altri sistemi:
  • delete_at(indice)  elimina l'elemento esattamente alla posizione dell'indice selezionato. Un indice fuori range genera un errore (e non un nil come in Ruby)
  • delete(elemento) elimina tutte le occorrenze dell'elemento. Indicare un elemento che non esiste non fa semplicememente nulla.
  • clear elimina tutti gli elmenti dell'array
Il tutto è riassunto nel seguente codice:

ar1 = [1,2,3,4]
ar1.delete_at(2)
puts ar1
ar2 = [1,2,2,2,3]
ar2.delete(8)
puts ar2
ar1.clear
puts ar1.size

delete_at ha anche due formati alternativi:
delete_at(indice, n) permette di eliminare n elementi a partire dalla  posizione indice.
delete_at(n..m) cancella gli elementi dall'indice n all'indice m compresi.

Se invece volete cercare gli elementi all'interno di un array potete usare index(elemento) che restituisce la prima occorrenza dell'elemento cercato. Attenzione che restituisce, come detto, la prima, quindi potrebbero esservene altre con indice più elevato. Se volete un conteggio di tutte le occorrenze di un certo elemento invece dovete usare count(elemento)

ar1 = [1,2,3,4,4,4,4,4]
puts ar1.count(4)
puts ar1.index(4)

ci dà come output

5
3


ovvero il numero di "4" contenuti nell'array l'indice del primo di essi che si incontra partendo dall'indice 0.

Possono venire comodi altri due metodi:
- first(n) che ci restituisce i primi n elementi dell'array
- last(n) che ci restituisce gli ultimi n elementi delle'array.
un esempio veloce:

ar1 = [1,2,3,4,5,6,7,8]
ar2 = ar1.first(3)
ar3 = ar1.last(3)
puts ar2
puts ar3

da cui risulta:

[1, 2, 3]
[6, 7, 8]
 
Interessante è l'uso di + e - come operatori tra array.

- il + concatena gli array
- il - sottrae gli elementi comuni

ar1 = [1,2,3]
ar2 = [1,2]
ar3 = ar1 + ar2
puts ar3
ar4 = ar1 - ar2
puts ar4

che in base a quanto detto ci restituisce il seguente output:

[1, 2, 3, 1, 2]
[3]

Si possono anche costruire operazioni multiple:

ar1 = [1,2,3,4]
ar2 = [1,2]
ar3 = [4]
ar4 = ar1 + ar2 + ar3
puts ar4
ar5 = ar1 - ar2 - ar3
puts ar5
ar6 = ar1 + ar2 - ar3
puts ar6

Un problema che spesso si presenta è quello di effettuare una copia degli array. Certo, come vedremo è possibile iterare elemento per elemento costruendo passo dopo passo un altro array, ma con grosse quantità di dati non è esattamente il sistema più comodo. Un primo metodo prevede l'utilizzo di =

ar1 = [1,2,3]
ar2 = ar1

questo sistema in realtà crea non una copia bensì un doppio puntamento all'area di memoria che contiene i dati di ar1 quindi una modifica su uno dei due modifica i valori anche nell'altro. L'esempio seguente e il suo output mostrano questo problema:

  Esempio 10.2
1
2
3
4
5
6
7
8
ar1 = [1,2,3]
ar2 = ar1
ar1[0] = 9
puts ar1
puts ar2
ar2[1] = 33
puts ar1
puts ar2

output:

[9, 2, 3]
[9, 2, 3]
[9, 33, 3]
[9, 33, 3]

alla riga 9 modifichiamo un elemento di ar1 e alla 6 un elemento di ar2. In entrambi i casi questo ha effetto anche sull'altro array.
Insomma non creiamo una vera e propria copia, sicuramente non un backup dell'originale. Altri due metodi interessanti invece sono dup e clone. Possono sembrare intercambiabili ma non lo sono.

ar1 = [1,2,3,4]           ar1 = [1,2,3,4]
ar2 = ar1.dup             ar2 = ar1.clone
ar1[1] = 88               ar1[1] = 88
puts ar1                  puts ar1
puts ar2                  puts ar2

come vi sarà facile verificare in questo caso le modifiche effettuate su ar1 non intaccano ar2 (e viceversa, potete provare) ed il risultato è lo stesso per entrambi i programmi. Il punto però è che dup non copia gli oggetti che sono all'interno dell'array al contrario di clone. Prendiamo un array di array:

ar1 = [[1,2],[3,4]] ed applichiamo su di esso entrambi i metodi clone e dup, come nel seguente programma:

ar1 = [[1,2],[3,4]]
ar2 = ar1.clone
ar3 = ar1.dup
ar1[0][0] = 9
puts ar1
puts ar2
puts ar3

ora, l'output è il seguente:

[[9, 2], [3, 4]]
[[1, 2], [3, 4]]
[[9, 2], [3, 4]]

da qui si deduce che l'unica vera deep copy, una copia del tutto indipendente dall'originale è ottenuta tramite clone. In pratica dup effettua una duplicazione degli oggetti all'interno (da cui il corretto funzionamento dell'esempio con gli interi) ma non egli oggetti referenziati, come gli array. Non sono sicuro ma ritengo che dup sia più efficiente e preferibile per casi più semplici, che non coinvolgano oggetti referenziati, mentre clone sia la soluzione "definitiva" al problema della copia di array (salvo, appunto, effettuare una copia elemento per elemento). Questa parte deve essere sottoposta a ulteriori approfondimenti e verifiche.
Comunque, se volete fare "da soli" e crearvi una deep copy assolutamente vostra di cuk avete pieno controllo, vi propongo il seguente codice, su cui potrete tornare dopo aver appreso altri concetti:

def deep_copy(arr)
  arr.map do |elem|
  if elem.is_a?(Array)
    deep_copy(elem) # Ricorsione per gli array nidificati
  else
    elem
    end
  end
end

arr1 = [[1, 2], [3, 4]]
arr2 = deep_copy(arr1)

arr1[0][0] = 99
puts arr1 # Output: [[1, 2], [3, 4]] (arr1 rimane immutato)
puts arr2 # Output: [[99, 2], [3, 4]]

Vediamo a questo punto alcuni metodi utili nella pratica


max estrae l'elemento massimo dalla sequenza di quelli compresi nell'array

ar1 = [99, 56,13, 44]
puts ar1.max
ar2 = ["bb", "cc", "aa"]
puts ar2.max

sort effettua un ordinamento crescente degli elementi interni all'array
reverse inverte gli elementi. Vediamoli all'opera insieme:

ar1 = [4, 6, 7, 19, 2, 0, 37, 3]
ar1 = ar1.sort
ar1 = ar1.reverse
puts ar1

che espone a video:

[37, 19, 7, 6, 4, 3, 2, 0]

In un prossimo paragrafo parleremo delle stringhe che sono strutturalmente piuttosto vicine agli array. Bene è possibile passare da array a stringa attaverso il metodo join che permette di inserire anche un eventuale separatore:

ar1 = ['a', 'b', 'c']
s01 = ar1.join()
s02 = ar1.join(':')
puts s01
puts s02

che ci dà:

abc
a:b:c

C'è ancora un importante argomento da affrontare relativamente agli array e si tratta dell'iterazione sui suoi elementi, cosa che riguarda altre strutture dati in questo linguaggio.

Un modo semplice è quello di utilizzare uno dei cicli già visti


  Esempio 10.3
1
2
3
4
5
6
ar1 = [1,2,3,4,5,6]
x = 0
while x < ar1.size
print ar1[x]," "
x = x + 1
end

quindi utilizziamo gli indici per eseguire una semplice operazione di stampa dei singoli elementi. Analogamente si possono utilizzare altri cicli stando attenti ovviamente ad un preciso calcolo degli indici, lo so che è banale dirlo.
Per il resto esiste anche la possibilità di usare un iteratore. Ne parleremo nel prossimo paragrafo.