CodeGym /Cours /Go SELF /Nous approfondissons le fonctionnement de copy

Nous approfondissons le fonctionnement de copy

Go SELF
Niveau 13, Leçon 1
Disponible

1. Signature de copy : que copie-t-elle, combien et que retourne-t-elle

Pour etre honnete, la question d'un debutant est tout a fait logique : « Pourquoi utiliser copy s'il y a append ? Je peux bien tout assembler avec append. » Et oui, parfois c'est possible. Mais des que vous voulez gerer de facon previsible le contenu d'un slice (surtout a l'interieur d'un meme backing array), append commence a ressembler a cet ami qui vous aide a demenager... en deplacant les cartons au hasard.

copy est interessante parce qu'elle fait une chose simple : elle reecrit des elements. Pas de magie, pas de modification de la longueur, pas de « nouvelle memoire allouee soudainement ». Cela fait de copy un outil ideal quand vous devez « deplacer des meubles dans une piece », et non « construire une nouvelle maison ».

Au passage, copy est l'une des fonctions integrées de base de Go (avec append), apparues comme une simplification importante du travail avec les slices.

Commencons par la formule principale du jour. En Go, il existe une fonction integrée :

copy(dst, src)

Elle copie les elements de src vers dst. Il est important de retenir l'ordre des arguments exactement ainsi : vers ou (dst), puis d'ou (src). Si vous les inversez, le code compilera, mais la logique sera « dans le style d'une release du vendredi soir ».

Combien d'elements sont copies ? Exactement autant que possible sans depasser les limites :

min(len(dst), len(src))

Et copy retourne le nombre d'elements reellement copies. Ce n'est pas un chiffre decoratif : il est souvent utile lorsque vous effectuez des deplacements et que vous calculez une nouvelle « longueur logique » du resultat.

Detail tres important : copy ne modifie pas len ni de dst, ni de src. Elle reecrit simplement des elements. Si, apres la copie, vous devez « raccourcir/agrandir » quelque chose, c'est une etape separee via reslice (par exemple, s = s[:...]) ou append.

Mini-exemple pour comprendre « combien elle copie » :

package main

import "fmt"

func main() {
	dst := make([]int, 2)       // len=2
	src := []int{10, 20, 30}    // len=3

	n := copy(dst, src)
	fmt.Println("n =", n) // n = 2
	fmt.Println(dst)      // [10 20]
}

3. Patron n°1 : une vraie copie d'un slice

Quand vous ecrivez :

b := a

vous ne copiez pas les elements — vous copiez l'en-tete du slice (pointer+len+cap). Autrement dit, a et b continuent de pointer vers le meme backing array. Vous modifiez b[0] — et a[0] change aussi. Parfois c'est pratique, mais souvent c'est la source de « bugs paranormaux ».

Pour obtenir une copie independante des donnees (avec un nouveau backing array), on utilise la combinaison make + copy :

package main

import "fmt"

func main() {
	tasks := []string{"buy milk", "read book", "write code"}

	snapshot := make([]string, len(tasks)) // nouveau backing array
	copy(snapshot, tasks)

	snapshot[0] = "buy coffee"

	fmt.Println(tasks)    // [buy milk read book write code]
	fmt.Println(snapshot) // [buy coffee read book write code]
}

Ici, snapshot a un backing array different, donc les modifications ne « fuient » pas en retour.

Ce patron est tellement fondamental qu'il faut le voir comme « prendre un instantane de l'etat ». Dans le code reel, vous clonerez souvent un slice avant de le trier, de le filtrer ou de le renvoyer a l'exterieur (par exemple, depuis une fonction), afin de ne pas laisser le code appelant endommager vos donnees par erreur.

4. Nuance : len est plus important que cap pour copy

Voici le piege typique. Un debutant pense ainsi : « J'ai fait make([]int, 0, 10), donc j'ai beaucoup de place, maintenant copy va tout copier la-dedans. » Mais copy regarde len, pas cap. Si len(dst) == 0, il n'y a tout simplement rien ou copier.

Exemple mauvais (mais parlant) :

package main

import "fmt"

func main() {
	src := []int{1, 2, 3}

	dst := make([]int, 0, 10) // len=0, cap=10
	n := copy(dst, src)

	fmt.Println("n =", n) // n = 0
	fmt.Println(dst)      // []
}

Pour que la copie se produise, dst doit avoir une longueur. L'option la plus directe est de donner a dst la longueur necessaire :

package main

import "fmt"

func main() {
	src := []int{1, 2, 3}

	dst := make([]int, len(src)) // len=3
	copy(dst, src)

	fmt.Println(dst) // [1 2 3]
}

Et si vous voulez deliberement utiliser une capacite deja allouee (cap), vous pouvez etendre le slice par la longueur via reslice, mais seulement si cap le permet :

