Exercice - Explorer les coupes

Effectué

Dans la section précédente, nous avons exploré les tableaux et nous avons appris qu’ils constituaient la base des coupes et des mappages. Vous comprendrez pourquoi dans un moment. Comme le tableau, la coupe est un type de données du langage Go qui permet de représenter une séquence d’éléments du même type. Toutefois, la plus grande différence qui existe entre les tableaux et les coupes est que la taille de celles-ci est dynamique, et non fixe.

Une coupe est une structure de données qui vient s’ajouter à un tableau ou à une autre coupe. Le tableau ou la coupe d’origine est désigné sous le terme de tableau sous-jacent. Avec une coupe, vous pouvez accéder à l’ensemble du tableau sous-jacent ou uniquement à une sous-séquence de ses éléments.

Une coupe ne comporte que trois composants :

  • Pointeur vers le premier élément accessible du tableau sous-jacent. Cet élément n’est pas nécessairement le premier élément du tableau, array[0].
  • Longueur de la coupe. Nombre d’éléments dans la coupe.
  • Capacité de la coupe. Nombre d’éléments qui se trouvent entre le début de la coupe et la fin du tableau sous-jacent.

L’image suivante représente une coupe :

Diagram showing how slices look in Go.

Notez que la coupe n’est qu’un sous-ensemble du tableau sous-jacent. Voyons comment vous pouvez représenter l’image précédente dans le code.

Déclarer et initialiser une coupe

Pour déclarer une coupe, procédez de la même façon que pour un tableau. Par exemple, le code suivant représente ce que vous avez vu dans l’image de la coupe :

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))
}

Quand vous exécutez le code, vous voyez la sortie suivante :

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

Notez que, pour le moment, une coupe n’est pas si différente d’un tableau. Vous les déclarez de la même façon. Pour obtenir les informations relatives à une coupe, vous pouvez utiliser les fonctions intégrées len() et cap(). Nous continuerons à utiliser ces fonctions pour montrer qu’une coupe peut comprendre une sous-séquence des éléments d’un tableau sous-jacent.

Éléments d’une coupe

Go prend en charge l’opérateur de coupe s[i:p], où :

  • s représente le tableau.
  • i représente le pointeur vers le premier élément du tableau sous-jacent (ou d’une autre coupe) à ajouter à la nouvelle coupe. La variable i correspond à l’élément situé à l’emplacement d’index i dans le tableau, array[i]. Nous vous rappelons que cet élément n’est pas nécessairement le premier élément du tableau sous-jacent, array[0].
  • p représente le nombre d’éléments dans le tableau sous-jacent à utiliser lors de la création de la nouvelle tranche ainsi que la position des éléments. La variable p correspond au dernier élément dans le tableau sous-jacent qui peut être utilisé dans la nouvelle coupe. L’élément à la position p dans le tableau sous-jacent est trouvé à l’emplacement array[i+1]. Notez que cet élément n’est pas nécessairement le dernier élément du tableau sous-jacent, array[len(array)-1].

Une tranche peut donc référencer seulement un sous-ensemble d’éléments.

Supposons que vous souhaitiez représenter chaque trimestre de l’année par quatre variables, et que vous ayez une coupe months avec 12 éléments. L’illustration suivante montre comment découper months en quatre nouvelles coupes quarter :

Diagram showing how multiple slices look in Go.

Pour représenter sous forme de code ce que vous avez vu dans l’image précédente, vous pouvez utiliser le code suivant :

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))
}

Quand vous exécutez le code, vous obtenez la sortie suivante :

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

Notez que la longueur des coupes est la même, mais que la capacité est différente. Nous allons maintenant explorer la coupe quarter2. Lorsque vous déclarez cette coupe, vous décidez que celle-ci doit commencer à la position numéro 3 et que le dernier élément doit être situé à la position numéro 6. La longueur de la coupe est de trois éléments, mais la capacité est de neuf éléments, car le tableau sous-jacent comprend davantage d’éléments ou de positions disponibles qui ne sont pas visibles par la coupe. Par exemple, si vous essayez d’afficher quelque chose comme fmt.Println(quarter2[3]), vous obtiendrez l’erreur suivante : panic: runtime error: index out of range [3] with length 3.

La capacité d’une coupe vous indique de combien d’éléments vous pouvez l’étendre. Pour cette raison, vous pouvez créer une coupe étendue à partir de quarter2, comme dans l’exemple suivant :

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))
}

Quand vous exécutez le code précédent, vous obtenez la sortie suivante :

[April May June] 3 9
[April May June July] 4 9

Notez que lorsque vous déclarez la variable quarter2Extended, vous n’êtes pas obligé de spécifier la position initiale ([:4]). Dans ce cas, Go suppose que vous voulez obtenir la première position de la coupe. Vous pouvez faire de même pour la dernière position ([1:]). Go suppose que vous souhaitez référencer tous les éléments jusqu’à la dernière position d’une coupe (len()-1).

Ajouter des éléments

Nous avons étudié le fonctionnement des coupes et leurs similitudes avec les tableaux. Examinons maintenant les différences entre les coupes et les tableaux. La première différence est que la taille d’une coupe n’est pas fixe, mais dynamique. Une fois que vous avez créé une coupe, vous pouvez lui ajouter des éléments afin de l’étendre. Nous allons voir bientôt ce qui arrive au tableau sous-jacent.

Pour ajouter un élément à une coupe, Go met à disposition la fonction intégrée append(slice, element). Vous passez la coupe à modifier et l’élément à ajouter sous la forme de valeurs à la fonction. La fonction append retourne ensuite une nouvelle coupe que vous stockez dans une variable. Il peut s’agir de la même variable que celle de la coupe que vous modifiez.

Voyons comment le processus d’ajout se présente dans le code :

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)
    }
}

Quand vous exécutez le code précédent, vous obtenez la sortie suivante :

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]

Cette sortie est intéressante. En particulier ce que retourne l’appel à la fonction cap(). Tout semble normal jusqu’à la troisième itération, où la capacité passe à 4, et où il n’y a que trois éléments dans la coupe. Dans la cinquième itération, la capacité passe à 8, et dans la neuvième, elle passe à 16.

Remarquez-vous un modèle récurrent dans la sortie de capacité ? Quand une coupe ne dispose pas d’une capacité suffisante pour contenir plus d’éléments, Go double sa capacité. Il crée un autre tableau sous-jacent avec la nouvelle capacité. Vous n’avez rien à faire pour que cette augmentation de capacité se produise. Go le fait automatiquement. Soyez vigilant, car il peut arriver qu’une coupe ait plus de capacité que nécessaire, gaspillant ainsi de la mémoire.

Supprimer des éléments

Vous vous demandez peut-être comment supprimer des éléments ? Go ne comprend pas de fonction intégrée permettant de supprimer des éléments d’une coupe. Vous pouvez utiliser l’opérateur de coupe s[i:p] dont nous avons parlé précédemment avant de créer une coupe comprenant uniquement les éléments dont vous avez besoin.

Par exemple, le code suivant permet de supprimer un élément d’une coupe :

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)
	}

}

Quand vous exécutez le code précédent, vous obtenez la sortie suivante :

Before [A B C D E] Remove  C
After [A B D E]

Ce code supprime un élément d’une coupe. Il remplace l’élément supprimé par l’élément suivant dans la coupe, ou n’effectue aucun remplacement si l’élément supprimé était le dernier.

Une autre approche consiste à créer une autre copie de la coupe. Nous allons apprendre à créer des copies de coupes dans la prochaine section.

Créer des copies de coupes

Go comprend la fonction intégrée copy(dst, src []Type) qui permet de créer des copies d’une coupe. Vous envoyez la coupe de destination et la coupe source. Par exemple, vous pouvez créer une copie d’une coupe comme dans l’exemple suivant :

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

Pourquoi créer des copies ? Lorsque vous modifiez un élément d’une coupe, vous modifiez également le tableau sous-jacent. Toutes les autres coupes qui référencent ce même tableau sous-jacent en seront affectées. Voyons d’abord à quoi ce processus ressemble dans le code. Ensuite, nous corrigerons ce problème en créant une copie d’une coupe.

Utilisez le code suivant pour vérifier qu’une coupe pointe vers un tableau, et que chaque modification que vous apporterez à cette coupe affectera le tableau sous-jacent.

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)
}

Quand vous exécutez le code précédent, vous obtenez la sortie suivante :

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

Remarquez comment la modification que nous avons apportée à slice1 a affecté le tableau letters et slice2. Vous pouvez voir dans la sortie que la lettre B a été remplacée par la lettre Z, et que cela affecte tous les éléments qui pointent vers le tableau letters.

Pour résoudre ce problème, vous devez créer une copie de la coupe, ce qui crée implicitement un nouveau tableau sous-jacent. Vous pouvez utiliser le code suivant :

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)
}

Quand vous exécutez le code précédent, vous obtenez la sortie suivante :

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

Notez que la modification de slice1 a affecté le tableau sous-jacent, mais n’a pas affecté la nouvelle slice2.