Rust 练习册 31:啤酒歌与字符串格式化艺术

在编程学习过程中,我们经常会遇到一些看似简单但实际涉及复杂逻辑处理的问题。"Beer Song"(啤酒歌)就是这样一个经典练习,它源自一首传统的倒数歌曲,需要根据不同的数字生成不同的歌词。在 Exercism 的 "beer-song" 练习中,我们将实现这首经典的倒数歌曲,这不仅能帮助我们掌握字符串格式化的技巧,还能深入学习 Rust 中的模式匹配和条件逻辑处理。

啤酒歌的规则

啤酒歌是一首简单的倒数歌曲,歌词遵循以下模式:

  1. 对于 n > 1:

    复制代码
    {n} bottles of beer on the wall, {n} bottles of beer.
    Take one down and pass it around, {n-1} bottles of beer on the wall.
  2. 对于 n = 1:

    复制代码
    1 bottle of beer on the wall, 1 bottle of beer.
    Take it down and pass it around, no more bottles of beer on the wall.
  3. 对于 n = 0:

    复制代码
    No more bottles of beer on the wall, no more bottles of beer.
    Go to the store and buy some more, 99 bottles of beer on the wall.

让我们先看看练习提供的函数签名:

rust 复制代码
pub fn verse(n: u32) -> String {
    unimplemented!("emit verse {}", n)
}

pub fn sing(start: u32, end: u32) -> String {
    unimplemented!("sing verses {} to {}, inclusive", start, end)
}

我们需要实现这两个函数,其中:

  1. verse 函数生成单节歌词
  2. sing 函数生成从 start 到 end 的完整歌曲(包含两端)

算法实现

1. 单节歌词实现

首先实现 verse 函数,这是整个练习的核心:

rust 复制代码
pub fn verse(n: u32) -> String {
    match n {
        0 => String::from("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"),
        1 => String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
        2 => String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
        _ => format!(
            "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
            n,
            n,
            n - 1
        ),
    }
}

这个实现使用了模式匹配来处理不同的情况:

  • 0 的特殊情况
  • 1 的特殊情况
  • 2 的特殊情况(因为 "1 bottle" 是单数)
  • 其他情况的通用格式

2. 完整歌曲实现

接下来实现 sing 函数:

rust 复制代码
pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

这个实现使用了函数式编程的方法:

  • 生成从 end 到 start 的数字范围
  • 反转范围以实现倒数效果
  • 对每个数字应用 verse 函数
  • 将结果连接成完整歌曲

测试用例分析

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

rust 复制代码
#[test]
fn test_verse_0() {
    assert_eq!(beer::verse(0), "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n");
}

第0节歌词的特殊处理。

rust 复制代码
#[test]
fn test_verse_1() {
    assert_eq!(beer::verse(1), "1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n");
}

第1节歌词中使用 "bottle"(单数)和 "it"。

rust 复制代码
#[test]
fn test_verse_2() {
    assert_eq!(beer::verse(2), "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n");
}

第2节歌词中下一句使用 "1 bottle"(单数)。

rust 复制代码
#[test]
fn test_song_3_0() {
    assert_eq!(beer::sing(3, 0), "3 bottles of beer on the wall, 3 bottles of beer.\nTake one down and pass it around, 2 bottles of beer on the wall.\n\n2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n\n1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n\nNo more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n");
}

完整歌曲的生成,注意节与节之间用空行分隔。

优化实现

我们可以进一步优化实现,使其更加清晰和高效:

rust 复制代码
pub fn verse(n: u32) -> String {
    match n {
        0 => no_more_bottles(),
        1 => one_bottle(),
        2 => two_bottles(),
        _ => many_bottles(n),
    }
}

fn no_more_bottles() -> String {
    String::from("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n")
}

fn one_bottle() -> String {
    String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n")
}

fn two_bottles() -> String {
    String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n")
}

