在计算机科学中,表达式求值是一个经典问题。今天我们将通过实现一个逆波兰表达式(Reverse Polish Notation,RPN)计算器来深入学习 Rust 的枚举类型、模式匹配和栈数据结构的使用。

什么是逆波兰表达式?

逆波兰表达式是一种后缀表达式,操作符位于操作数之后。例如,我们熟悉的中缀表达式 “3 + 4” 在逆波兰表示法中写作 “3 4 +”。这种表示法的优势在于:

  1. 不需要括号来指定运算优先级
  2. 可以通过简单的栈操作进行求值

让我们通过一些例子来理解:

  • 中缀:“2 + 3” → 后缀:“2 3 +”
  • 中缀:“(4 + 8) / (7 - 5)” → 后缀:“4 8 + 7 5 - /”

核心数据结构

#[derive(Debug)]
pub enum CalculatorInput {
    Add,
    Subtract,
    Multiply,
    Divide,
    Value(i32),
}

我们使用 Rust 的枚举类型来表示计算器的输入。枚举让我们可以清晰地表达有限的几种可能性,这比使用整数常量或字符串要安全得多。

完整实现

#[derive(Debug)]
pub enum CalculatorInput {
    Add,
    Subtract,
    Multiply,
    Divide,
    Value(i32),
}

pub fn evaluate(inputs: &[CalculatorInput]) -> Option<i32> {
    let mut stack: Vec<i32> = Vec::new();
    
    for input in inputs {
        match input {
            CalculatorInput::Value(value) => stack.push(*value),
            _ => {
                if stack.len() < 2 {
                    return None;
                }
                
                let right = stack.pop().unwrap();
                let left = stack.pop().unwrap();
                
                let result = match input {
                    CalculatorInput::Add => left + right,
                    CalculatorInput::Subtract => left - right,
                    CalculatorInput::Multiply => left * right,
                    CalculatorInput::Divide => left / right,
                    CalculatorInput::Value(_) => unreachable!(),
                };
                
                stack.push(result);
            }
        }
    }
    
    if stack.len() == 1 {
        Some(stack[0])
    } else {
        None
    }
}

代码深度解析

枚举与模式匹配

#[derive(Debug)]
pub enum CalculatorInput {
    Add,
    Subtract,
    Multiply,
    Divide,
    Value(i32),
}

