input en rust: trim, unwrap et to_string

a marqué ce sujet comme résolu.

Salut, dans coding game on peut retrouver ceci:

use std::io;

macro_rules! parse_input {
    ($x:expr, $t:ident) => ($x.trim().parse::<$t>().unwrap())
}

fn main() {

    // game loop
    loop {
        let mut input_line = String::new();
        io::stdin().read_line(&mut input_line).unwrap();
        
        let enemy_1 = input_line.trim().to_string(); // name of enemy 1
        let mut input_line = String::new();
        
        io::stdin().read_line(&mut input_line).unwrap();
        let dist_1 = parse_input!(input_line, i32); // distance to enemy 1
        
        let mut input_line = String::new();
        io::stdin().read_line(&mut input_line).unwrap();
        
        let enemy_2 = input_line.trim().to_string(); // name of enemy 2
        let mut input_line = String::new();
        
        io::stdin().read_line(&mut input_line).unwrap();
        let dist_2 = parse_input!(input_line, i32); // distance to enemy 2

        // Write an action using println!("message...");

        // Enter the code here

    }
}

à quoi servent trim, unwrap et to_string dans ce cas de figure ? le bout de code à inserer était

if dist_1 < dist_2 {
    println!("{}", enemy_1);
} else {
    println!("{}", enemy_2);
}

et que signifie:

($x:expr, $t:ident) => ($x.trim().parse::<$t>().unwrap())

Que de questions ^^

unwrap

unwrap est une méthode des Result<T, E> (ou des Option<T>, y’a la même) de Rust, une structure qui peut contenir ou bien un résultat (Ok()) ou une erreur (Err()).

Si le résultat contient quelque chose, alors unwrap va retourner ce qu’il contient, en “retirant la couche” qu’était le Result, d’où le nom. Par exemple :

let x = Ok("zeste");  // Construit un Result qui contient un succès (Ok).
assert_eq!(x.unwrap(), "zeste");

Par contre, si le Result est en erreur (Err), unwrap va paniquer, c’est pourquoi on préfère utiliser unwrap_or qui permet de fournir une alternative dans ce cas précis, ou encore mieux un match pour gérer l’erreur — mais je diverge, c’est juste pour info.

let x: Result<&str, &str> = Err("emergency failure");

x.unwrap();                                 // va paniquer
assert_eq!(x.unwrap_or("zeste"), "zeste");  // ok

match x {
    Ok(s) => // faire quelque chose avec s,
    Err(e) => // gérer le problème
}

Ici :

let mut input_line = String::new();
io::stdin().read_line(&mut input_line).unwrap();

…la méthode read_line retourne un Result (qui va être Ok si la lecture de l’entrée standard a réussi, et Err dans le cas contraire). On utilise unwrap en partant du principe que ça va marcher, essentiellement car on est dans un cas où si ça plante pour ça, on s’en fout un peu. Mais là, si la lecture de stdin échoue, le programme va planter.

À noter qu’ici, on ignore le retour de read_line car cette fonction va modifier la variable directement par référence (pour ça qu’on lui passe un pointeur mutable en argument) — le Result qu’elle retourne contient en fait le nombre d’octets lus, et il faut dire qu’on s’en fiche un peu. On utilise uniquement unwrap pour que le programme panique si la lecture échoue. Mais dans la majorité des cas (on va d’ailleurs en voir un plus bas) on utilise unwrap pour récupérer ce que la méthode retourne ^^ .

trim

trim est une méthode de String qui permet de retirer les nouvelles lignes, tabulations, espaces, etc., des extrémités d’une chaîne de caractère. Par exemple :

let s = "     Zeste de\tsavoir\t\n";
assert_eq!("Zeste de\tsavoir", s.trim());

C’est une méthode très classique qu’on retrouve dans beaucoup d’autres langages de programmation sous le même nom (sauf Python où elle s’appelle strip).

to_string

La méthode to_string est une méthode commune (elle vient du trait ToString et est implémentée par la majorité des types standard de Rust) qui permet de transformer une structure en une String. Par exemple :

let i = 5;
let five = String::from("5");

assert_eq!(five, i.to_string());

Dans le cas présent :

let enemy_1 = input_line.trim().to_string();

…ça va surtout servir à convertir le retour de trim, qu’est une &str, en String, afin d’éviter des problèmes d'ownership. Si tu ne maîtrises pas trop ça, je t’invite à (re)lire la partie concernée du guide, car en gros, &str est le pendant emprunté (borrowed) de String.

($x:expr, $t:ident) => ($x.trim().parse::<$t>().unwrap())

Le code complet était :

macro_rules! parse_input {
    ($x:expr, $t:ident) => ($x.trim().parse::<$t>().unwrap())
}

macro_rules! est une macro qui sert à définir une… macro. (Plus généralement, en Rust, toute instruction qui se termine par un ! est une macro.) En gros, c’est un outil en Rust qui sert à faire de la génération de code avant la compilation, pour simplifier l’écriture de parties répétitives (ça rappelle un peu le pré-processeur en C ou C++).

