すべての列挙型バリアントがコンパイル時に特定の関数から返されるようにする方法は?


8

私は列挙型を持っています:

enum Operation {
    Add,
    Subtract,
}

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

コンパイル時に、列挙型のすべてのバリアントが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`.
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

一致式が列挙型のすべてのバリアントを返すことを保証することは可能ですか?そうでない場合、この動作を模倣する最良の方法は何ですか?

回答:


13

解決策は、マクロを使用して列挙、バリアント、および翻訳アーム全体を生成することです。

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

この方法でバリアントを追加することは簡単で、解析を忘れることはできません。また、非常に乾燥したソリューションです。

この構成を他の関数(たとえば、逆変換)で拡張するのは簡単です。後で必要になるので、解析文字を複製する必要はありません。

遊び場


1
私は答えを残しておきますが、これは間違いなく優れています!
Peter Hall

12

手続き型マクロを使用してコードを検査する方法は確かに複雑で、壊れやすい方法ですが、テストを使用する方がはるかに良い方法です。テストはより堅牢で、作成がはるかに速く、どこかに現れるだけでなく、各バリアントが返される状況を検証します。

列挙型に新しいバリアントを追加した後もテストに合格し続けることが懸念される場合は、マクロを使用して、すべてのケースがテストされることを確認できます。

#[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,
   }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.