枚举是 Rust 中表示多种可能值的强大工具。每个变体可以携带不同类型的数据,如 [Value(i32)](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/rpn-calculator/src/lib.rs#L7-L7) 携带一个整数值。

栈操作

let mut stack: Vec<i32> = Vec::new();

for input in inputs {
    match input {
        CalculatorInput::Value(value) => stack.push(*value),
        // ...
    }
}

我们使用 Vec<i32> 作为栈来存储操作数。当遇到数值时,将其压入栈中。

嵌套模式匹配

match input {
    CalculatorInput::Value(value) => stack.push(*value),
    _ => {
        // ...
        let result = match input {
            CalculatorInput::Add => left + right,
            CalculatorInput::Subtract => left - right,
            CalculatorInput::Multiply => left * right,
            CalculatorInput::Divide => left / right,
            CalculatorInput::Value(_) => unreachable!(),
        };
        // ...
    }
}

我们使用了两层模式匹配:

  1. 第一层区分是数值还是操作符
  2. 第二层确定具体的操作符类型

错误处理

if stack.len() < 2 {
    return None;
}

在执行操作前,我们检查栈中是否有足够的操作数。如果没有,立即返回 None 表示计算失败。

unreachable! 宏

CalculatorInput::Value(_) => unreachable!(),

由于外层已经处理了 [Value](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/rpn-calculator/src/lib.rs#L7-L7) 情况,内层的 [Value](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/rpn-calculator/src/lib.rs#L7-L7) 分支永远不会被执行。使用 [unreachable!()](file:///Users/zacksleo/projects/github/zacksleo/exercism-rust/exercises/concept/short-fibonacci/src/lib.rs#L13-L13) 宏可以明确表达这一点。

测试用例详解

use rpn_calculator::*;

fn calculator_input(s: &str) -> Vec<CalculatorInput> {
    s.split_whitespace()
        .map(|s| match s {
            "+" => CalculatorInput::Add,
            "-" => CalculatorInput::Subtract,
            "*" => CalculatorInput::Multiply,
            "/" => CalculatorInput::Divide,
            n => CalculatorInput::Value(n.parse().unwrap()),
        })
        .collect()
}

#[test]
fn test_empty_input_returns_none() {
    let input = calculator_input("");
    assert_eq!(evaluate(&input), None);
}

#[test]
fn test_simple_value() {
    let input = calculator_input("10");
    assert_eq!(evaluate(&input), Some(10));
}

#[test]
fn test_simple_addition() {
    let input = calculator_input("2 2 +");
    assert_eq!(evaluate(&input), Some(4));
}

#[test]
fn test_simple_subtraction() {
    let input = calculator_input("7 11 -");
    assert_eq!(evaluate(&input), Some(-4));
}

#[test]
fn test_simple_multiplication() {
    let input = calculator_input("6 9 *");
    assert_eq!(evaluate(&input), Some(54));
}

#[test]
fn test_simple_division() {
    let input = calculator_input("57 19 /");
    assert_eq!(evaluate(&input), Some(3));
}

#[test]
fn test_complex_operation() {
    let input = calculator_input("4 8 + 7 5 - /");
    assert_eq!(evaluate(&input), Some(6));
}

#[test]
fn test_too_few_operands_returns_none() {
    let input = calculator_input("2 +");
    assert_eq!(evaluate(&input), None);
}

#[test]
fn test_too_many_operands_returns_none() {
    let input = calculator_input("2 2");
    assert_eq!(evaluate(&input), None);
}

这些测试用例覆盖了各种情况:

  • 空输入
  • 简单数值
  • 各种基本运算
  • 复杂表达式
  • 错误情况处理

Rust 特性的体现

1. 强大的枚举类型

pub enum CalculatorInput {
    Add,
    Subtract,
    Multiply,
    Divide,
    Value(i32),
}

Rust 的枚举比其他语言中的枚举更加强大,可以携带数据,这使得它们成为表达复杂数据结构的理想选择。

2. 模式匹配的安全性

match input {
    CalculatorInput::Value(value) => stack.push(*value),
    _ => { /* 处理操作符 */ }
}

编译器会确保我们处理了所有可能的枚举变体,避免了运行时错误。

3. Vec 作为栈的使用

let mut stack: Vec<i32> = Vec::new();
stack.push(value);          // 入栈
let value = stack.pop();    // 出栈

标准库中的 Vec 提供了完美的栈操作支持。

4. Option 类型的错误处理

pub fn evaluate(inputs: &[CalculatorInput]) -> Option<i32>

使用 Option 类型明确表示函数可能失败,调用者必须显式处理这种情况。

算法工作原理

RPN 计算器的工作原理如下:

  1. 从左到右遍历输入
  2. 遇到数值时,压入栈中
  3. 遇到操作符时:
    • 从栈中弹出两个操作数(注意顺序)
    • 执行运算
    • 将结果压回栈中
  4. 最终栈中应该只剩一个值,即结果

例如,计算 “4 8 + 7 5 - /”:

  1. 4 入栈:[4]
  2. 8 入栈:[4, 8]
    • 运算:4+8=12,12 入栈:[12]
  3. 7 入栈:[12, 7]
  4. 5 入栈:[12, 7, 5]
    • 运算:7-5=2,2 入栈:[12, 2]
  5. / 运算:12/2=6,6 入栈:[6]
  6. 结果:6

实际应用场景

RPN 计算器在许多领域都有应用:

  1. 编程语言解释器:许多语言的虚拟机使用栈来执行指令
  2. 计算器应用:HP 计算器就以 RPN 著称
  3. 表达式求值:编译器将中缀表达式转换为后缀表达式进行求值
  4. 图形计算器:PostScript 语言使用 RPN
  5. 嵌入式系统:Forth 语言完全基于栈和 RPN

扩展思考

这个实现可以进一步扩展:

impl CalculatorInput {
    pub fn is_operator(&self) -> bool {
        match self {
            CalculatorInput::Value(_) => false,
            _ => true,
        }
    }
}

pub fn evaluate_with_error(inputs: &[CalculatorInput]) -> Result<i32, String> {
    // 返回详细的错误信息
}

总结

通过这个 RPN 计算器练习,我们学习了 Rust 中几个关键概念:

  • 枚举类型用于表示有限的几种可能性
  • 模式匹配提供了一种安全且表达力强的方式来处理不同情况
  • Vec 可以作为栈使用
  • Option 类型用于优雅地处理可能失败的操作
  • 嵌套模式匹配处理复杂逻辑

这些特性共同构成了 Rust 强大而安全的表达能力,使我们能够编写清晰、安全且高效的代码。

在下一篇文章中,我们将继续探索 Rust 的更多强大功能!

Logo

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

更多推荐