楼主: jxapp_44629
84 0

[其他] Rust 练习册 31:啤酒歌与字符串格式化艺术 [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

学前班

40%

还不是VIP/贵宾

-

威望
0
论坛币
2 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
20 点
帖子
1
精华
0
在线时间
0 小时
注册时间
2018-2-12
最后登录
2018-3-27

楼主
jxapp_44629 发表于 2025-11-21 22:04:42 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币
在编程学习的过程中,我们常常会碰到一些表面简单但背后逻辑较为复杂的题目。“Beer Song”(啤酒歌)便是一个典型的例子。这个练习源于一首传统的倒数歌曲,要求根据不同的数字输出对应的歌词内容。在 Exercism 平台的 “beer-song” 练习中,我们需要用 Rust 实现这首倒数歌,借此掌握字符串格式化、模式匹配以及条件控制等核心编程技巧。

啤酒歌的基本规则

这首歌遵循一种固定的歌词结构,依据当前剩余的啤酒瓶数量进行变化: 当 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.
当 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.
当 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.
这些规则决定了每一段歌词的具体表达方式,尤其是名词单复数和代词的变化。

函数接口说明

练习中给出了两个需要实现的函数:
verse
该函数用于生成指定节数的单段歌词。
sing
此函数负责生成从起始值到结束值之间的全部歌词(包含边界),即整首歌曲的一部分或完整版本。

核心算法设计与实现

1. 单节歌词的构建

实现的核心是 verse 函数,其功能是根据传入的数字生成对应的一节歌词。以下是具体实现:
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
        ),
    }
}
该实现通过 Rust 的模式匹配(match)处理不同情况: - 数字为 0 时,表示没有啤酒了,需描述去商店购买更多; - 数字为 1 时,使用单数形式 “bottle” 和代词 “it”; - 数字为 2 时,虽然仍为复数,但下一句要变为 “1 bottle”,需要注意语法一致性; - 其他情况则统一采用通用模板处理。
verse

2. 完整歌曲的拼接

接下来是 sing 函数的实现,用于生成从 startend 的连续歌词段落:
pub fn sing(start: u32, end: u32) -> String {
    (end..=start)
        .rev()
        .map(verse)
        .collect::<Vec<String>>()
        .join("\n")
}
这一部分采用了函数式编程的思想: - 使用范围操作符 end..=start 创建闭区间; - 调用 .rev() 将顺序反转,实现从高到低的倒数; - 对每个数值应用 verse 函数生成对应歌词; - 最后将所有字符串收集并以换行符连接成完整文本。
sing

测试用例解析

为了验证实现是否正确,可以通过分析测试用例来深入理解需求细节。 #[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 节的特殊逻辑——当没有啤酒时,应提示重新购买,并回到 99 瓶的状态。 #[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"); } 此用例关注单数形式的正确使用,包括 “bottle” 和 “Take it down”。 #[test] fn test_verse_2() { (后续测试省略,结构保持不变) 通过对这些测试的理解,可以确保代码满足各种边界条件和语言表达上的准确性。

测试用例验证第2节歌词的下一句应使用单数形式“1 bottle”:

#[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");
}

完整歌曲生成的测试,注意各节之间以空行分隔:

#[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"
    );
}
{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.

实现优化方案

为了提高代码可读性和执行效率,可以将 verse 函数重构为模式匹配结构:

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
    )
}
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.

灵活扩展的实现方式

为进一步增强对复数形式和语句变化的支持,可通过引入独立的辅助函数来动态生成文本内容:

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),

该方法通过分离数量词与名词形式,使语言规则更易于维护和本地化。整体结构保持不变,但具备更强的适应性。

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

在实现“99瓶啤酒”这首经典歌曲的代码时,可以通过多种方式来完成。其中一种方法是利用宏来减少重复的格式化逻辑,使代码更加简洁和易于维护。

首先,定义两个辅助函数:bottles_count 和 bottles。前者用于将数字转换为对应的字符串表示(例如,0 转换为 "no more"),后者根据数量决定使用单数 "bottle" 还是复数 "bottles"。

