Discussion du code bases2

Concepts

Les concepts abordés dans cet exemple sont:

  1. Les types énumérés.
  2. Les fonctions statiques.
  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 valeurs 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 NumberOfNothing il est nécessaire d'utiliser la syntaxe Type::Variante.

Les fonctions statiques

Sur ce type énuméré, nous définissons deux fonctions statiques à l'aide de la syntaxe

impl NumberOrNothing {
// fonction statiques
}

Ici nous avons deux fonctions. La première, fn new(val), créer 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 fonction, fn print(self), 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 qu'elle prend en argument le mot-clé self qui se réfère à l'instance sur laquelle la fonction est appelée. Ainsi quand on veut appeler la fonction print() on utilise la syntaxe du sélecteur sur la variable sur laquelle s'appelle la fonction: 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 a été détruite 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 que 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 [N; i32] 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é 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 a suivante

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

où le dernier _ est un remplacement pour toutes les variantes pas 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

La structures

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