C’est une fonctionnalité assez avancée de Rust (d’ailleurs elle est présentée presque à la fin du Rust Book). Ici, quand on utilise la macro, Rust va, avant la compilation, transformer ceci :

let dist_2 = parse_input!(input_line, i32); // distance to enemy 2

en cela :

let dist_2 = input_line.trim().parse::<i32>().unwrap()

…ce qui concrètement va utiliser la méthode parse pour convertir la chaîne de caractères entrée en i32 (un nombre positif ou négatif sur 32 bits). On retrouve d’ailleurs unwrap, car parse retourne également un Result (par conséquent, si l’utilisateur entre autre chose qu’un nombre, le programme va paniquer, vu qu’on utilise pas unwrap_or ou un pattern matching pour gérer l’éventuelle erreur).

Le langage de création de macros est un peu complexe, mais pour l’idée, si on décortique cette macro toute simple, ça va donner quelque chose comme ça :

macro_rules! parse_input {  // le nom de la macro
    // Définit les paramètres de la macro
    (
        $x:expr, // Le premier s'appellera $x et sera une expression (plus haut `input_line`)
        $t:ident // Le second s'appellera $t et sera un type de données (plus haut `i32`)
    ) 

    // On transforme ces deux arguments en ce code là qui sera généré par Rust et qui remplacera
    // la macro avant la compilation
    => ($x.trim().parse::<$t>().unwrap())
}

Toute la puissance des macros se fait sentir quand elles peuvent générer du code plus complexe, avec des boucles ou similaire. Par exemple, tu as peut-être déjà croisé la macro vec! qui permet de créer un vecteur (une liste). En fait, elle va transformer le code qu’elle reçoit ainsi :

let list = vec![1, 2, 3];

// devient, en simplifié :
// (en réalité, il y a également des contrôles et de l'allocation mémoire plus précise)

let list = {
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

Et le premier est, on l’admettra, bien plus agréable à écrire ^^ .

Note subsidiaire

Pour information, tu peux colorer le code Rust en précisant le langage après les trois accents grave :

```rust
let x = 42;
```
+3 -0

Pardon, je m’incruste juste un peu. Mais c’est une question sur le code de l’OP.

io::stdin().read_line(&mut input_line).unwrap();

C’est quoi l’idée là ? Pourquoi on unwrap() à la fin sans stocker le résultat ? Pour le fun ? Pour paniquer si les données ne sont pas valides ?

J’ai pas regardé la doc, mais un ip::stdin, typiquement pour moi ça devrait retourner un Result. Donc unwrrap me semble inutile. À moins qu’on veuille forcer le plus tôt possible la panique.

+0 -0

C’est ce que j’ai supposé, personnellement, paniquer directement si on a pas de lecture valide (vu que input_line resterait vide et serait donc impossible à traiter). Mais ce n’est qu’une hypothèse.

J’ai pas regardé la doc, mais un io::stdin, typiquement pour moi ça devrait retourner un Result. Donc unwrrap me semble inutile. À moins qu’on veuille forcer le plus tôt possible la panique.

Ça retourne bien un Result qui contient le nombre d’octets lus.


Au passage, ce n’est pas son propre code, mais celui d’un site de défis de programmation ^^

Salut, dans coding game on peut retrouver ceci […]

+1 -0

Vue la façon dont c’est présenté, je soupçonne que ce ne soit pas son propre code mais un code d’un site d’exercices ou de défis quelconque ^^

Amaury

Oui, c’est exactement ça ^^

Coding game présente des exercices en rapport avec des jeux liés à la programmation dans divers langages.

(vu que input_line resterait vide et serait donc impossible à traiter)

Amaury

Aaaah ! J’ai zappé ça !
Du coup, c’est certainement le moyen le plus rapide de différencié une entrée vide d’une erreur. Ça reste surprenant comme pratique je trouve.

+1 -0

Pour du quick & dirty ça ne me choque pas, mais pour du code plus propre on a vu mieux je suis d’accord

Coding game présente des exercices en rapport avec des jeux liés à la programmation dans divers langages.

Yep j’ai modifié après en relisant l’intro (mais tu as répondu trop vite), j’avais zappé que le site y était explicité ^^ .

+0 -0

Salut! Juste pour dire qu’il y a une troisième manière de traiter un Result : l’opérateur de propagation d’erreur ?.

Pour du quick & dirty ça ne me choque pas, mais pour du code plus propre on a vu mieux je suis d’accord

Amaury

Je vois de toute façon mal comment gérer une erreur avec un flux standard. À part utiliser expect au lieu de unwrap ou propager l’erreur qui résultera de toute façon en un appel à panic!, il n’y a pas vraiment d’autre façon, encore moins multi-plateforme.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte