|
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:
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:
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
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:
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
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:
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:
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
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. |