Esercizio - Esplorare le slice
Nella sezione precedente sono state esaminate le matrici e si è visto che le matrici rappresentano la base per le slice e le mappe. I motivi saranno chiari tra poco. Analogamente alle matrici, una slice è un tipo di dati usato in Go per rappresentare una sequenza di elementi dello stesso tipo. Tuttavia, la differenza più significativa rispetto alle matrici è che le dimensioni di una slice sono dinamiche e non fisse.
Una slice è una struttura di dati sovrapposta a una matrice o a un'altra slice. La matrice o la slice di origine viene detta matrice sottostante. Con una slice è possibile accedere all'intera matrice sottostante o solo a una sottosequenza di elementi.
Una slice include solo tre componenti:
- Puntatore al primo elemento raggiungibile della matrice sottostante. Questo elemento non è necessariamente il primo elemento della matrice,
array[0]
. - Lunghezza della slice. Numero di elementi nella slice.
- Capacità della slice. Numero di elementi tra l'inizio della slice e la fine della matrice sottostante.
L'immagine seguente rappresenta una slice:
Si noti che la slice è solo un subset della matrice sottostante. Si vedrà ora come è possibile rappresentare l'immagine precedente nel codice.
Dichiarare e inizializzare una slice
Per dichiarare una slice, è necessario procedere come per dichiarare una matrice. Ad esempio, il codice seguente corrisponde all'immagine della slice:
package main
import "fmt"
func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
fmt.Println(months)
fmt.Println("Length:", len(months))
fmt.Println("Capacity:", cap(months))
}
Con l'esecuzione del codice precedente si ottiene questo output:
[January February March April May June July August September October November December]
Length: 12
Capacity: 12
Si noti che, al momento, una slice non è tanto diversa da una matrice. Vengono dichiarate nello stesso modo. Per ottenere informazioni da una slice, è possibile usare le funzioni predefinite len()
e cap()
. Si continuerà a usare queste funzioni per confermare che una slice può avere una sottosequenza di elementi da una matrice sottostante.
Elementi della slice
Go supporta l'operatore per slice s[i:p]
, dove:
s
rappresenta la matrice.i
rappresenta il puntatore al primo elemento della matrice sottostante (o di un'altra slice) da aggiungere alla nuova slice. La variabilei
corrisponde all'elemento in corrispondenza della posizione dell'indicei
nella matricearray[i]
. Tenere presente che questo elemento non è necessariamente il primo elemento della matrice sottostante,array[0]
.p
rappresenta il numero di elementi nella matrice sottostante da usare durante la creazione della nuova sezione, nonché la posizione degli elementi. La variabilep
corrisponde all'ultimo elemento nella matrice sottostante che può essere usato nella nuova slice. L'elemento in corrispondenza della posizionep
nella matrice sottostante si trova nella posizionearray[i+1]
. Si noti che questo elemento non è necessariamente l'ultimo elemento della matrice sottostante,array[len(array)-1]
.
Pertanto, una sezione può fare riferimento solo a un subset di elementi.
Si immagini, ad esempio, di voler usare quattro variabili per rappresentare i trimestri dell'anno e di avere una slice di months
con 12 elementi. L'immagine seguente illustra come suddividere months
in quattro nuove slice quarter
:
Per rappresentare nel codice l'immagine precedente, è possibile usare il codice seguente:
package main
import "fmt"
func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter1 := months[0:3]
quarter2 := months[3:6]
quarter3 := months[6:9]
quarter4 := months[9:12]
fmt.Println(quarter1, len(quarter1), cap(quarter1))
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter3, len(quarter3), cap(quarter3))
fmt.Println(quarter4, len(quarter4), cap(quarter4))
}
Con l'esecuzione del codice precedente si ottiene questo output:
[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3
Si noti che la lunghezza delle slice è la stessa, ma la capacità è diversa. Analizziamo la slice quarter2
. Nella dichiarazione di questa slice si specifica che la slice inizia alla posizione numero tre e l'ultimo elemento si trova nella posizione numero sei. La lunghezza della slice è di tre elementi, ma la capacità è nove perché la matrice sottostante ha più elementi o posizioni disponibili ma non visibili per la slice. Ad esempio, se si tenta di stampare qualcosa di simile a fmt.Println(quarter2[3])
, si otterrà l'errore seguente: panic: runtime error: index out of range [3] with length 3
.
La capacità di una slice indica solo l'estensione possibile. Per questo motivo, è possibile creare una slice estesa da quarter2
, come nell'esempio seguente:
package main
import "fmt"
func main() {
months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
quarter2 := months[3:6]
quarter2Extended := quarter2[:4]
fmt.Println(quarter2, len(quarter2), cap(quarter2))
fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}
Con l'esecuzione del codice precedente si ottiene questo output:
[April May June] 3 9
[April May June July] 4 9
Si noti che quando si dichiara la variabile quarter2Extended
, non è necessario specificare la posizione iniziale ([:4]
). Go presuppone che si voglia partire dalla prima posizione della slice. Stesso vale per l'ultima posizione ([1:]
). Go presuppone che si voglia fare riferimento a tutti gli elementi fino all'ultima posizione di una sezione (len()-1
).
Accodare elementi
Sono stati illustrati il funzionamento delle slice e le similitudini con le matrici. Verranno ora descritte le differenze. La prima differenza è che le dimensioni di una slice non sono fisse, ma dinamiche. Dopo aver creato una slice, è possibile aggiungervi altri elementi e la slice verrà estesa. Si vedrà tra breve cosa accade alla matrice sottostante.
Per aggiungere un elemento a una slice, Go offre la funzione predefinita append(slice, element)
. Si passa la slice da modificare e l'elemento da aggiungere come valori alla funzione. La funzione append
restituisce quindi una nuova sezione archiviata in una variabile. Potrebbe essere la stessa variabile della slice che si sta modificando.
Questo è l'aspetto del processo di accodamento nel codice:
package main
import "fmt"
func main() {
var numbers []int
for i := 0; i < 10; i++ {
numbers = append(numbers, i)
fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
}
}
Con l'esecuzione del codice precedente si ottiene questo output:
0 cap=1 [0]
1 cap=2 [0 1]
2 cap=4 [0 1 2]
3 cap=4 [0 1 2 3]
4 cap=8 [0 1 2 3 4]
5 cap=8 [0 1 2 3 4 5]
6 cap=8 [0 1 2 3 4 5 6]
7 cap=8 [0 1 2 3 4 5 6 7]
8 cap=16 [0 1 2 3 4 5 6 7 8]
9 cap=16 [0 1 2 3 4 5 6 7 8 9]
Questo output è interessante. In particolare per il risultato restituito dalla chiamata alla funzione cap()
. Tutto sembra normale fino alla terza iterazione, in cui la capacità passa a 4 e sono presenti solo tre elementi nella slice. Nella quinta iterazione, la capacità cambia di nuovo e diventa 8 e nella nona iterazione diventa 16.
Si nota uno schema nell'output della capacità? Quando una slice non ha capacità sufficiente per contenere altri elementi, Go ne raddoppia la capacità. Crea una nuova matrice sottostante con la nuova capacità. Non è necessario eseguire alcuna operazione per ottenere questo aumento della capacità. Go lo gestisce automaticamente. È necessario prestare attenzione. A un certo punto, una slice potrebbe avere una capacità superiore a quella necessaria con conseguente spreco di memoria.
Rimuovere elementi
Potrebbe anche essere necessario rimuovere elementi. Go non include una funzione predefinita per rimuovere elementi da una slice. È possibile usare l'operatore per slice s[i:p]
presentato in precedenza per creare una nuova slice con solo gli elementi necessari.
Il codice seguente, ad esempio, rimuove un elemento da una slice:
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
remove := 2
if remove < len(letters) {
fmt.Println("Before", letters, "Remove ", letters[remove])
letters = append(letters[:remove], letters[remove+1:]...)
fmt.Println("After", letters)
}
}
Con l'esecuzione del codice precedente si ottiene questo output:
Before [A B C D E] Remove C
After [A B D E]
Questo codice rimuove un elemento da una slice. Sostituisce l'elemento da rimuovere con l'elemento successivo nella slice o con nessuno se si rimuove l'ultimo elemento.
Un altro approccio consiste nel creare una nuova copia della slice. Nella sezione successiva verrà illustrato come creare copie delle slice.
Creare copie di slice
Go include la funzione predefinita copy(dst, src []Type)
per creare copie di una slice. Si inviano la slice di destinazione e la slice di origine. Ad esempio, è possibile creare una copia di una slice come illustrato nell'esempio seguente:
slice2 := make([]string, 3)
copy(slice2, letters[1:4])
Perché può essere utile creare copie? Quando si modifica un elemento di una slice, si cambia anche la matrice sottostante e saranno interessate anche tutte le altre sezioni che fanno riferimento alla stessa matrice sottostante. Si vedrà questo processo nel codice, che verrà poi corretto creando una copia di una slice.
Usare il codice seguente per verificare che una slice fa riferimento a una matrice e che ogni modifica apportata in una slice influisce sulla matrice sottostante.
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)
slice1 := letters[0:2]
slice2 := letters[1:4]
slice1[1] = "Z"
fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
}
Con l'esecuzione del codice precedente si ottiene questo output:
Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]
Si noti che la modifica apportata a slice1
ha avuto effetti sulla matrice letters
e su slice2
. Nell'output è possibile osservare che la lettera B è stata sostituita con la Z e che la modifica interessa tutti coloro che fanno riferimento alla matrice letters
.
Per risolvere il problema, è necessario creare una copia della slice, che crea una nuova matrice sottostante dietro le quinte. È possibile usare il codice seguente:
package main
import "fmt"
func main() {
letters := []string{"A", "B", "C", "D", "E"}
fmt.Println("Before", letters)
slice1 := letters[0:2]
slice2 := make([]string, 3)
copy(slice2, letters[1:4])
slice1[1] = "Z"
fmt.Println("After", letters)
fmt.Println("Slice2", slice2)
}
Con l'esecuzione del codice precedente si ottiene questo output:
Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]
Si noti che la modifica in slice1
ha interessato la matrice sottostante, ma non ha influito sulla nuova slice2
.