Rust 练习册 53:位运算与过敏测试架构
在现实生活中,许多人对某些食物或物质过敏。医生通常使用测试来确定患者对哪些物质过敏。在 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.");
}
}
我们需要实现这个结构体,使其能够:
- 根据给定的分数构建 Allergies 实例
- 检查患者是否对特定过敏原过敏
- 返回患者过敏的所有过敏原列表
位运算基础
在开始实现之前,让我们理解一下位运算的原理。每种过敏原都被分配了一个特定的位位置:
#[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()
}
}
位运算的实际应用
位运算在实际开发中有很多应用:
- 权限系统:Unix 文件权限使用类似的机制
- 标志位:网络协议和配置选项经常使用位标志
- 状态管理:游戏开发中常用位运算管理对象状态
- 优化:位运算通常比算术运算更快
性能考虑
在实现中,我们需要注意:
- 时间复杂度:[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)
- 内存效率:使用位运算可以极大地节省存储空间
- 缓存友好:预先计算所有过敏原列表可以提高性能
总结
通过 allergies 练习,我们学到了:
- 位运算:理解了按位与运算在标志检查中的应用
- 枚举:学会了如何给枚举成员分配显式值
- 结构体实现:掌握了如何为结构体实现方法
- 迭代器链:实践了使用 filter 和 collect 组合处理集合
- 实际应用:了解了位运算在现实世界中的用途
这些技能在实际开发中非常有用,特别是在处理权限系统、配置选项和状态管理时。位运算虽然看起来简单,但它是一种高效且强大的技术,是每个程序员都应该掌握的基础技能。
浙公网安备 33010602011771号