fn many_bottles(n: u32) -> String {
    format!(
        "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
        n,
        n,
        n - 1
    )
}

pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

更加灵活的实现

为了更好地处理复数形式和文本变化,我们可以创建辅助函数:

rust 复制代码
pub fn verse(n: u32) -> String {
    match n {
        0 => format!(
            "{} {} of beer on the wall, {} {} of beer.\n{}",
            bottles_count(n),
            bottles(n),
            bottles_count(n),
            bottles(n),
            "Go to the store and buy some more, 99 bottles of beer on the wall.\n"
        ),
        1 => format!(
            "{} {} of beer on the wall, {} {} of beer.\nTake it down and pass it around, {} {} of beer on the wall.\n",
            bottles_count(n),
            bottles(n),
            bottles_count(n),
            bottles(n),
            bottles_count(n - 1),
            bottles(n - 1)
        ),
        _ => format!(
            "{} {} of beer on the wall, {} {} of beer.\nTake one down and pass it around, {} {} of beer on the wall.\n",
            bottles_count(n),
            bottles(n),
            bottles_count(n),
            bottles(n),
            bottles_count(n - 1),
            bottles(n - 1)
        ),
    }
}

fn bottles_count(n: u32) -> String {
    match n {
        0 => String::from("no more"),
        _ => n.to_string(),
    }
}

fn bottles(n: u32) -> String {
    if n == 1 {
        String::from("bottle")
    } else {
        String::from("bottles")
    }
}

pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

使用宏的实现

我们还可以使用宏来简化重复的格式化代码:

rust 复制代码
macro_rules! verse_template {
    ($n:expr, $action:expr) => {
        format!(
            "{} {} of beer on the wall, {} {} of beer.\n{}",
            bottles_count($n),
            bottles($n),
            bottles_count($n),
            bottles($n),
            $action
        )
    };
}

pub fn verse(n: u32) -> String {
    match n {
        0 => verse_template!(
            n,
            "Go to the store and buy some more, 99 bottles of beer on the wall.\n"
        ),
        1 => verse_template!(
            n,
            format!(
                "Take it down and pass it around, {} {} of beer on the wall.\n",
                bottles_count(n - 1),
                bottles(n - 1)
            )
        ),
        _ => verse_template!(
            n,
            format!(
                "Take one down and pass it around, {} {} of beer on the wall.\n",
                bottles_count(n - 1),
                bottles(n - 1)
            )
        ),
    }
}

fn bottles_count(n: u32) -> String {
    match n {
        0 => String::from("no more"),
        _ => n.to_string(),
    }
}

fn bottles(n: u32) -> String {
    if n == 1 {
        String::from("bottle")
    } else {
        String::from("bottles")
    }
}

pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

性能优化版本

考虑到性能,我们可以预分配字符串容量:

rust 复制代码
pub fn verse(n: u32) -> String {
    let mut result = String::with_capacity(200); // 预估容量
    
    match n {
        0 => result.push_str("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"),
        1 => result.push_str("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
        2 => result.push_str("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
        _ => {
            use std::fmt::Write;
            write!(
                result,
                "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
                n,
                n,
                n - 1
            ).unwrap();
        }
    }
    
    result
}

pub fn sing(start: u32, end: u32) -> String {
    let capacity = (start - end + 1) as usize * 200; // 估算总容量
    let mut result = String::with_capacity(capacity);
    
    for i in (end..=start).rev() {
        result.push_str(&verse(i));
        if i > end {
            result.push('\n');
        }
    }
    
    result
}

错误处理和边界情况

在实际应用中,我们需要考虑输入验证:

rust 复制代码
#[derive(Debug, PartialEq)]
pub enum BeerSongError {
    InvalidRange,
}

pub fn verse(n: u32) -> String {
    match n {
        0 => String::from("No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall.\n"),
        1 => String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
        2 => String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
        _ => format!(
            "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
            n,
            n,
            n - 1
        ),
    }
}

pub fn sing(start: u32, end: u32) -> String {
    if start < end {
        // 可以选择返回错误或交换参数
        return sing(end, start);
    }
    
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}

实际应用场景

虽然啤酒歌看起来只是一个练习,但它在实际开发中有以下应用:

  1. 模板引擎:学习如何根据数据生成文本
  2. 代码生成:根据规则生成重复性代码
  3. 教育工具:教授条件逻辑和字符串处理
  4. 测试用例:生成标准化的测试数据

扩展功能

基于这个基础实现,我们可以添加更多功能:

rust 复制代码
pub struct BeerSong {
    max_bottles: u32,
}

impl BeerSong {
    pub fn new(max_bottles: u32) -> Self {
        BeerSong { max_bottles }
    }
    
    pub fn verse(&self, n: u32) -> String {
        match n {
            0 => format!(
                "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, {} bottles of beer on the wall.\n",
                self.max_bottles
            ),
            1 => String::from("1 bottle of beer on the wall, 1 bottle of beer.\nTake it down and pass it around, no more bottles of beer on the wall.\n"),
            2 => String::from("2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall.\n"),
            _ => format!(
                "{} bottles of beer on the wall, {} bottles of beer.\nTake one down and pass it around, {} bottles of beer on the wall.\n",
                n,
                n,
                n - 1
            ),
        }
    }
    
    pub fn sing(&self, start: u32, end: u32) -> String {
        if start < end {
            return String::new(); // 或者返回错误
        }
        
        (end..=start)
            .rev()
            .map(|n| self.verse(n))
            .collect::<Vec<String>>()
            .join("\n")
    }
}

总结

通过 beer-song 练习,我们学到了:

  1. 字符串格式化:掌握了 Rust 中多种字符串格式化方法
  2. 模式匹配:熟练使用 match 表达式处理不同情况
  3. 条件逻辑:学会了如何处理复杂的条件分支
  4. 函数组合:理解了如何将简单函数组合成复杂功能
  5. 性能优化:了解了字符串预分配等优化技巧
  6. 错误处理:学会了如何处理边界情况

这些技能在实际开发中非常有用,特别是在生成报告、构建模板系统、处理自然语言和进行文本操作时。啤酒歌虽然看起来简单,但它涉及到了字符串处理、条件逻辑和格式化输出等许多核心概念,是学习 Rust 文本处理的良好起点。

通过这个练习,我们也看到了 Rust 在处理字符串和文本格式化方面的强大能力,以及如何用清晰的代码表达复杂的逻辑。这种优雅的实现方式正是 Rust 语言的魅力所在。

相关推荐
迎仔18 小时前
02-网络硬件设备详解:从大喇叭到算力工厂的进化
网络·智能路由器
南极星100518 小时前
蓝桥杯JAVA--启蒙之路(十)class版本 模块
java·开发语言
baidu_2474386118 小时前
Android ViewModel定时任务
android·开发语言·javascript
嘿起屁儿整18 小时前
面试点(网络层面)
前端·网络
Dev7z18 小时前
基于 MATLAB 的铣削切削力建模与仿真
开发语言·matlab
不能隔夜的咖喱18 小时前
牛客网刷题(2)
java·开发语言·算法
serve the people19 小时前
python环境搭建 (十二) pydantic和pydantic-settings类型验证与解析
java·网络·python
小天源19 小时前
Error 1053 Error 1067 服务“启动后立即停止” Java / Python 程序无法后台运行 windows nssm注册器下载与报错处理
开发语言·windows·python·nssm·error 1053·error 1067
_运维那些事儿19 小时前
VM环境的CI/CD
linux·运维·网络·阿里云·ci/cd·docker·云计算
云小逸19 小时前
【nmap源码学习】 Nmap网络扫描工具深度解析:从基础参数到核心扫描逻辑
网络·数据库·学习