package main
import "fmt"
func appendAndChange(numbers []int) {
newNumbers := append(numbers, 42)
newNumbers[0] = 66
fmt.Println("inside", newNumbers)
}
func main() {
slice := []int{1, 2, 3}
fmt.Println("before", slice)
appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is intact")
fmt.Println("-------")
slice = append(slice, 5)
fmt.Println("before", slice)
appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is modified")
}
Output:
before [1 2 3]
inside [66 2 3 42]
after [1 2 3]
original slice is intact
-------
before [1 2 3 5]
inside [66 2 3 5 42]
after [66 2 3 5]
original slice is modified
Alors, pourquoi deux appels à appendAndChange se comportent-ils différemment ??????
Les tranches de Go ont une longueur et une capacité. La longueur d'une tranche est le nombre d'éléments qu'elle contient. La capacité d'une tranche est le nombre d'éléments du tableau sous-jacent, en comptant à partir du premier élément de la tranche. La longueur et la capacité d'une tranche s peuvent être obtenues à l'aide des expressions len(s) et cap(s).
La clé pour comprendre la raison de ce comportement se trouve dans la documentation de append :
La valeur résultante de append est une tranche contenant tous les éléments de la tranche originale plus les valeurs fournies. Si le tableau de sauvegarde de s est trop petit pour contenir toutes les valeurs fournies, un tableau plus grand sera alloué. La tranche renvoyée pointera vers le tableau nouvellement alloué.
Passons en revue chaque étape.
Premièrement, tranche := []int{1, 2, 3}
Une tranche est créée avec len=3 et capacity=3. slice pointe vers un tableau sous-jacent où les données sont effectivement stockées :

Next,
appendAndChange(slice)
newNumbers := append(numbers, 42)
newNumbers[0] = 66
où nombres = tranche.
Nous essayons d'ajouter 42 à la tranche, mais la tranche n'a qu'une capacité de 3, et le tableau d'appui est trop petit pour contenir toutes les valeurs, donc un tableau plus grand est alloué, et la tranche retournée pointe vers le tableau nouvellement alloué.

Note : Pour ce code, la capacité de la tranche retournée semble doubler pour atteindre 6, mais la stratégie de croissance n'est pas documentée.
Nous disposons maintenant d'un nouveau tableau d'appui que nouveauxNombres
pointe vers lui. Après avoir retourné la fonction appendAndChange()
la fonction newNumbers
sort du champ d'application, et notre tranche originale reste inchangée.
Nous appelons maintenant
slice = append(slice, 5)

Un nouveau tableau d'appui est créé et la variable slice est mise à jour pour pointer vers lui. L'ancien tableau d'accompagnement n'a plus de pointeurs vers lui et sort de la portée. Cette fois, lorsque appendAndChange
est appelée, la fonction interne append()
n'a pas besoin de créer un nouveau tableau d'accompagnement, car la capacité est suffisante.
newNumbers := append(numbers, 42)
newNumbers[0] = 66
où nombres = tranche

Maintenant, la tranche et les nombres pointent vers le nouveau tableau d'appui, mais la tranche a une longueur de 4 et les nombres de nouveauxNombres
a une longueur de 5. Cela conduit à :
nouveauxNuméros = [66, 2, 3, 5, 42]
tranche = [66, 2, 3, 5]
Correct Go
Il est vrai que la résolution de ce problème est très simple et que la réponse se trouve dans la documentation elle-même :
Append renvoie la tranche mise à jour. Il est donc nécessaire de stocker le résultat de append, souvent dans la variable contenant la tranche elle-même :
https://pkg.go.dev/builtin#append
Cela signifie que nous pouvons simplement renvoyer nouveauxNombres
dans appendAndChange
et nous assurer que nous définissons la tranche à cette valeur lorsque nous l'appelons, comme suit :
https://go.dev/play/p/Vkm72dXxpnx
package main
import "fmt"
func appendAndChange(numbers []int) []int {
newNumbers := append(numbers, 42)
newNumbers[0] = 66
fmt.Println("inside", newNumbers)
return newNumbers
}
func main() {
slice := []int{1, 2, 3}
fmt.Println("before", slice)
slice = appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is modified")
fmt.Println("-------")
slice = append(slice, 5)
fmt.Println("before", slice)
slice = appendAndChange(slice)
fmt.Println("after", slice)
fmt.Println("original slice is modified")
}
Output
before [1 2 3]
inside [66 2 3 42]
after [66 2 3 42]
original slice is modified
-------
before [66 2 3 42 5]
inside [66 2 3 42 5 42]
after [66 2 3 42 5 42]
original slice is modified
This works, and technically is the correct way to use append according to the documentation, but developers should not ha
Cela fonctionne, et c'est techniquement la bonne façon d'utiliser append selon la documentation, mais les développeurs ne devraient pas avoir à lire la documentation de la bibliothèque de base pour rechercher des cas particuliers.
Présentation de Rust
A titre de comparaison, essayons de reproduire ce comportement en Rust, qui dispose de plus de contrôles de sécurité à la compilation.
#[allow(non_snake_case)]
fn appendAndChange(mut numbers: Vec<i32>) -> () {
numbers.push(24);
numbers[0] = 66;
println!("inside {:?}", numbers);
}
fn main() {
let mut slice = vec![1, 2, 3];
println!("before: {:?}", slice);
appendAndChange(slice);
println!("after: {:?}", slice);
println!("original slice is modified");
println!("-------");
slice.push(5);
println!("before: {:?}", slice);
appendAndChange(slice);
println!("after: {:?}", slice);
println!("original slice is modified");
}
Output: (just the first error...)
error[E0382]: borrow of moved value: `slice`
--> src/main.rs:12:29
|
9 | let mut slice = vec![1, 2, 3];
| --------- move occurs because `slice` has type `Vec<i32>`, which does not implement the `Copy` trait
10 | println!("before: {:?}", slice);
11 | appendAndChange(slice);
| ----- value moved here
12 | println!("after: {:?}", slice);
| ^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function appendAndChange to borrow instead if owning the value isn't necessary
Les vecs de Rust sont très similaires aux slices de Go. Cependant, au lieu que push soit une fonction, c'est une méthode, qui doit prendre une référence mutable à self, et qui retourne (). https://doc.rust-lang.org/std/vec/struct.Vec.html#method.push Cette différence subtile mais puissante devrait suffire à éviter toute confusion sur la manière d'utiliser la bibliothèque standard.
Cette méthode échoue parce que la valeur de slice est déplacée dans appendAndChange, mais qu'elle est à nouveau référencée après le déplacement. Voici quelques images (très simplifiées) pour expliquer ce qui se passe :
Nous commençons par :

Puis appendAndChange
arrive et prend toute la valeur. Notre message d'erreur nous le fait savoir :
--------- move occurs because `slice` has type `Vec<i32>`, which does not implement the `Copy` trait
appendAndChange(slice) ;
| La valeur ----- est déplacée ici

Rust ne permet pas au tableau d'avoir 2 pointeurs vers lui, donc déplace les données du tableau vers les nombres, en enlevant le pointeur de la tranche.
Ensuite, des modifications sont apportées aux nombres, de sorte que les nombres sont maintenant [66, 2, 3, 24]
et appendAndChange()
retourne à la fonction principale.
Jusqu'à présent, tout s'est déroulé avec succès, et si nous n'avions plus de lignes de code, le programme se compilerait et fonctionnerait correctement. Mais nous voulons que la nouvelle valeur des nombres soit réintégrée dans notre fonction principale
println !("after : {:?}", slice) ;
Voici maintenant notre véritable erreur :
println !("after : {:?}", slice) ;
| ^^^^^ valeur empruntée ici après le déplacement
Le compilateur nous dit : "Attendez, vous essayez d'imprimer 'slice' ? Vous avez déplacé la valeur loin de slice. Je ne vais pas pouvoir le faire parce que tranche n'a plus de valeur maintenant" Heureusement pour nous, cela donne aussi une suggestion :
note : envisagez de modifier ce type de paramètre dans la fonction `appendAndChange
` pour emprunter à la place si posséder la valeur n'est pas nécessaire.
Rust nous dit que nous essayons de faire quelque chose qui n'est pas sûr, et il nous arrête. Essayons tout de même de le contourner. Ils suggèrent d'emprunter le paramètre pour appendAndChange
. Passons donc une référence mutable :
#[allow(non_snake_case)]
fn appendAndChange(numbers: &mut Vec<i32>) -> () {
numbers.push(24);
numbers[0] = 66;
println!("inside {:?}", numbers);
}
fn main() {
let mut slice = vec![1, 2, 3];
println!("before: {:?}", slice);
appendAndChange(&mut slice);
println!("after: {:?}", slice);
println!("original slice is modified");
println!("-------");
slice.push(5);
println!("before: {:?}", slice);
appendAndChange(&mut slice);
println!("after: {:?}", slice);
println!("original slice is modified");
}
Output:
before: [1, 2, 3]
inside [66, 2, 3, 24]
after: [66, 2, 3, 24]
original slice is modified
-------
before: [66, 2, 3, 24, 5]
inside [66, 2, 3, 24, 5, 24]
after: [66, 2, 3, 24, 5, 24]
original slice is modified
Et voilà ! Comme ça, ça fonctionne maintenant et il n'y a pas de cas étranges à craindre. Voici comment cela fonctionne. Lorsque nous passons une référence mutable de la tranche à appendAndChange
il crée un nouveau pointeur :

Maintenant, les nombres peuvent modifier les valeurs sous-jacentes du tableau. Lorsque appendAndChange
retourne à la fonction main()
les nombres sortent de la portée, mais la tranche reste un pointeur vers les nouvelles valeurs.
Conclusion
Fondamentalement, le problème en Go provenait du fait que deux pointeurs avec des attributs différents pointaient vers la même valeur :

Le compilateur Rust ne permet pas que cela se produise, ce qui évite de nombreux bogues potentiels au moment de la compilation !