https://lafor.ge/feed.xml

Move, Copy et Clone

2022-10-31

Move

Lorsqu'une fonction désire obtenir l'ownership d'une variable.

On vient déplacer la variable du contexte appelant vers le contexte appelé.

Prenons une structure Toto.

missing alt

On déclare une fonction function_1, celle-ci ne fait rien à part prendre l'ownership de la variable passé en paramètre.

missing alt

On déclare dans un main, une variable toto de type Toto.

missing alt

Lorsque l'on passe en paramètre la variable toto à la fonction function_1, on dit que l'on déplace ou move la variable vers function_1.

missing alt

C'est comme si vous aviez un carton dans une maison 1 et que vous le déplaciez dans une maison 2.

missing alt

Et donc très logiquement si on réitère l'expérience, ça coince.

missing alt

Pourquoi, ça coince ?

Et bien, votre carton n'a pas le don d'ubiquité (ce mot est cadeau pour vous 😁). Autrement dit, elle ne peut pas à la fois être dans la maison main et dans la maison function_1.

Donc si les déménageurs arrivent, ils ne trouveront pas le carton dans la maison main! Et ne pourront pas le déplacer vers la maison function_1*

missing alt

Clone

Pour remédier à cette situation, nous allons devoir implémenter un trait.

Celui-ci ce nomme Clone.

Il ne dispose que d'une fonction à implémenter.

trait Clone {
    fn clone(&self) -> Self
}

Ce trait renvoit une duplication de lui-même, cela impliquant que chaque champ de la structure qui l'implémente doivent eux-aussi être clonable.

Pour notre structure Toto ce n'est pas un soucis.

impl Clone for Toto {
    fn clone(&self) {
        *self
    }
}

On déréférence le self et on renvoit une copie.

Un clone est déclenché de manière consciente et explicite par le développeur.

fn main() {
    let toto = Toto;
    let toto_clone = toto.clone(); // l'opération de clone se fait ici
}

Si ce que vous clonez est gros, cela peut avoir des conséquences sur les performances de votre programme.

struct BigData {
    data: [u8; 10000000000]
}

Chaque .clone() aura pour effet de dupliquer chaque valeur du tableau.

Si les opérations de clonage sont trop fréquentes, cela risque d'engorger votre programme.

Il est possible de se passer de l'implémentation du trait Clone en passant par la dérivation.

#[derive(Clone)]
struct Toto;

Nous avons maintenant des armes pour travailler. 😁

missing alt

Il faut voir le clone comme une machine qui prend en entré une variable clonable, en réalise une deuxième version exacte et rend la première version.

missing alt

Notre scénario avec les déménageurs va donc se dérouler en deux temps:

Premièrement, on clone le carton toto en un deuxième carton toto_clone.

missing alt

Puis on demande à l'équipe 1 de déménager le carton étiqueté toto_clone et à l'équipe 2 de déménager le carton étiqueté toto.

missing alt

Chaque équipe trouve son carton, tout le monde est content. 😀

Comme tout à l'heure, toto ayant été déplacé dans function_1*, il n'existe plus dans main.

Cela implique qu'il n'est plus clonable dans main et n'est pas déplaçable dans une troisième maison function_1**

Copy

Il existe un deuxième mécanisme que nous avons d'ailleurs utilisé sans nous en rendre compte, il s'agit de la copie.

Pour pouvoir bénéficier de cette capacité une variable doit être copiable.

Cette attribut est fourni par le trait Copy.

Le trait Copy est un super trait de Clone. Cela implique que pour implémenter Copy une structure doit obligatoirement implémenter Clone.

Le trait Copy est ce qu'on appelle un Marker, un trait sans implémentation.

#[derive(Clone)]
struct Toto;

impl Copy for Toto {}

Il est également possible de passer la dérivation.

#[derive(Clone, Copy)]
struct Toto;

Celle-ci est fourni par le langage et ne peut pas être défini par le développeur.

Les types primimitifs sont tous copiables. A l'exeception des chaîne de caractères qui sont en fait des références.

Une structure composée de types primitifs est donc copiable.

#[derive(Clone, Copy)]
struct Data {
    number: u64,
    float: f64,
    boolean: bool,
    array: [u8; 1024]
}

Quel super-pouvoirs cela nous octroie ?

Et bien, plus besoin de demander explicitement un clone via .clone() tout se fait magiquement !

fn main() {
    let toto = Toto;
    let toto_copy = toto; // la variable est ici copié
}

Ce qui implique que l'on peut écrire ceci

fn main() {
    let toto = Toto;
    function_1(toto); // toto est copié
    function_1(toto); // toto est copié
    function_1(toto); // toto est copié
    function_1(toto);
    // etc ...
    function_1(toto);
    // toto est toujours présent dans main !! 
}

Si on reprend la scenette des déménageurs, c'est comme si à chaque fois qu'on prenait un carton, celui-ci se dupliquait de lui-même.

Ce qui permet à chaque maison function_1 d'avoir sa propre copie de toto.

Autre avantage, le toto originel ne quitte jamais la maison main!

missing alt

Il est donc possible de déménager le carton toto autant de fois que l'on le désire.

La copie étant implicite, il est possible de copier de grandes quantitée de données sans s'en rendre compte !

Conclusion

Si une fonction désire l'ownership d'une variable il faut déplacer la variable du contexte appellant vers le contexte appelé.

Une fois qu'une variable est déplacée dans le contexte appelé, elle n'est plus disponible dans le contexte appellant.

Pour conserver la disponibilité d'une variable dans le contexte appellant, il faut dupliquer la variable.

Soit via un Clone expicite.

missing alt

Soit via un Copy implicite.

missing alt

Toute variable copiable est clonable mais l'inverse n'est pas vrai.

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.