web-dev-qa-db-fra.com

Pourquoi l'assert_eq de Rust! mis en œuvre en utilisant une correspondance?

Voici Rust's assert_eq! implémentation de la macro . Je n'ai copié que la première branche par souci de concision:

macro_rules! assert_eq {
    ($left:expr, $right:expr) => ({
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, left_val, right_val)
                }
            }
        }
    });
}

À quoi sert le match ici? Pourquoi la vérification de la non-égalité n'est-elle pas suffisante?

37
Shmoopy

D'accord, supprimons la correspondance.

    macro_rules! assert_eq_2 {
        ($left:expr, $right:expr) => ({
            if !($left == $right) {
                panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, $left, $right)
            }
        });
    }

Maintenant, prenons un exemple complètement aléatoire ...

fn really_complex_fn() -> i32 {
    // Hit the disk, send some network requests,
    // and mine some bitcoin, then...
    return 1;
}

assert_eq_2!(really_complex_fn(), 1);

Cela s'étendrait à ...

{
    if !(really_complex_fn() == 1) {
        panic!(r#"assertion failed: `(left == right)`
  left: `{:?}`,
 right: `{:?}`"#, really_complex_fn(), 1)
    }
}

Comme vous pouvez le voir, nous appelons la fonction deux fois. C'est loin d'être idéal, d'autant plus si le résultat de la fonction peut changer à chaque appel.

match est juste un moyen rapide et facile d'évaluer les deux "arguments" de la macro exactement une fois et de les lier aux noms de variables.

47
DK.

L'utilisation de match garantit que les expressions $left Et $right Sont chacune évaluées une seule fois, et qui tous les temporaires créés lors de leur évaluation vivent au moins aussi longtemps que les liaisons de résultat left et right.

Une expansion qui utilisait $left Et $right Plusieurs fois - une fois lors de la comparaison, et de nouveau lors de l'interpolation dans un message d'erreur - se comporterait de manière inattendue si l'une ou l'autre expression avait des effets secondaires. Mais pourquoi l'extension ne peut-elle pas faire quelque chose comme let left = &$left; let right = &$right;?

Considérer:

let vals = vec![1, 2, 3, 4].into_iter();
assert_eq!(vals.collect::<Vec<_>>().as_slice(), [1, 2, 3, 4]);

Supposons que cela soit étendu à:

let left = &vals.collect::<Vec<_>>().as_slice();
let right = &[1,2,3,4];
if !(*left == *right) {
    panic!("...");
}

Dans Rust, la durée de vie des produits temporaires produits dans une déclaration est généralement limitée à la déclaration elle-même. Par conséquent, cette expansion est une erreur :

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:5:21
   |
5  |         let left = &vals.collect::<Vec<_>>().as_slice();
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^           - temporary value dropped here while still borrowed
   |                     |
   |                     temporary value does not live long enough

Le vals.collect::<Vec<_>>() temporaire doit vivre au moins aussi longtemps que left, mais en fait il est supprimé à la fin de l'instruction let.

Comparez cela avec l'expansion

match (&vals.collect::<Vec<_>>().as_slice(), &[1,2,3,4]) {
    (left, right) => {
        if !(*left == *right) {
            panic!("...");
        }
    }
}

Cela produit le même temporaire, mais sa durée de vie s'étend sur toute l'expression de correspondance - suffisamment longtemps pour que nous puissions comparer left et right, et les interpoler dans le message d'erreur si la comparaison échoue.

En ce sens, match est la construction let ... in De Rust.

Notez que cette situation est inchangée avec durées de vie non lexicales . Malgré son nom, NLL ne modifie pas la durée de vie des valeurs, c'est-à-dire lorsqu'elles sont supprimées. Il ne fait que préciser la portée des emprunts . Cela ne nous aide donc pas dans cette situation.

2
John