Rust简单计算器:模式匹配与表达式求值
引言
在编程的世界里,计算器程序堪称初学者的“练手神器”。而对于Rust这门以安全性和性能著称的编程语言来说,打造一个简单计算器,不仅能让我们熟悉语法,更是深入理解其独特特性的绝佳契机。本文就带大家从零开始,一步步构建一个基于Rust的简单计算器。我们将重点聚焦在模式匹配与表达式求值上,这两项技术堪称Rust编程的“看家本领”,学好了,你在Rust的世界里就能横着走。
I.模式匹配的奥秘
模式匹配是什么
模式匹配是Rust里处理数据的一种强大方法,它允许我们根据不同的模式来执行不同的代码。这就好比给数据做“人脸识别”,一旦匹配成功,就触发相应的操作。
为什么模式匹配很重要
在编程里,我们经常会遇到需要根据不同情况做不同处理的场景。比如说,判断一个数的正负、处理用户输入的不同类型等等。模式匹配能让这些操作变得既清晰又安全,还能有效避免遗漏情况。
Rust里的模式匹配
Rust提供了两种主要的模式匹配机制:match
表达式和if let
/while let
语句。
match表达式
match
表达式是最常用的模式匹配方式。它的基本语法如下:
match 变量 {
模式1 => 表达式1,
模式2 => 表达式2,
// 更多模式...
_ => 表达式n, // 通配符,用于捕获所有未匹配的模式
}
举个栗子🌰:我们来判断一个整数的正负。
fn main() {
let num = -5;
match num {
n if n > 0 => println!("{} 是正数", n),
0 => println!("零"),
_ => println!("负数"),
}
}
在这段代码里,我们用match num
开始模式匹配。对于正数的情况,我们用n if n > 0
这个模式,既匹配了正数,还能在打印时用到具体的数值。对于0,直接写0
这个模式,打印“零”。最后用通配符_
来匹配负数情况,输出“负数”。
if let 和 while let
有时候,我们只想匹配一种特定的模式,其他情况都不关心。这时候if let
和while let
就派上用场啦。
let optional_value = Some(42);
if let Some(value) = optional_value {
println!("有值:{}", value);
} else {
println!("无值");
}
在这段代码里,if let Some(value) = optional_value
表示只关心optional_value
是Some
的情况,并把里面的值提取出来赋给value
变量。如果optional_value
是None
,就走else
分支。
mermaid 总结
II.表达式求值的艺术
表达式求值是啥
表达式求值,简单来说,就是按照一定的规则计算出一个表达式的值。比如计算3 + 5 * 2
,就得先算乘法再算加法。
Rust里的表达式求值
Rust里的表达式求值遵循常见的运算符优先级规则。比如乘法、除法的优先级高于加法、减法。但有时候,我们可能需要自定义复杂的表达式求值逻辑,这就需要用模式匹配来处理不同的运算符。
分析表达式结构
一个典型的算术表达式由操作数和运算符组成。比如3 + 5
,3和5是操作数,+是运算符。我们可以把表达式抽象成不同的类型,比如加法表达式、减法表达式等等。
mermaid 总结
Lexical error on line 4. Unrecognized text. ...符优先级] C --> D[示例:3 + 5 * 2] A -- ----------------------^III.动手实现简单计算器
项目搭建
创建新项目
打开终端,输入以下命令创建一个名为calculator
的新项目:
cargo new calculator
Cargo会自动创建一个项目目录结构,包括Cargo.toml
文件和src
目录下的main.rs
文件。
项目结构说明
项目目录结构如下:
calculator
├── Cargo.toml
└── src
└── main.rs
Cargo.toml
文件用于配置项目信息和依赖项。main.rs
是程序的入口文件。
设计表达式数据结构
为了表示不同的算术表达式,我们定义一个枚举类型Expression
:
enum Expression {
Number(i32),
Addition(Box<Expression>, Box<Expression>),
Subtraction(Box<Expression>, Box<Expression>),
Multiplication(Box<Expression>, Box<Expression>),
Division(Box<Expression>, Box<Expression>),
}
这个枚举类型有五种变体:
Number
表示一个单独的数字。Addition
表示加法表达式,包含两个子表达式,用Box
进行堆分配,以便于处理递归结构。Subtraction
表示减法表达式。Multiplication
表示乘法表达式。Division
表示除法表达式。
实现表达式求值函数
接下来,我们编写一个函数来计算Expression
的值:
fn evaluate(expr: &Expression) -> f64 {
match expr {
Expression::Number(n) => *n as f64,
Expression::Addition(a, b) => evaluate(a) + evaluate(b),
Expression::Subtraction(a, b) => evaluate(a) - evaluate(b),
Expression::Multiplication(a, b) => evaluate(a) * evaluate(b),
Expression::Division(a, b) => evaluate(a) / evaluate(b),
}
}
这个函数使用match
表达式对expr
进行模式匹配。对于数字,直接返回其值;对于复合表达式,递归计算子表达式的值,然后根据运算符执行相应的操作。
构建表达式示例
在main
函数中,我们构建一个表达式并计算其值:
fn main() {
let expr = Expression::Addition(
Box::new(Expression::Number(3)),
Box::new(Expression::Multiplication(
Box::new(Expression::Number(5)),
Box::new(Expression::Number(2)),
)),
);
let result = evaluate(&expr);
println!("表达式:3 + 5 * 2");
println!("结果:{}", result);
}
这段代码构建了一个表达式3 + 5 * 2
,然后调用evaluate
函数计算结果,并打印出来。
构建与运行
在项目根目录下,执行以下命令构建项目:
cargo build
构建成功后,运行生成的可执行文件(Windows系统上为.\target\debug\calculator.exe
,macOS和Linux系统上为./target/debug/calculator
)。程序会输出:
表达式:3 + 5 * 2
结果:13
mermaid 总结
IV.增强计算器功能:支持用户输入
从用户获取输入
为了使计算器更具实用性,我们需要支持用户输入表达式。在Rust里,可以用std::io
模块来读取用户输入。
use std::io;
fn get_user_input() -> String {
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("读取输入失败");
input.trim().to_string()
}
这个函数首先创建一个空的String
对象input
。然后调用io::stdin().read_line(&mut input)
读取用户输入,并将其存储在input
中。trim()
方法用于去除输入两端的空白字符,to_string()
将其转换为String
类型。
解析用户输入
用户输入的表达式需要解析成我们的Expression
枚举类型,这样才能用evaluate
函数计算。这里我们简单地根据输入的运算符来创建相应的表达式。
fn parse_expression(input: &str) -> Option<Expression> {
if let Some(expr) = parse_addition_or_subtraction(input) {
Some(expr)
} else if let Some(expr) = parse_multiplication_or_division(input) {
Some(expr)
} else if let Some(n) = input.parse::<i32>().ok() {
Some(Expression::Number(n))
} else {
None
}
}
fn parse_addition_or_subtraction(input: &str) -> Option<Expression> {
for (i, c) in input.chars().enumerate() {
if c == '+' || c == '-' {
let left = &input[0..i];
let right = &input[i + 1..];
if let (Some(left_expr), Some(right_expr)) = (parse_expression(left), parse_expression(right)) {
return Some(if c == '+' {
Expression::Addition(Box::new(left_expr), Box::new(right_expr))
} else {
Expression::Subtraction(Box::new(left_expr), Box::new(right_expr))
});
}
}
}
None
}
fn parse_multiplication_or_division(input: &str) -> Option<Expression> {
for (i, c) in input.chars().enumerate() {
if c == '*' || c == '/' {
let left = &input[0..i];
let right = &input[i + 1..];
if let (Some(left_expr), Some(right_expr)) = (parse_expression(left), parse_expression(right)) {
return Some(if c == '*' {
Expression::Multiplication(Box::new(left_expr), Box::new(right_expr))
} else {
Expression::Division(Box::new(left_expr), Box::new(right_expr))
});
}
}
}
None
}
parse_expression
函数尝试解析加减法、乘除法以及单独数字的情况。parse_addition_or_subtraction
和parse_multiplication_or_division
函数分别处理加减法和乘除法的解析。它们遍历输入字符串,查找运算符的位置,然后递归解析左右两边的表达式。
更新主函数支持用户交互
修改main
函数,使其支持用户输入并计算结果:
fn main() {
loop {
println!("请输入表达式(例如:3 + 5 * 2),输入 q 退出:");
let input = get_user_input();
if input == "q" {
break;
}
match parse_expression(&input) {
Some(expr) => {
let result = evaluate(&expr);
println!("结果:{}", result);
}
None => println!("无法解析的表达式"),
}
}
}
这个循环不断提示用户输入表达式。如果用户输入“q”,程序退出。否则,调用parse_expression
函数解析输入,若解析成功则计算并打印结果,否则提示无法解析。
测试用户输入功能
重新构建并运行项目。在控制台输入不同的表达式进行测试,例如:
请输入表达式(例如:3 + 5 * 2),输入 q 退出:
10 + 20
结果:30
请输入表达式(例如:3 + 5 * 2),输入 q 退出:
100 / 10 - 5
结果:5
请输入表达式(例如:3 + 5 * 2),输入 q 退出:
q
mermaid 总结
V.异常处理与错误管理
为什么需要异常处理
在实际编程中,错误是无法避免的。比如用户输入的表达式格式不正确、除数为零等情况。为了使我们的计算器健壮可靠,必须妥善处理这些异常情况。
Rust里的错误处理机制
Rust主要通过Result
和Option
类型来处理错误。Result
类型有两种变体:Ok
表示操作成功,Err
表示操作失败。Option
类型有Some
和None
两种变体,用于表示可能存在或不存在的值。
改进解析函数返回 Result 类型
修改parse_expression
函数,使其返回Result<Expression, String>
类型,以便传递错误信息:
fn parse_expression(input: &str) -> Result<Expression, String> {
if input.is_empty() {
return Err("空表达式".to_string());
}
if let Some(expr) = parse_addition_or_subtraction(input) {
Ok(expr)
} else if let Some(expr) = parse_multiplication_or_division(input) {
Ok(expr)
} else if let Some(n) = input.parse::<i32>().ok() {
Ok(Expression::Number(n))
} else {
Err("无法解析的表达式".to_string())
}
}
现在,如果解析成功,返回Ok(expr)
;如果失败,返回Err
携带错误信息。
在主函数中处理错误
更新main
函数,处理解析过程中可能出现的错误:
fn main() {
loop {
println!("请输入表达式(例如:3 + 5 * 2),输入 q 退出:");
let input = get_user_input();
if input == "q" {
break;
}
match parse_expression(&input) {
Ok(expr) => {
let result = evaluate(&expr);
println!("结果:{}", result);
}
Err(e) => println!("错误:{}", e),
}
}
}
现在,当解析失败时,程序会打印具体的错误信息,而不是简单地显示“无法解析的表达式”。
处理除零错误
进一步改进evaluate
函数,处理除法中的除零错误:
fn evaluate(expr: &Expression) -> Result<f64, String> {
match expr {
Expression::Number(n) => Ok(*n as f64),
Expression::Addition(a, b) => {
let a_val = evaluate(a)?;
let b_val = evaluate(b)?;
Ok(a_val + b_val)
}
Expression::Subtraction(a, b) => {
let a_val = evaluate(a)?;
let b_val = evaluate(b)?;
Ok(a_val - b_val)
}
Expression::Multiplication(a, b) => {
let a_val = evaluate(a)?;
let b_val = evaluate(b)?;
Ok(a_val * b_val)
}
Expression::Division(a, b) => {
let a_val = evaluate(a)?;
let b_val = evaluate(b)?;
if b_val == 0.0 {
Err("除数不能为零".to_string())
} else {
Ok(a_val / b_val)
}
}
}
}
在这个改进版中,evaluate
函数也返回Result<f64, String>
类型。对于除法操作,先计算分母的值b_val
,如果b_val
为零,返回Err
错误信息。否则,正常执行除法操作并返回结果。
同时,更新main
函数中的表达式计算部分:
match parse_expression(&input) {
Ok(expr) => match evaluate(&expr) {
Ok(result) => println!("结果:{}", result),
Err(e) => println!("错误:{}", e),
},
Err(e) => println!("错误:{}", e),
}
现在,程序能正确处理除零错误并给出提示。
测试错误处理功能
重新构建并运行项目。尝试输入以下表达式进行测试:
请输入表达式(例如:3 + 5 * 2),输入 q 退出:
5 / 0
错误:除数不能为零
请输入表达式(例如:3 + 5 * 2),输入 q 退出:
abc
错误:无法解析的表达式
请输入表达式(例如:3 + 5 * 2),输入 q 退出:
q
mermaid 总结
VI.性能优化与代码改进
性能优化的必要性
随着计算器功能的增强,处理复杂的表达式可能会消耗较多计算资源。为了提升用户体验,性能优化变得至关重要。
使用更高效的数据结构
目前我们用递归的方式构建和解析表达式,这在处理复杂表达式时可能会比较低效。我们可以考虑使用更高效的数据结构,比如直接用元组来表示表达式。
type Expression = (Operator, Box<Expression>, Box<Expression>);
enum Operator {
Add,
Subtract,
Multiply,
Divide,
Number(i32),
}
不过,这种改变需要对现有代码进行较大调整,在实际项目中,我们需要权衡性能提升与代码重构成本。
代码改进:减少内存分配
在当前实现中,我们大量使用Box
进行堆分配,这虽然方便处理递归结构,但也增加了内存分配的开销。可以通过减少不必要的堆分配来优化性能。例如,对于简单的数字表达式,可以将其直接存储在栈上,而不是用Box
包装。
代码改进:提升用户输入解析效率
优化parse_expression
函数,减少不必要的字符串操作和递归调用。可以预先对输入字符串进行一次扫描,确定运算符优先级,然后按照优先级进行解析。
mermaid 总结
- 点赞
- 收藏
- 关注作者
评论(0)