Как гарантировать, что каждый вариант enum может быть возвращен из определенной функции во время компиляции? - PullRequest
7 голосов
/ 05 ноября 2019

У меня есть enum:

enum Operation {
    Add,
    Subtract,
}

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

Я хочу убедиться, что во время компиляции каждый вариант enum обрабатывается в функции from.

Зачем мне это нужно? Например, я мог бы добавить операцию Product и забыть обработать этот случай в функции from:

enum Operation {
    // ...
    Product,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        // No changes, I forgot to add a match arm for `Product`, "*".
    }
}

Можно ли гарантировать, что выражение соответствия возвращает каждый вариант перечисления? Если нет, то как лучше всего имитировать это поведение?

Ответы [ 2 ]

13 голосов
/ 05 ноября 2019

Решением было бы сгенерировать все перечисление, варианты и ветви перевода с помощью макроса:

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: "-"
}

Таким образом, добавление варианта тривиально, и вы не можете забыть синтаксический анализ. Это также очень СУХОЕ решение.

Легко расширить эту конструкцию с помощью других функций (например, обратного перевода), которые вам наверняка понадобятся позже, и вам не придется дублировать синтаксический анализ.

детская площадка

12 голосов
/ 05 ноября 2019

Хотя, безусловно, существует сложный и хрупкий способ проверки вашего кода с помощью процедурных макросов, гораздо лучше использовать тесты. Тесты более надежны, намного быстрее пишут и проверяют обстоятельства, при которых возвращается каждый вариант, а не только то, что он где-то появляется.

Если вы обеспокоены тем, что тесты могут продолжить проходить после добавления новоговарианты перечисления, вы можете использовать макрос для проверки всех случаев:

#[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,
   }
}
...