Wie kann sichergestellt werden, dass jede Enum-Variante zur Kompilierungszeit von einer bestimmten Funktion zurückgegeben werden kann?

8

Ich habe eine Aufzählung:

enum Operation {
    Add,
    Subtract,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

Ich möchte beim Kompilieren sicherstellen, dass jede Enum-Variante in der fromFunktion behandelt wird.

Warum brauche ich das? Zum Beispiel könnte ich eine ProductOperation hinzufügen und vergessen, diesen Fall in der fromFunktion zu behandeln:

enum Operation {
    // ...
    Product,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        // No changes, I forgot to add a match arm for `Product`.
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

Kann garantiert werden, dass der Übereinstimmungsausdruck jede Variante einer Aufzählung zurückgibt? Wenn nicht, wie kann dieses Verhalten am besten nachgeahmt werden?

Oleh Misarosh
quelle

Antworten:

13

Eine Lösung wäre, die gesamte Aufzählung, Varianten und Übersetzungsarme mit einem Makro zu generieren:

macro_rules! operations {
    (
        $($name:ident: $chr:expr)*
    ) => {
        #[derive(Debug)]
        pub enum Operation {
            $($name,)*
        }
        impl Operation {
            fn from(s: &str) -> Result<Self, &str> {
                match s {
                    $($chr => Ok(Self::$name),)*
                    _ => Err("Invalid operation"),
                }
            }
        }
    }
}

operations! {
    Add: "+"
    Subtract: "-"
}

Auf diese Weise ist das Hinzufügen einer Variante trivial und Sie können eine Analyse nicht vergessen. Es ist auch eine sehr trockene Lösung.

Es ist einfach, dieses Konstrukt um andere Funktionen (z. B. die inverse Übersetzung) zu erweitern, die Sie sicherlich später benötigen, und Sie müssen das Parsing-Zeichen nicht duplizieren.

Spielplatz

Denys Séguret
quelle
1
Ich werde meine Antwort hinterlassen, aber das ist definitiv besser!
Peter Hall
12

Während es sicherlich eine komplizierte und fragile Möglichkeit gibt, Ihren Code mit prozeduralen Makros zu überprüfen, besteht ein viel besserer Weg darin, Tests zu verwenden. Tests sind robuster, viel schneller zu schreiben und überprüfen die Umstände, unter denen jede Variante zurückgegeben wird, und nicht nur, dass sie irgendwo erscheint.

Wenn Sie befürchten, dass die Tests nach dem Hinzufügen neuer Varianten zur Aufzählung weiterhin bestanden werden, können Sie mithilfe eines Makros sicherstellen, dass alle Fälle getestet werden:

#[derive(PartialEq, Debug)]
enum Operation {
    Add,
    Subtract,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

macro_rules! ensure_mapping {
    ($($str: literal => $variant: path),+ $(,)?) => {
        // assert that the given strings produce the expected variants
        $(assert_eq!(Operation::from($str), Ok($variant));)+

        // this generated fn will never be called but will produce a 
        // non-exhaustive pattern error if you've missed a variant
        fn check_all_covered(op: Operation) {
            match op {
                $($variant => {})+
            };
        }
    }
}

#[test]
fn all_variants_are_returned_by_from() {
   ensure_mapping! {
      "+" => Operation::Add,
       "-" => Operation::Subtract,
   }
}
Peter Hall
quelle