package main

import "fmt"

func main() {
	src := []int{1, 2, 3}

	dst := make([]int, 0, 10)
	dst = dst[:len(src)]  // maintenant len=3
	copy(dst, src)

	fmt.Println(dst) // [1 2 3]
}

5. copy a l'interieur d'un meme slice : chevauchements et deplacements

Maintenant, le plus interessant : copy est utile non seulement pour « creer un clone », mais aussi pour « deplacer un morceau ».

Idee cle : en Go, copy fonctionne correctement meme lorsque dst et src se chevauchent. C'est exactement ce qu'il faut pour les deplacements a l'interieur d'un meme backing array : on prend la queue et on la deplace vers la gauche ou vers la droite.

Cette approche apparait souvent dans le code de la bibliotheque standard et dans de vrais exemples de traitement en flux : on copie un morceau de donnees, puis on « deplace » le slice via reslice. Par exemple, dans un article sur le decodeur GIF, on montre une technique caracteristique : on prend un buffer-slice, on le copie dans p, puis on « coupe » la partie deja utilisee avec reslice.

Pour nous, cela signifie que copy est le « moteur », et la modification de len est la « boite de vitesses » ; les confondre est assez facile.

Ou vivent copy et la modification de la longueur dans ces patrons

Pour ne pas se melanger, il est utile de garder en tete un modele mental simple : copy fait toujours une « reecriture », et la longueur ne change qu'a part, dans une etape separee.

flowchart TD
    A[Il existe un slice s] --> B{Que faut-il faire ?}

    B -->|"Faire une copie independante"| C["make un nouveau slice avec la len voulue"]
    C --> D["copy(dst, src)"]
    D --> E[Termine : autre backing array]

    B -->|"Deplacer vers la gauche"| F["copy(s[i:], s[j:])"]
    F --> G[len encore identique]
    G --> H[Plus tard : reslice/logique de suppression]

    B -->|"Deplacer vers la droite"| I[Augmenter d'abord la len : append]
    I --> J["copy(s[i+k:], s[i:])"]
    J --> K[Plus tard : ecrire l'insertion]

Ce schema souligne volontairement qu'apres un deplacement, vous restez souvent dans un « etat intermediaire », et c'est normal. L'important est l'ordre des etapes.

6. Deplacement vers la gauche : rapprocher la queue

Imaginez que nous ayons une liste de taches, et que nous voulions retirer un element du milieu. Nous verrons la suppression complete plus tard, mais apprenons d'abord l'etape cle : fermer le trou en deplacant la queue vers la gauche.

Le deplacement vers la gauche ressemble a ceci :

copy(s[i:], s[i+1:])

Autrement dit, nous copions les elements a partir de i+1 vers la position i.

Mini-exemple :

package main

import "fmt"

func main() {
	s := []int{1, 2, 3, 4}
	copy(s[1:], s[2:])

	fmt.Println(s) // [1 3 4 4]
}

Remarquez le resultat : le « trou » entre 2 et 3 est ferme, mais un doublon apparait a la fin. C'est normal et attendu, parce que len n'a pas change. Nous avons simplement reecrit des elements.

Si vous vous etes dit maintenant « pourquoi Go n'a-t-il pas raccourci le slice tout seul ? » — parce que copy n'a pas du tout le droit de modifier la longueur : elle fonctionne comme une operation de reecriture de bas niveau. Raccourcir est une etape separee, et cela relevera deja d'une logique volontaire du programme.

7. Deplacement vers la droite : ecarter la queue

Le deplacement vers la droite est necessaire lorsque vous voulez inserer un element au milieu. Ici, point important : vous ne pouvez pas deplacer vers la droite s'il n'y a pas de place en longueur. La premiere etape consiste donc a augmenter len. Le moyen le plus simple est append (oui, nous restons amis avec append, nous l'utilisons simplement avec prudence).

Le schema d'insertion « en gros » est le suivant :

  1. augmenter la longueur de 1
  2. deplacer la queue vers la droite avec copy
  3. ecrire la valeur dans la case liberee

Exemple ou nous « liberons de la place » a la position i :

package main

import "fmt"

func main() {
	s := []int{10, 20, 30}

	i := 1
	s = append(s, 0)      // len+1, il y a de la place
	copy(s[i+1:], s[i:])  // queue vers la droite de 1
	s[i] = 99             // insertion

	fmt.Println(s) // [10 99 20 30]
}

Ici, copy fait chevaucher les plages : la source et la destination se recouvrent partiellement, et c'est normal — copy sait fonctionner ainsi.

8. Mini-gestionnaire de taches sur []string

Faisons maintenant un petit morceau de code qui ressemble a une partie d'application, et non a de simples exemples abstraits. Supposons que nous ayons une liste minimale de taches []string, et que nous voulions savoir faire :

  1. prendre un « instantane » de la liste (pour l'afficher en securite a l'utilisateur sans risquer qu'on modifie l'original),
  2. deplacer un bloc de taches (pour l'instant sans suppression/insertion complete, seulement la mecanique du deplacement).

Commencons par deux petites fonctions : cloneStrings et shiftLeftByOne.

package main

import "fmt"

// cloneStrings cree une copie independante du slice.
func cloneStrings(src []string) []string {
	dst := make([]string, len(src))
	copy(dst, src)
	return dst
}

// shiftLeftByOne deplace les elements de 1 vers la gauche, en commençant a la position i.
// C'est un "semi-produit" : on ne change pas len, on reecrit simplement.
func shiftLeftByOne(s []string, i int) {
	if i < 0 || i >= len(s)-1 {
		return
	}
	copy(s[i:], s[i+1:])
}

func main() {
	tasks := []string{"buy milk", "read book", "write code"}

	view := cloneStrings(tasks)
	view[0] = "buy coffee"

	fmt.Println("tasks:", tasks) // tasks: [buy milk read book write code]
	fmt.Println("view: ", view)  // view:  [buy coffee read book write code]

	shiftLeftByOne(tasks, 1)
	fmt.Println("shift:", tasks) // shift: [buy milk write code write code]
}

Il y a ici deux sensations « pratiques » importantes.

Premierement, cloneStrings est une vraie protection contre l'aliasing : vous renvoyez une copie vers l'exterieur, et le code externe ne peut pas endommager accidentellement votre etat interne.

Deuxiemement, shiftLeftByOne montre une mecanique honnete : le deplacement est fait, mais il reste une « queue en trop ». Tant que nous n'avons pas modifie len, cette queue reste visible. Dans les prochaines lecons de la journee, nous apprendrons a mener de tels semi-produits jusqu'a une operation terminee (suppression/insertion), mais la base est deja ici.

9. Erreurs typiques lors de l'utilisation de copy

Erreur n°1 : s'attendre a ce que copy modifie len.
En general, cela ressemble a ceci : vous avez fait copy(s[i:], s[i+1:]), vous avez affiche s et vous vous etes demande « pourquoi le dernier element est-il duplique ? ». Parce que copy n'est qu'une reecriture. Ce sont append ou reslice qui modifient la longueur. Si vous gardez en tete la regle « copy ne touche pas a la longueur », la moitie de la confusion disparait.

Erreur n°2 : confondre dst et src.
Visuellement, copy(a, b) semble symetrique, mais le sens des arguments est different. Si vous les confondez, vous pouvez abimer les donnees source au lieu de remplir le tampon. Une aide mnemonique : « le premier, c'est vers ou ; le second, c'est d'ou. » Si vous avez un doute, renommez temporairement les variables en dst et src directement dans le code — oui, ce n'est pas la litterature la plus poetique, mais ca marche.

Erreur n°3 : essayer de copier dans un slice avec len=0 en comptant sur cap.
make([]T, 0, 10) donne de la place en memoire, mais pas d'elements en longueur. Pour copy, cela signifie « rien ou copier ». La correction est simple : soit vous creez dst avec la bonne longueur des le depart, soit vous etendez soigneusement dst avec dst = dst[:n] (et seulement si cap(dst) >= n).

Erreur n°4 : faire un deplacement vers la droite sans augmenter la longueur.
Si vous essayez d'« ecarter la queue » avec copy(s[i+1:], s[i:]), mais que len(s) n'a pas ete augmente, vous obtiendrez soit un resultat incorrect, soit (plus souvent) rien d'utile du tout. La logique d'insertion commence toujours par la creation de place en longueur — en general avec append.

Erreur n°5 : oublier que des sous-slices peuvent partager un meme backing array.
Parfois, vous faites head := s[:k], puis vous effectuez des deplacements dans s, et soudain head « change tout seul ». Il ne change pas tout seul : il pointe vers la meme zone que s. Si vous avez besoin d'un head independant, il faudra le cloner avec make + copy. Et oui, c'est exactement le cas ou une allocation supplementaire n'est pas un peche, mais le prix a payer pour la previsibility.

1
Mission
Go SELF, niveau 13, leçon 1
Bloqué
Instantané de la liste
Instantané de la liste
1
Mission
Go SELF, niveau 13, leçon 1
Bloqué
Combien a pu être placé
Combien a pu être placé
1
Mission
Go SELF, niveau 13, leçon 1
Bloqué
Combler le trou
Combler le trou
1
Mission
Go SELF, niveau 13, leçon 1
Bloqué
Insertion urgente
Insertion urgente
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION