Discussion du code types_avances

Concepts

Les concepts abordés dans cet exemple sont:

  1. Les types énumérés.
  2. Fonctions associées et méthodes.
  3. Les arguments des fonctions en Rust.
  4. Le pattern matching.

Discussion

Les types énumérés

Dans ce code, nous introduisons un type complexe: le type énuméré NumberOrNothing

#![allow(unused)]
fn main() {
enum NumberOrNothing {
    Nothing,
    Number(i32),
}
}

Un type énuméré est très utile quand on veut décrire un objet ayant un nombre fini de formes (de variantes) possibles. Ici nous avons deux variantes pour le type NumberOrNothing:

  1. NumberOrNothing::Nothing qui décrit l’absence de valeur,
  2. NumberOrNothing::Number(i32) qui décrit la présence d'une valeur et qui encapsule un entier 32 bits.

On note ici que pour accéder à une variante de NumberOrNothing il est nécessaire d'utiliser la syntaxe Type::Variante.

Fonctions associées et méthodes

Sur ce type énuméré, nous définissons deux fonctions dans un bloc impl

impl NumberOrNothing {
// une fonction associée et une méthode
}

Ici nous avons deux fonctions. La première, fn new(val), est une fonction associée (elle ne prend pas self en paramètre) : elle crée une instance de NumberOrNothing à partir d'un entier et le stocke dans la variante NumberOrNothing::Number(val). En général la fonction new sur un type est une sorte de "constructeur": elle permet de créer une nouvelle instance d'un type. La notion d'instance est très importante. Si nous prenons un type connu i32, il s'agit du type et let i:i32 = 5; est une instance de ce type.

enum NumberOrNothing {
    Nothing,
    Number(i32),
}
impl NumberOrNothing {
    fn new(val: i32) -> Self {
        NumberOrNothing::Number(val)
    }
}
fn main() {
let num = NumberOrNothing::new(10);
}

On voit ici que pour appeler la fonction new on la préfixe par le nom du type suivi de ::, soit NumberOrNothing::new(val).

La seconde, fn print(self), est une méthode (elle prend self en paramètre) et sert à afficher le contenu d'une instance d'un NumberOrNothing.

enum NumberOrNothing {
    Nothing,
    Number(i32),
}
impl NumberOrNothing {
    fn new(val: i32) -> Self {
        NumberOrNothing::Number(val)
    }
    fn print(self) {
        match self {
            NumberOrNothing::Nothing => println!("No number."),
            NumberOrNothing::Number(val) => println!("The number is: {}", val),
        }
    }
}
fn main() {
let num = NumberOrNothing::new(10);
num.print();
// NumberOrNothing::print(num); équivalent
}

Les arguments des fonctions en Rust

Il y a plusieurs choses à noter dans cette fonction. La première est son premier paramètre, self, qui désigne l'instance sur laquelle la méthode est appelée (self est un raccourci pour self: Self). Ainsi, pour appeler print() on utilise l'opérateur . sur l'instance : num.print(). Il existe une syntaxe équivalente qui serait:

NumberOrNothing::print(num);

Avant de nous intéresser au code qui est présent dans le corps de la fonction print(), essayons d'appeler deux fois de suite la fonction print():

enum NumberOrNothing {
    Nothing,
    Number(i32),
}
impl NumberOrNothing {
    fn new(val: i32) -> Self {
        NumberOrNothing::Number(val)
    }
    fn print(self) {
        match self {
            NumberOrNothing::Nothing => println!("No number."),
            NumberOrNothing::Number(val) => println!("The number is: {}", val),
        }
    }
}
fn main() {
    let num = NumberOrNothing::new(10);
    NumberOrNothing::print(num);
    NumberOrNothing::print(num);
}

On constate que la compilation échoue avec un message intéressant

   Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `num`
  --> src/main.rs:19:28
   |
17 |     let num = NumberOrNothing::new(10);
   |         --- move occurs because `num` has type `NumberOrNothing`, which does not implement the `Copy` trait
18 |     NumberOrNothing::print(num);
   |     --------------------------- `num` moved due to this method call
19 |     NumberOrNothing::print(num);
   |                            ^^^ value used here after move
   |
note: consider changing this parameter type in method `print` to borrow instead if owning the value isn't necessary
  --> src/main.rs:9:14
   |
9  |     fn print(self) {
   |        ----- ^^^^ this parameter takes ownership of the value
   |        |
   |        in this method
note: `NumberOrNothing::print` takes ownership of the receiver `self`, which moves `num`
  --> src/main.rs:9:14
   |
9  |     fn print(self) {
   |              ^^^^

For more information about this error, try `rustc --explain E0382`.

En fait, cela signifie que la fonction est devenue propriétaire de la valeur de num lors du premier appel à print(), puis lorsque la fonction s'est terminée, la variable et les données qu'elle contient ont été détruites automatiquement. Ainsi la variable num ne contient "plus rien" (la variable n'est plus liée à la valeur Number(10)) et ne peut pas rappeler print(). En fait, il s'agit du comportement par défaut des fonctions en Rust. Elles deviennent propriétaires (owners) des valeurs passées en argument et la valeur est déplacée (moved).

Mais alors comment cela se fait que les fonctions

print_tab(tab);
let min = find_min(tab);

ne donnent pas de message d'erreur à la compilation? En fait, ici tab est copié implicitement avant d'être passé à ces deux fonctions. Ainsi, c'est cette valeur copiée qui est déplacée dans la fonction et qui est détruite à la fin. Cela est le cas pour des types "simples" comme tous les types de base de Rust et les tableaux statiques si le type des éléments est également un type "copiable" (on dit qu'ils sont Copy). Ainsi, les types i32, usize et [i32; SIZE] utilisés dans ce programme sont Copy. On verra dans les chapitres suivants plus de détails sur ce fonctionnement.

Le pattern matching

L'autre nouveauté introduite dans ce code est le pattern matching.

match self {
    NumberOrNothing::Nothing => println!("No number."),
    NumberOrNothing::Number(val) => println!("The number is: {}", val),
}

Ici, on vérifie quelle variante du type est encapsulée par notre instance représentée par la variable self. Dans le cas où c'est NumberOrNothing::Nothing, on affiche No number., et dans le cas où c'est NumberOrNothing::Number(val) on lie la valeur encapsulée dans la variante Number et on affiche cette valeur. Le compilateur détectera s'il manque une variante du type énuméré et produira une erreur si cela est le cas.

La syntaxe en général est la suivante

match variable {
    variante_1 => {
        expression
    },
    variante_2 => {
        expression
    },
    // ...
    variante_n => {
        expression
    },
    _ => {
        expression
    },
}

où le dernier _ est un remplacement pour toutes les variantes non traitées du type énuméré (il est similaire au default du switch ... case de C).

enum NumberOrNothing {
    Nothing,
    Number(i32),
}
impl NumberOrNothing {
    fn new(val: i32) -> Self {
        NumberOrNothing::Number(val)
    }
    fn print(self) {
        match self {
            NumberOrNothing::Nothing => println!("No number."),
            NumberOrNothing::Number(val) => println!("The number is: {}", val),
        }
    }
}
const SIZE: usize = 9;
fn read_command_line() -> [i32; SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
}
fn print_tab(tab: [i32; SIZE]) {
    for t in tab {
        print!("{} ", t);
    }
    println!();
}
fn min_i32(lhs: i32, rhs: i32) -> i32 {
    if lhs < rhs {
        lhs
    } else {
        rhs
    }
}
fn find_min(tab: [i32; SIZE]) -> NumberOrNothing {
    let mut min = NumberOrNothing::Nothing;
    for t in tab {
        match min {
            NumberOrNothing::Nothing => min = NumberOrNothing::new(t),
            NumberOrNothing::Number(val) => min = NumberOrNothing::new(min_i32(val, t)),
        }
    }
    min
}
fn main() {
    let tab = read_command_line();
    println!("Among the numbers in the list:");
    print_tab(tab);
    let min = find_min(tab);
    min.print();
    let nothing = NumberOrNothing::Nothing;
    NumberOrNothing::print(nothing);
}

Rustlings

Les rustlings à faire dans ce chapitre sont les suivants:

Les types énumérés

$ rustlings run enums1
$ rustlings run enums2
$ rustlings run enums3

Les structures

$ rustlings run structs1
$ rustlings run structs2
$ rustlings run structs3