Rust中具有复杂条件的if-else链的替代方法

41ik7eoe  于 2022-11-12  发布在  其他
关注(0)|答案(3)|浏览(203)

在rust中,我有时需要编写链接的if-else语句。它们不是管理多个条件的最好方法,因为条件中有多个东西要检查。
下面是一个人工的rust-playgroung example,在下面的代码中,您可以看到所讨论的if-else链。

// see playground for rest of the code

fn check_thing(t: Thing) -> CarryRule {
    let allowed = vec!["birds","dogs","cats","elefants","unknown","veggies","meat"];
    let max_carry_size = 30;
    let max_carry_unknown_size = 3;
    let max_carry_dog_size = 5;
    let typ = &t.typ.as_str();

    if t.size > max_carry_size {
        CarryRule::Forbidden
    } else if ! allowed.contains(typ) {
        CarryRule::Forbidden
    } else if t.typ == "unknown" && t.size > max_carry_unknown_size {
        CarryRule::Forbidden
    } else if t.typ == "dogs" && t.size > max_carry_dog_size {
        CarryRule::Forbidden
    } else if t.typ == "birds" {
        CarryRule::UseCage
    } else {
        CarryRule::UseBox
    }
}

我知道,我应该使用match语句,但我不知道如何使用单个匹配块来执行上述所有检查。

  • 匹配t.typ
  • 检查t.size是否大于某个值
  • 调用allowed.contains(typ)函数

我正在寻找Go语言的非参数化switch case的Rust版本,如下所示。

switch {
case a && b: return 1
case c || d: fallthrough
case e || f: return 2
default:     return 0
}

当然,我也可以重构整个示例,以更一致的方式对t.sizet.typallowed列表进行建模,以允许更好的match块。但有时这些类型超出了我的控制范围,我不想用太多额外的 Package 来 Package 给定的类型。
在Rust中,有什么好的可读替代方案可以替代这种具有复杂条件的if-else链?

qmelpv7a

qmelpv7a1#

如果你有一个enum和一个struct,你可以更好地处理这个问题:

enum Item {
  Bird,
  Dog,
  Cat,
  Elephant,
  Vegetable,
  Meat,
  Unknown
}

impl Item {
  pub fn can_check(&self) -> bool {
    match self {
      Bird | Cat | Dog => true,
      _ => false
    }
  }
}

现在您可以组成一个整洁的容器:

struct CarryOn {
  item: Item,
  size: usize,
}

impl CarryOn {
  pub fn can_check(&self) -> CarryRule {
    if !self.item.can_check() {
      return CarryRule::Forbidden;
    }

    match (self.item, self.size) {
      (Item::Unknown, s) if s < max_carry_unknown_size => CarryRule::Forbidden,
      (_,_) => CarryRule::UseBox
    }
  }
}

这里的想法是尝试使用一种更像Rust-like的表达式来表示您的情况,并使用match工具和元组来尽可能好地覆盖所有古怪的情况。
尝试将其分解为一系列更小、更简单、更容易理解的测试也是一个好主意,比如can_check()的委托。

pod7payv

pod7payv2#

您可以使用火柴防护装置:

match () {
    () if t.size > max_carry_size => CarryRule::Forbidden,
    () if !allowed.contains(typ) => CarryRule::Forbidden,
    () if t.typ == "unknown" && t.size > max_carry_unknown_size => CarryRule::Forbidden,
    () if t.typ == "dogs" && t.size > max_carry_dog_size => CarryRule::Forbidden,
    () if t.typ == "birds" => CarryRule::UseCage,
    () => CarryRule::UseBox,
}

不过,我认为使用if-else链更好。

woobm2wo

woobm2wo3#

根据其他答案中的好建议,这里是一个playground和我的首选解决方案,它使用了以下新的check_thing函数。

const MAX_CARRY_SIZE: usize= 30;
const MAX_CARRY_UNKNOWN_SIZE: usize = 3;
const MAX_CARRY_DOG_SIZE: usize = 5;
const FORBIDDEN: [Typ; 1] = [Typ::Cake];

