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 语言的魅力所在。

相关推荐
辰辰观影11 分钟前
字幕导入教程:外挂字幕一键加载方法
网络·经验分享
无糖可乐没有灵魂14 分钟前
Golang ❀ Netdiag 一个Go开源的网络排障信息收集工具
网络
努力努力再努力wz15 分钟前
【Lua 入门系列】Lua 函数深入理解:函数对象、多返回值、可变参数、闭包、递归与尾调用优化
开发语言·lua
飞浪纪元[FWC–FE]1 小时前
从零到一键绿:Qt 5.14.2 + VS2019 开发环境搭建、Kit 套件配置与构建错误修复完全手册
开发语言·qt
Wang's Blog1 小时前
Java 项目实战:交友项目 - 联系人列表
java·开发语言·交友
我命由我123451 小时前
访客系统形式(有用户概念、无用户概念)
java·开发语言·前端·javascript·python·产品运营·产品经理
架构师伯子明1 小时前
FFmpeg C++学习(一):编译和demo
开发语言·c++·学习
海域云SeaArea_1 小时前
Windows Server 2008 R2 从零安装 IIS7.5(二):php.ini 配置
开发语言·windows·php
叼烟扛炮1 小时前
[LangChain] 学习笔记(02):LCEL 链式调用与动态模型配置实战
服务器·开发语言·网络·笔记·学习·langchain
xixingzhe21 小时前
图表中同比与环比的区别:看趋势还是看变化?
开发语言·python