接下来,通过 match 表达式处理不同情况下的歌词生成:

  • 当剩余瓶数为 0 时,输出提示信息:“Go to the store and buy some more, 99 bottles of beer on the wall.”
  • 当只剩 1 瓶时,使用 “Take it down and pass it around” 的句式,并计算下一句中的瓶子数量。
  • 对于其他情况,则统一使用 “Take one down and pass it around” 的表达方式。
{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.

为了提升代码可读性,可以引入宏 verse_template 来封装通用的字符串格式化结构。该宏接收当前瓶数 $n 和对应的动作描述 $action,然后自动生成前半部分歌词并拼接后续内容。

使用宏后,主函数 verse 可以更清晰地组织各种条件分支,而无需重复书写相同的 format! 调用。这不仅减少了冗余代码,也提高了后期修改的便利性。

此外,在性能敏感的场景中,还可以对字符串构建过程进行优化。例如,在生成每段歌词时预先分配足够的内存空间(如设置初始容量为 200 字节),从而避免多次动态扩容带来的开销。

最终,sing 函数负责从起始值到结束值逆序调用 verse,并将所有结果用换行符连接成完整歌词。整个流程高效且结构清晰,适用于不同规模的输出需求。

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.

在实际开发中,虽然“啤酒歌”常被视为一个编程练习题,但其背后所体现的逻辑结构和文本生成机制具有广泛的应用价值。通过对不同数量酒瓶状态的处理,可以深入理解条件判断、字符串拼接以及迭代操作等核心编程概念。

以下是关于该程序的核心实现方式:

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
        ),
    }
}

对于连续多段歌词的生成,可以通过 sing 函数实现从起始值到结束值的逆序遍历:

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")
}
{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.

为了更高效地管理内存分配,也可以预先估算最终字符串的容量:

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');
    }
}
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.

在真实应用场景中,还需考虑输入的有效性验证。例如,定义一个错误类型来处理非法范围:

#[derive(Debug, PartialEq)]
pub enum BeerSongError {
    InvalidRange,
}

尽管参数顺序颠倒时可以选择自动纠正而非报错,但在某些严格场景下应返回明确的错误信息以确保调用者知晓问题所在。

实际应用方向

  • 模板引擎:根据动态数据生成格式化文本,是构建邮件模板、HTML渲染等系统的基础。
  • 代码生成:利用规则批量生成重复结构的代码,提升开发效率。
  • 教学示例:帮助初学者掌握循环、分支与字符串操作的基本语法。
  • 测试数据构造:快速生成标准化、结构化的测试用例内容。

功能扩展建议

基于当前基础,可引入配置化结构体以支持更多自定义行为:

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 编程练习的过程中,我们深入理解了 Rust 中多个关键编程概念。该练习虽然以经典的“啤酒歌”为背景,但其背后涵盖的技术点非常实用且具有代表性。

字符串格式化处理

掌握了 Rust 提供的多种字符串构建方式,特别是 format! 宏的应用,使得动态生成结构化文本变得高效而清晰。对于不同数量的酒瓶状态,能够灵活插入变量并保持输出一致性。

模式匹配机制

通过 match 表达式对不同的瓶子数量(如 0、1、2 和其余情况)进行精确区分,体现了 Rust 强大的模式匹配能力。这种语法不仅安全,还能覆盖所有可能分支,避免逻辑遗漏。

条件与边界控制

sing 方法中,加入了对起始值小于结束值的判断,防止无效范围迭代。这展示了如何在函数层面处理非法输入,提升程序健壮性。

函数的组合与复用

verse 作为基础单元,通过 mapcollect 组合成整首歌曲输出,体现了函数式编程的思想——小功能模块化,再通过链式调用组装成复杂行为。

性能考量与优化意识

虽然本例未显式预分配内存,但通过使用 join 连接字符串向量的方式,相比多次拼接更高效,间接反映了在文本处理中对性能的关注。

错误与边界情况处理

当传入的起始 verse 编号小于终止编号时,返回空字符串或可扩展为返回 Result 类型,说明已考虑到异常流程,是良好工程实践的一部分。

学习价值与实际应用

尽管 beer-song 看似只是一个简单的文本输出任务,但它融合了字符串操作、逻辑分支、函数抽象等多个核心编程技能。这些技术广泛应用于报告生成、模板引擎开发、自然语言处理和日志格式化等真实场景中。

Rust 在此练习中展现出的表达力和安全性,让我们看到其在文本处理领域的潜力。清晰的语法结构和强大的类型系统,使代码既易于维护又不易出错,充分体现了语言设计的优雅与实用性。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:格式化 字符串 练习册 Capacity Template

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
jg-xs1
拉您进交流群
GMT+8, 2025-12-5 17:02