enums - ¿Cómo asegurar que cada variante enum pueda ser devuelta desde una función específica en tiempo de compilación?
rust (2)
Tengo una enumeración:
enum Operation {
Add,
Subtract,
}
impl Operation {
fn from(s: &str) -> Result<Self, &str> {
match s {
"+" => Ok(Self::Add),
"-" => Ok(Self::Subtract),
_ => Err("Invalid operation"),
}
}
}
Quiero asegurarme en tiempo de compilación de que cada variante enum se maneja en la función
from
.
¿Por qué necesito esto?
Por ejemplo, podría agregar una operación de
Product
y olvidarme de manejar este caso en la función
from
:
enum Operation {
// ...
Product,
}
impl Operation {
fn from(s: &str) -> Result<Self, &str> {
// No changes, I forgot to add a match arm for `Product`, "*".
}
}
¿Es posible garantizar que la expresión de coincidencia devuelva cada variante de una enumeración? Si no, ¿cuál es la mejor manera de imitar este comportamiento?
Si bien existe una forma complicada, y frágil, de inspeccionar su código con macros de procedimiento, una ruta mucho mejor es utilizar pruebas. Las pruebas son más robustas, mucho más rápidas de escribir y verificarán las circunstancias en las que se devuelve cada variante, no solo que aparece en algún lugar.
Si le preocupa que las pruebas continúen pasando después de agregar nuevas variantes a la enumeración, puede usar una macro para asegurarse de que se prueben todos los casos:
#[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,
}
}
Una solución sería generar toda la enumeración, variantes y brazos de traducción con una macro:
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: "-"
}
De esta manera, agregar una variante es trivial y no puede olvidar un análisis. También es una solución muy SECA.
Es fácil extender esta construcción con otras funciones (por ejemplo, la traducción inversa) que seguramente necesitará más adelante y no tendrá que duplicar el análisis de caracteres.