fn check_thing(t: Thing) -> CarryRule {
    // First apply the general rules and use early returns.
    // This is more readable then if-else-ing all cases from the beginning.
    if t.size > MAX_CARRY_SIZE {
        return CarryRule::Forbidden
    }
    if FORBIDDEN.contains(&t.typ) {
        return CarryRule::Forbidden
    }

    // Match all cases that need special handling using a single `enum`.
    // The rust compiler will help you to have a match for any enum value.
    // Implement special case logic as match guards for the matching enum value.
    match t.typ {
        Typ::Dog if t.size > MAX_CARRY_DOG_SIZE => CarryRule::Forbidden,
        Typ::Unknown if t.size > MAX_CARRY_UNKNOWN_SIZE => CarryRule::Forbidden,
        Typ::Bird => CarryRule::UseCage,
        // use a default for all non-special cases
        _ => CarryRule::UseBox,
    }
}

这个解决方案采用了所提出的枚举和匹配保护,但也关注可读性。它试图将所有的if-else逻辑保留在check_thing函数中,就像以前一样。和以前一样,它一步一步地应用这个逻辑,就像它在if-else链中一样。
但让我们回到最初的问题:
在Rust中,有什么好的可读替代方案可以替代这种具有复杂条件的if-else链?
这里有一些可以帮助和补充使用的选项。

匹配和保护

使用一个简单的match语句(只有一个参数),并将一些复杂的语句实现为match guards。另外,不要使用字符串进行匹配,而是使用一个enum。这样,rust编译器就可以帮助您覆盖所有的逻辑。
一个带有一个参数和额外保护的较长的match可以保持相当的可读性,如果你正在寻找类似Go语言的普通switch的东西,这是你能得到的最接近的东西。

分开

将if-else块拆分为单独的部分,使用早期返回,而不是添加更多的else块(参见示例中的通用部分和特定部分)。如果匹配保护太复杂,请找到通用部分并将其上移。

泛化

将复杂的条件表达为对象的配置,而不是使用长的if-else链。使用trait或简单地添加enum有助于捕获一些逻辑,并使实体更具可配置性。
如果你从一个复杂的if-else链开始,这种重构和泛化需要更多的代码修改,但是它可以使你的逻辑变得可配置和可扩展,如果你是代码库的所有者,它可能是首选的解决方案。
这是一个广义的版本。

const MAX_CARRY_SIZE: usize= 30;
const MAX_CARRY_UNKNOWN_SIZE: usize = 3;
const MAX_CARRY_DOG_SIZE: usize = 5;

impl Typ {
    pub fn is_carriable(&self) -> bool {
        match self {
            Self::Cake => false,
            _ => true,
        }
    }
    pub fn carry_rule(&self) -> CarryRule {
        match self {
            Self::Bird => CarryRule::UseCage,
            _ => CarryRule::UseBox,
        }
    }
}

impl Thing {
    fn has_allowed_carry_size(&self) -> bool {
        if self.size > MAX_CARRY_SIZE {
            return false
        }
        match self.typ {
            Typ::Dog => self.size <= MAX_CARRY_DOG_SIZE,
            Typ::Unknown => self.size <= MAX_CARRY_UNKNOWN_SIZE,
            _ => true
        }
    }

    pub fn is_carriable(&self) -> bool {
        self.typ.is_carriable() && self.has_allowed_carry_size()
    }

    pub fn carry_rule (&self) -> CarryRule {
        if !self.is_carriable() {
            return CarryRule::Forbidden
        }
        self.typ.carry_rule()
    }
}

pub fn check_thing(t: Thing) -> CarryRule { t.carry_rule() }

正如您所看到的,它将Typsize逻辑分开,这样做的好处是使内容可配置和可重用,但缺点是进位逻辑现在在不同的地方,您不能再像在最初的if-else块或在本文开头我的首选解决方案中那样从上到下阅读规则。
你应该采取哪种解决方案取决于你的项目要求。
最后,感谢您的所有意见和答案,并教我一些更多的 rust 今天!

相关问题