在现实生活中,许多人对某些食物或物质过敏。医生通常使用测试来确定患者对哪些物质过敏。在 Exercism 的 “allergies” 练习中,我们将构建一个过敏测试系统,它使用一个简单的数值来表示患者的过敏情况。这不仅能帮助我们理解位运算的实际应用,还能深入学习 Rust 中的结构体实现和枚举使用。

问题背景

在这个练习中,过敏测试的结果是一个数值,其中每个位代表一种特定的过敏原。这种使用位掩码表示多个布尔值的技术在计算机科学中非常常见,比如 Unix 文件权限、网络协议标志等。

让我们先看看练习提供的结构和枚举:

pub struct Allergies;

#[derive(Debug, PartialEq)]
pub enum Allergen {
    Eggs,
    Peanuts,
    Shellfish,
    Strawberries,
    Tomatoes,
    Chocolate,
    Pollen,
    Cats,
}

impl Allergies {
    pub fn new(score: u32) -> Self {
        unimplemented!(
            "Given the '{}' score, construct a new Allergies struct.",
            score
        );
    }

    pub fn is_allergic_to(&self, allergen: &Allergen) -> bool {
        unimplemented!(
            "Determine if the patient is allergic to the '{:?}' allergen.",
            allergen
        );
    }

    pub fn allergies(&self) -> Vec<Allergen> {
        unimplemented!("Return the list of allergens contained within the score with which the Allergies struct was made.");
    }
}

我们需要实现这个结构体,使其能够:

  1. 根据给定的分数构建 Allergies 实例
  2. 检查患者是否对特定过敏原过敏
  3. 返回患者过敏的所有过敏原列表

位运算基础

在开始实现之前,让我们理解一下位运算的原理。每种过敏原都被分配了一个特定的位位置:

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Allergen {
    Eggs = 1,        // 2^0 = 1
    Peanuts = 2,     // 2^1 = 2
    Shellfish = 4,   // 2^2 = 4
    Strawberries = 8, // 2^3 = 8
    Tomatoes = 16,   // 2^4 = 16
    Chocolate = 32,  // 2^5 = 32
    Pollen = 64,     // 2^6 = 64
    Cats = 128,      // 2^7 = 128
}

如果一个人对鸡蛋和花生过敏,他们的过敏分数将是 1 + 2 = 3。如果对鸡蛋、贝类和草莓过敏,分数将是 1 + 4 + 8 = 13。

实现 Allergies 结构体

让我们实现 Allergies 结构体:

pub struct Allergies {
    score: u32,
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Allergen {
    Eggs = 1,
    Peanuts = 2,
    Shellfish = 4,
    Strawberries = 8,
    Tomatoes = 16,
    Chocolate = 32,
    Pollen = 64,
    Cats = 128,
}

impl Allergen {
    fn all() -> Vec<Allergen> {
        vec![
            Allergen::Eggs,
            Allergen::Peanuts,
            Allergen::Shellfish,
            Allergen::Strawberries,
            Allergen::Tomatoes,
            Allergen::Chocolate,
            Allergen::Pollen,
            Allergen::Cats,
        ]
    }
}

impl Allergies {
    pub fn new(score: u32) -> Self {
        Allergies { score }
    }

    pub fn is_allergic_to(&self, allergen: &Allergen) -> bool {
        self.score & (*allergen as u32) != 0
    }

    pub fn allergies(&self) -> Vec<Allergen> {
        Allergen::all()
            .into_iter()
            .filter(|allergen| self.is_allergic_to(allergen))
            .collect()
    }
}

代码解析

1. 结构体定义

pub struct Allergies {
    score: u32,
}

我们在结构体中存储过敏分数,这是所有后续操作的基础。

2. 枚举定义

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Allergen {
    Eggs = 1,
    Peanuts = 2,
    Shellfish = 4,
    Strawberries = 8,
    Tomatoes = 16,
    Chocolate = 32,
    Pollen = 64,
    Cats = 128,
}

每个过敏原都有一个显式的值,这些值是 2 的幂,这样每个过敏原都对应二进制表示中的一个位。

3. 检查过敏

pub fn is_allergic_to(&self, allergen: &Allergen) -> bool {
    self.score & (*allergen as u32) != 0
}

这是核心逻辑。我们使用按位与运算符 & 来检查特定的位是否被设置。例如,如果分数是 3(二进制 11),我们要检查是否对鸡蛋过敏(值为 1,二进制 01):

