Ripasso
Settimana scorsa abbiamo parlato di gestione di errori e di array. Mi pare che questo secondo argomento vi abbia un po' confuso, quindi prima di concluderlo occorre riprendere alcuni concetti importanti che forse non sono risultati chiari.
Struttura di un array
Gli array sono uno dei tipi che Go mette a disposizione per la gestione di una collezione di dati, ovvero di un numero di variabili che hanno qualcosa in comune. Solitamente i dati che vanno conservati in un array corrispondono ad una lista di dati simili (ovvero dello stesso tipo).
Possiamo pensare ai dati di un array come ad una lista di variabili dello stesso tipo conservate in una zona contigua di memoria. La sintassi per dichiarare un array è la seguente:
var nome [nelem]tipo
dove nome
sarà l'identificatore con cui riferirsi alla variabile
di tipo array, nelem
è la lunghezza dell'array (il numero di
elementi) e tipo
è il tipo di ciascuno degli elementi (può essere
uno di quelli che abbiamo già visto: float64
, int
, bool
).
In particolare nelem
deve essere un numero costante, non una
variabile: può essere un letterale, per esempio 10
, oppure un
valore dichiarato con la parola chiave const
.
Indicizzazione
Quello che mi pare vi abbia confuso maggiormente è l'accesso agli elementi di un array, e di conseguenza la modifica degli elementi.
Per riferirsi a ciascun elemento dell'array si utilizza un indice, ovvero un numero che corrisponde alla posizione dell'elemento nell'array. Ciascun array è indicizzato a partire da 0, ovvero il primo elemento è lo 0-esimo, il secondo elemento è il primo e così discorrendo.
Supponiamo di avere un array di 10 interi dichiarato in questo modo:
var list [10]int
la variabile che contiene il primo elemento dell'array è identificata da:
list[0]
Questo identificatore permette di utilizzare il primo elemento dell'array in un calcolo, oppure di modificarlo: permette di utilizzarlo come una delle variabili che abbiamo usato fino ad ora.
Accesso a tutti gli elementi
Siccome ad un array corrisponde una lista di elementi, per accedere a tutti i valori occorre un ciclo for. Go mette a disposizione due tipi di cicli for per lavorare su array: il primo è un semplice ciclo che conti da 0 a nelem - 1 ed usi l'indicizzazione che abbiamo visto prima
// Stampo tutti gli elementi di list
for i := 0; i < nelem; i++ {
fmt.Println(list[i])
}
il secondo è un ciclo for-range che permette di listare gli
elementi di un array senza dover specificare la lunghezza (Go la
conosce a priori perchè l'abbiamo specificata nella dichiarazione di
list
).
// Stampo tutti gli elementi di list
for i := range list {
// la variabile i contiene un indice che va da 0 a nelem - 1
// e cambia ad ogni iterazione
fmt.Println(list[i])
}
Con le operazioni di indicizzazione ed i cicli for si possono svolgere la maggior parte delle operazioni sugli array.
Slice
Gli array sono molto comodi, ma spesso occorre trattare i dati indipendentemente dal loro numero, in particolare se si ottiene l'input in maniera dinamica (chiedendolo all'utente, oppure leggendo un file) non è sempre nota a priori la quantità di dati che bisogna trattare. Per risolvere i problemi dovuti alla staticità della dimensione degli array, Go mette a disposizione un altra collezione di elementi chiamata slice. Una slice è in pratica un array di dimensioni variabili, Go si preoccupa di aumentarne la grandezza quando necessario, ed è sempre possibile aggiungere elementi alla lista.
La sintassi per dichiarare una slice è la seguente:
var nome []tipo
In seguito a questa dichiarazione avremo a disposizione una slice di
elementi di tipo tipo
, identificata da nome
; ciascuno degli
elementi della slice corrisponderà allo zero del tipo selezionato.
Operazioni sulle slice
La natura dinamica delle slice richiede alcune operazioni particolari per inizializzare ed aggiungere elementi.
Inizializzare
Dopo una dichiarazione di questo tipo
var list []float64
abbiamo a disposizione una slice vuota di float64
, se sappiamo che
la slice avrà grandezza minima 10 possiamo utilizzare la funzione
make
per inizializzarla:
list = make([]float64, 10)
Una volta inizializzata possiamo accedere a tutti gli elementi tra
list[0]
e list[9]
, ma non oltre.
Aggiungere elementi
Per aggiungere elementi in coda alla slice si usa la funzione
append
:
// func append(slice []Type, elems ...Type) []Type
list = append(list, 43.22)
append restituisce una nuova slice, fatta dalla slice di partenza e dagli altri elementi aggiunti in coda.
In particolare se non si conosce la grandezza della slice si può
evitare di inizializzarla con make
e si possono semplicemente
aggiungere elementi in coda uno alla volta, partendo dalla slice
vuota:
var list []float64{} // slice vuota
for {
// ...
list = append(list, elem)
}
Questo approccio permette di gestire in maniera semplice un numero variabile di input.
Cicli
Per accedere agli elementi di una slice solitamente si possono usare i due approcci che abbiamo visto con gli array, in generale si preferisce il for-range perchè è più elegante.
for i := 0; i < len(list); i++ {
// len(list) restituisce la lunghezza di list
fmt.Println(list[i])
}
for i := range list {
fmt.Println(list[i])
}
Le stringhe
Per quelli di voi che si ricordano ciò che avevamo detto riguardo alle stringhe, questo può sembrare molto simile: in effetti lo è poiché le stringhe sono soltanto slice di rune.
Esercizi
Esercizio 0 - Database
Problema: Riprendete l'esercizio della volta scorsa e modificatelo in modo che sia possibile specificare un numero arbitrario di voti per ciascuna materia.
Soluzione
package main
import (
"fmt"
"io"
"os"
)
var matematica, fisica, informatica []float64
func main() {
err := leggiVoti("database.txt")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("Medie:")
fmt.Println("Matematica: ", media(matematica))
fmt.Println("Fisica: ", media(fisica))
fmt.Println("Informatica: ", media(informatica))
return
}
func leggiVoti(filename string) error {
var materia string
matematica = make([]float64, 0)
fisica = make([]float64, 0)
informatica = make([]float64, 0)
f, err := os.Open(filename)
if err != nil {
return err
}
for err != io.EOF {
var voto float64
if _, err = fmt.Fscanf(f, "%s %f\n", &materia, &voto); err != nil && err != io.EOF {
return err
}
switch materia {
case "matematica":
matematica = append(matematica, voto)
case "fisica":
fisica = append(fisica, voto)
case "informatica":
informatica = append(informatica, voto)
}
}
f.Close()
return nil
}
// Restituisce la media dei voti
func media(voti []float64) float64 {
var sum float64
for i := range voti {
if voti[i] < 3 {
fmt.Fprintf(os.Stderr, "Voto non valido %f, utilizzo il valore valido più vicino: %d\n", voti[i], 3)
sum += 3
} else if voti[i] > 10 {
fmt.Fprintf(os.Stderr, "Voto non valido %f, utilizzo il valore valido più vicino: %d\n", voti[i], 10)
sum += 10
} else {
sum += voti[i]
}
}
return sum / float64(len(voti))
}
Esercizio 1 - Fibonacci
Problema: Riprendete l'esercizio della volta scorsa e modificatelo
in modo che sia possibile specificare un intero n
da terminale, ed
il programma generi una slice contenente i primi n
numeri di
fibonacci.
Soluzione
package main
import (
"fmt"
"os"
)
func main() {
var max int
fmt.Print("Massimo numero della sequenza: ")
for _, err := fmt.Scan(&max); err != nil; {
fmt.Fprintln(os.Stderr, err)
_, err = fmt.Scan(&max)
}
fmt.Println(fib(max))
}
func fib(n int) []int {
var fibonacci []int
fibonacci = make([]int, n)
if n > 1 {
fibonacci[0] = 0
fibonacci[1] = 1
for i := 1; i < len(fibonacci); i++ {
fibonacci[i + 1] = fibonacci[i] + fibonacci[i - 1]
}
}
return fibonacci
}