  11 (3)
& 01 (1)
----
  01 (1) != 0  // 对鸡蛋过敏

4. 获取所有过敏原

pub fn allergies(&self) -> Vec<Allergen> {
    Allergen::all()
        .into_iter()
        .filter(|allergen| self.is_allergic_to(allergen))
        .collect()
}

这个方法遍历所有可能的过敏原,过滤出患者确实过敏的那些,并收集到一个向量中。

测试用例分析

通过查看测试用例,我们可以更好地理解需求:

#[test]
fn is_not_allergic_to_anything() {
    let allergies = Allergies::new(0);
    assert!(!allergies.is_allergic_to(&Allergen::Peanuts));
    assert!(!allergies.is_allergic_to(&Allergen::Cats));
    assert!(!allergies.is_allergic_to(&Allergen::Strawberries));
}

分数为 0 表示对任何东西都不过敏。

#[test]
fn is_allergic_to_egg_shellfish_and_strawberries() {
    let allergies = Allergies::new(5);
    assert!(allergies.is_allergic_to(&Allergen::Eggs));
    assert!(allergies.is_allergic_to(&Allergen::Shellfish));
    assert!(!allergies.is_allergic_to(&Allergen::Strawberries));
}

分数 5 是 1 + 4,表示对鸡蛋和贝类过敏,但对草莓不过敏。

#[test]
fn allergic_to_everything() {
    let expected = &[
        Allergen::Eggs,
        Allergen::Peanuts,
        Allergen::Shellfish,
        Allergen::Strawberries,
        Allergen::Tomatoes,
        Allergen::Chocolate,
        Allergen::Pollen,
        Allergen::Cats,
    ];
    let allergies = Allergies::new(255).allergies();

    compare_allergy_vectors(expected, &allergies);
}

分数 255(二进制 11111111)表示对所有 8 种过敏原都过敏。

#[test]
fn scores_over_255_do_not_trigger_false_positives() {
    let expected = &[
        Allergen::Eggs,
        Allergen::Shellfish,
        Allergen::Strawberries,
        Allergen::Tomatoes,
        Allergen::Chocolate,
        Allergen::Pollen,
        Allergen::Cats,
    ];
    let allergies = Allergies::new(509).allergies();

    compare_allergy_vectors(expected, &allergies);
}

这个测试表明,超过 255 的分数不应该影响低 8 位的解释。

优化版本

考虑到分数可能超过 255,我们可以提供一个更健壮的版本:

pub struct Allergies {
    score: u32,
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Allergen {
    Eggs = 1,
    Peanuts = 2,
    Shellfish = 4,
    Strawberries = 8,
    Tomatoes = 16,
    Chocolate = 32,
    Pollen = 64,
    Cats = 128,
}

impl Allergen {
    fn all() -> Vec<Allergen> {
        vec![
            Allergen::Eggs,
            Allergen::Peanuts,
            Allergen::Shellfish,
            Allergen::Strawberries,
            Allergen::Tomatoes,
            Allergen::Chocolate,
            Allergen::Pollen,
            Allergen::Cats,
        ]
    }
}

impl Allergies {
    pub fn new(score: u32) -> Self {
        // 只考虑最低的8位
        Allergies { score: score & 255 }
    }

    pub fn is_allergic_to(&self, allergen: &Allergen) -> bool {
        self.score & (*allergen as u32) != 0
    }

    pub fn allergies(&self) -> Vec<Allergen> {
        Allergen::all()
            .into_iter()
            .filter(|allergen| self.is_allergic_to(allergen))
            .collect()
    }
}

位运算的实际应用

位运算在实际开发中有很多应用:

  1. 权限系统:Unix 文件权限使用类似的机制
  2. 标志位:网络协议和配置选项经常使用位标志
  3. 状态管理:游戏开发中常用位运算管理对象状态
  4. 优化:位运算通常比算术运算更快

性能考虑

在实现中,我们需要注意:

  1. 时间复杂度:[is_allergic_to](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/practice/allergies/src/lib.rs#L18-L21) 是 O(1),[allergies](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/practice/allergies/src/lib.rs#L23-L25) 是 O(n)
  2. 内存效率:使用位运算可以极大地节省存储空间
  3. 缓存友好:预先计算所有过敏原列表可以提高性能

总结

通过 allergies 练习,我们学到了:

  1. 位运算:理解了按位与运算在标志检查中的应用
  2. 枚举:学会了如何给枚举成员分配显式值
  3. 结构体实现:掌握了如何为结构体实现方法
  4. 迭代器链:实践了使用 filter 和 collect 组合处理集合
  5. 实际应用:了解了位运算在现实世界中的用途

这些技能在实际开发中非常有用,特别是在处理权限系统、配置选项和状态管理时。位运算虽然看起来简单,但它是一种高效且强大的技术,是每个程序员都应该掌握的基础技能。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