到目前为止,我们已经讲解了包括 变量、可变性、常量、数据类型、函数、if-else 语句 和 循环 在内的一些关于 Rust 编程的基础知识。
在 Rust 基础系列的最后一章里,让我们现在用 Rust 编写一个程序,使用这些主题,以便更好地理解它们在现实世界中的用途。让我们来编写一个相对简单的程序,用来从水果市场订购水果。
我们程序的基本结构
来让我们首先向用户问好,并告诉他们如何与程序交互。
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
}
获取用户输入
上面的代码非常简单。目前,你不知道接下来该做什么,因为你不知道用户接下来想做什么。
所以让我们添加一些代码,接受用户输入并将其存储在某个地方以便稍后解析,然后根据用户输入采取适当的操作。
use std::io;
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
// 获取用户输入
let mut user_input = String::new();
io::stdin()
.read_line(&mut user_input)
.expect("无法读取用户输入。");
}
有三个新元素需要告诉你。所以让我们对这些新元素进行浅层次的探索。
1. 理解 use 关键字
在这个程序的第一行,你可能已经注意到我们“使用”(哈哈!)了一个叫做 use
的新关键字。Rust 中的 use
关键字类似于 C/C++ 中的 #include
指令和 Python 中的 import
关键字。使用 use
关键字,我们从 Rust 标准库 std
中“导入”了 io
(输入输出)模块。
LCTT 译注:“使用”在原文中为“use”,与新介绍的关键字一样。
你可能会想知道为什么我们在可以使用 println
宏来将某些内容输出到标准输出时,导入 io
模块是必要的。Rust 的标准库有一个叫做 prelude
的模块,它会自动被包含。该模块包含了 Rust 程序员可能需要使用的所有常用函数,比如 println
宏。(你可以在 这里 阅读更多关于 std::prelude
模块的内容。)
Rust 标准库 std
中的 io
模块是接受用户输入所必需的。因此,我们在程序的第一行添加了一个 use
语句。
2. 理解 Rust 中的 String 类型
在第 11 行,我创建了一个新的可变变量 user_input
,正如它的名字所表示的那样,它将被用来存储用户输入。但是在同一行,你可能已经注意到了一些“新的”东西(哈哈,又来了!)。
LCTT 译注:“新的”在原文中为“new”,在第 11 行的代码中,原作者使用了
String::new()
函数,所以此处的梗与“使用”一样,原作者使用了一个在代码中用到的单词。
我没有使用双引号(""
)声明一个空字符串,而是使用 String::new()
函数来创建一个新的空字符串。
""
与 String::new()
的区别是你将在 Rust 系列的后续文章中学习到的。现在,只需要知道,使用 String::new()
函数,你可以创建一个可变的,位于堆上的字符串。
如果我使用 ""
创建了一个字符串,我将得到一个叫做“字符串切片”的东西。字符串切片的内容也位于堆上,但是字符串本身是不可变的。所以,即使变量本身是可变的,作为字符串存储的实际数据是不可变的,需要被覆盖而不是修改。
3. 接受用户输入
在第 12 行,我调用了 std::io
的 stdin()
函数。如果我在程序的开头没有导入 std::io
模块,那么这一行将是 std::io::stdin()
而不是 io::stdin()
。
sdtin()
函数返回一个终端的输入句柄。read_line()
函数抓住这个输入句柄,然后,正如它的名字所暗示的那样,读取一行输入。这个函数接受一个可变字符串的引用。所以,我传入了 user_input
变量,通过在它前面加上 &mut
,使它成为一个可变引用。
⚠️
read_line()
函数有一个 怪癖。这个函数在用户按下回车键之后 停止 读取输入。因此,这个函数也会记录换行符(\n
),并将一个换行符存储在你传入的可变字符串变量的结尾处。
所以,请在处理它时要么考虑到这个换行符,要么将它删除。
Rust 中的错误处理入门
最后,在这个链的末尾有一个 expect()
函数。让我们稍微偏题一下,来理解为什么要调用这个函数。
read_line()
函数返回一个叫做 Result
的枚举。我会在后面的文章中讲解 Rust 中的枚举,但是现在只需要知道,枚举在 Rust 中是非常强大的。这个 Result
枚举返回一个值,告诉程序员在读取用户输入时是否发生了错误。
expect()
函数接受这个 Result
枚举,并检查结果是否正常。如果没有发生错误,什么都不会发生。但是如果发生了错误,我传入的消息(无法读取用户输入。
)将会被打印到 STDERR,程序将会退出。
📋 所有我简要提及的新概念将会在后续的新 Rust 系列文章中讲解。
现在我希望你应该已经理解了这些新概念,让我们添加更多的代码来增加程序的功能。
验证用户输入
我接受了用户的输入,但是我没有对其进行验证。在当前的上下文中,验证意味着用户输入了一些“命令”,我们希望能够处理这些命令。目前,这些命令有两个“类别”。
第一类用户可以输入的命令是用户希望购买的水果的名称。第二个命令表示用户想要退出程序。
我们的任务现在是确保用户输入不会偏离 可接受的命令。
use std::io;
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
// 获取用户输入
let mut user_input = String::new();
io::stdin()
.read_line(&mut user_input)
.expect("无法读取用户输入。");
// 验证用户输入
let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
user_input = user_input.trim().to_lowercase();
let mut input_error = true;
for input in valid_inputs {
if input == user_input {
input_error = false;
break;
}
}
}
要使验证更容易,我创建了一个叫做 valid_inputs
的字符串切片数组(第 17 行)。这个数组包含了所有可以购买的水果的名称,以及字符串切片 q
和 quit
,让用户可以传达他们是否希望退出。
用户可能不知道我们希望输入是什么样的。用户可能会输入“Apple”、“apple”或 “APPLE” 来表示他们想要购买苹果。我们的工作是正确处理这些输入。
在第 18 行,我通过调用 trim()
函数从 user_input
字符串中删除了尾部的换行符。为了处理上面提到的问题,我使用 to_lowercase()
函数将所有字符转换为小写,这样 “Apple”、“apple” 和 “APPLE” 都会变成 “apple”。
现在,来看第 19 行,我创建了一个名为 input_error
的可变布尔变量,初始值为 true
。稍后在第 20 行,我创建了一个 for
循环,它遍历了 valid_inputs
数组的所有元素(字符串切片),并将迭代的模式存储在 input
变量中。
在循环内部,我检查用户输入是否等于其中一个有效字符串,如果是,我将 input_error
布尔值的值设置为 false
,并跳出 for
循环。
处理无效输入
现在是时候处理无效输入了。这可以通过将一些代码移动到无限循环中来完成,如果用户给出无效输入,则 继续 该无限循环。
use std::io;
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
'mart: loop {
let mut user_input = String::new();
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
// 读取用户输入
io::stdin()
.read_line(&mut user_input)
.expect("无法读取用户输入。");
user_input = user_input.trim().to_lowercase();
// 验证用户输入
let mut input_error = true;
for input in valid_inputs {
if input == user_input {
input_error = false;
break;
}
}
// 处理无效输入
if input_error {
println!("错误: 请输入有效的输入");
continue 'mart;
}
}
}
这里,我将一些代码移动到了循环内部,并重新组织了一下代码,以便更好地处理循环的引入。在循环内部,第 31 行,如果用户输入了一个无效的字符串,我将 continue
mart
循环。
对用户输入做出反应
现在,所有其他的状况都已经处理好了,是时候写一些代码来让用户从水果市场购买水果了,当用户希望退出时,程序也会退出。
因为你也知道用户选择了哪种水果,所以让我们问一下他们打算购买多少,并告诉他们输入数量的格式。
use std::io;
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
'mart: loop {
let mut user_input = String::new();
let mut quantity = String::new();
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
// 读取用户输入
io::stdin()
.read_line(&mut user_input)
.expect("无法读取用户输入。");
user_input = user_input.trim().to_lowercase();
// 验证用户输入
let mut input_error = true;
for input in valid_inputs {
if input == user_input {
input_error = false;
break;
}
}
// 处理无效输入
if input_error {
println!("错误: 请输入有效的输入");
continue 'mart;
}
// 如果用户想要退出,就退出
if user_input == "q" || user_input == "quit" {
break 'mart;
}
// 获取数量
println!(
"\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
user_input
);
io::stdin()
.read_line(&mut quantity)
.expect("无法读取用户输入。");
}
}
在第 11 行,我声明了另一个可变变量,它的值是一个空字符串,在第 48 行,我接受了用户的输入,但是这次是用户打算购买的水果的数量。
解析数量
我刚刚增加了一些代码,以已知的格式接受数量,但是这些数据被存储为字符串。我需要从中提取出浮点数。幸运的是,这可以通过 parse()
方法来完成。
就像 read_line()
方法一样,parse()
方法返回一个 Result
枚举。parse()
方法返回 Result
枚举的原因可以通过我们试图实现的内容来轻松理解。
我正在接受用户的字符串,并尝试将其转换为浮点数。浮点数有两个可能的值。一个是浮点数本身,另一个是小数。
字符串可以包含字母,但是浮点数不行。所以,如果用户输入的不是浮点数和小数,parse()
函数将会返回一个错误。
因此,这个错误也需要处理。我们将使用 expect()
函数来处理这个错误。
use std::io;
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
'mart: loop {
let mut user_input = String::new();
let mut quantity = String::new();
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
// 读取用户输入
io::stdin()
.read_line(&mut user_input)
.expect("无法读取用户输入。");
user_input = user_input.trim().to_lowercase();
// 验证用户输入
let mut input_error = true;
for input in valid_inputs {
if input == user_input {
input_error = false;
break;
}
}
// 处理无效输入
if input_error {
println!("错误: 请输入有效的输入");
continue 'mart;
}
// 如果用户想要退出,就退出
if user_input == "q" || user_input == "quit" {
break 'mart;
}
// 获取数量
println!(
"\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
user_input
);
io::stdin()
.read_line(&mut quantity)
.expect("无法读取用户输入。");
let quantity: f64 = quantity
.trim()
.parse()
.expect("请输入有效的数量。");
}
}
如你所见,我通过变量遮蔽将解析后的浮点数存储在变量 quantity
中。为了告诉 parse()
函数,我的意图是将字符串解析为 f64
,我手动将变量 quantity
的类型注释为 f64
。
现在,parse()
函数将会解析字符串并返回一个 f64
或者一个错误,expect()
函数将会处理这个错误。
计算价格 + 最后的修饰
现在我们知道了用户想要购买的水果及其数量,现在是时候进行计算了,并让用户知道结果/总价了。
为了真实起见,我将为每种水果设置两个价格。第一个价格是零售价,我们在购买少量水果时向水果供应商支付的价格。水果的第二个价格是当有人批量购买水果时支付的批发价。
批发价将会在订单数量大于被认为是批发购买的最低订单数量时确定。这个最低订单数量对于每种水果都是不同的。每种水果的价格都是每千克多少卢比。
想好了逻辑,下面是最终的程序。
use std::io;
const APPLE_RETAIL_PER_KG: f64 = 60.0;
const APPLE_WHOLESALE_PER_KG: f64 = 45.0;
const BANANA_RETAIL_PER_KG: f64 = 20.0;
const BANANA_WHOLESALE_PER_KG: f64 = 15.0;
const ORANGE_RETAIL_PER_KG: f64 = 100.0;
const ORANGE_WHOLESALE_PER_KG: f64 = 80.0;
const MANGO_RETAIL_PER_KG: f64 = 60.0;
const MANGO_WHOLESALE_PER_KG: f64 = 55.0;
const GRAPES_RETAIL_PER_KG: f64 = 120.0;
const GRAPES_WHOLESALE_PER_KG: f64 = 100.0;
fn main() {
println!("欢迎来到水果市场!");
println!("请选择要购买的水果。\n");
let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
'mart: loop {
let mut user_input = String::new();
let mut quantity = String::new();
println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
println!("购买完成后,请输入“quit”或“q”。\n");
// 读取用户输入
io::stdin()
.read_line(&mut user_input)
.expect("无法读取用户输入。");
user_input = user_input.trim().to_lowercase();
// 验证用户输入
let mut input_error = true;
for input in valid_inputs {
if input == user_input {
input_error = false;
break;
}
}
// 处理无效输入
if input_error {
println!("错误: 请输入有效的输入");
continue 'mart;
}
// 如果用户想要退出,就退出
if user_input == "q" || user_input == "quit" {
break 'mart;
}
// 获取数量
println!(
"\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
user_input
);
io::stdin()
.read_line(&mut quantity)
.expect("无法读取用户输入。");
let quantity: f64 = quantity
.trim()
.parse()
.expect("请输入有效的数量。");
total += calc_price(quantity, user_input);
}
println!("\n\n总价是 {} 卢比。", total);
}
fn calc_price(quantity: f64, fruit: String) -> f64 {
if fruit == "apple" {
price_apple(quantity)
} else if fruit == "banana" {
price_banana(quantity)
} else if fruit == "orange" {
price_orange(quantity)
} else if fruit == "mango" {
price_mango(quantity)
} else {
price_grapes(quantity)
}
}
fn price_apple(quantity: f64) -> f64 {
if quantity > 7.0 {
quantity * APPLE_WHOLESALE_PER_KG
} else {
quantity * APPLE_RETAIL_PER_KG
}
}
fn price_banana(quantity: f64) -> f64 {
if quantity > 4.0 {
quantity * BANANA_WHOLESALE_PER_KG
} else {
quantity * BANANA_RETAIL_PER_KG
}
}
fn price_orange(quantity: f64) -> f64 {
if quantity > 3.5 {
quantity * ORANGE_WHOLESALE_PER_KG
} else {
quantity * ORANGE_RETAIL_PER_KG
}
}
fn price_mango(quantity: f64) -> f64 {
if quantity > 5.0 {
quantity * MANGO_WHOLESALE_PER_KG
} else {
quantity * MANGO_RETAIL_PER_KG
}
}
fn price_grapes(quantity: f64) -> f64 {
if quantity > 2.0 {
quantity * GRAPES_WHOLESALE_PER_KG
} else {
quantity * GRAPES_RETAIL_PER_KG
}
}
对比之前的版本,我做了一些改动……
水果的价格可能会波动,但是在我们程序的生命周期内,这些价格不会波动。所以我将每种水果的零售价和批发价存储在常量中。我将这些常量定义在 main()
函数之外(即全局常量),因为我不会在 main()
函数内计算每种水果的价格。这些常量被声明为 f64
,因为它们将与 quantity
相乘,而 quantity
是 f64
。记住,Rust 没有隐式类型转换 😉
当水果名称和用户想要购买的数量被存下来之后,calc_price()
函数被调用来计算用户指定数量的水果的价格。这个函数接受水果名称和数量作为参数,并将价格作为 f64
返回。
当你看到 calc_price()
函数的内部时,你会发现它是许多人所说的包装函数。它被称为包装函数,因为它调用其他函数来完成它的脏活。
因为每种水果都有不同的最低订单数量,才能被认为是批发购买,为了确保代码在未来可以轻松维护,每种水果都有单独的函数负责计算价格。
所以,calc_price()
函数所做的就是确定用户选择了哪种水果,并调用相应的函数来计算所选水果的价格。这些水果特定的函数只接受一个参数:数量。这些水果特定的函数将价格作为 f64
返回。
现在,price_*()
函数只做一件事。它们检查订单数量是否大于被认为是批发购买的最低订单数量。如果是这样,quantity
将会乘以水果的每千克批发价格。否则,quantity
将会乘以水果的每千克零售价格。
由于乘法行末尾没有分号,所以函数返回乘积。
如果你仔细看看 calc_price()
函数中水果特定函数的函数调用,这些函数调用在末尾没有分号。这意味着,price_*()
函数返回的值将会被 calc_price()
函数返回给它的调用者。
而且 calc_price()
函数只有一个调用者。这个调用者在 mart
循环的末尾,这个调用者使用这个函数返回的值来增加 total
的值。
最终,当 mart
循环结束(当用户输入 q
或 quit
时),存储在变量 total
中的值将会被打印到屏幕上,并且用户将会被告知他/她需要支付的价格。
总结
这篇文章中,我使用了之前讲解的 Rust 编程语言的所有主题来创建一个简单的程序,这个程序仍然在某种程度上展示了一个现实世界的问题。
现在,我写的代码肯定可以用一种更符合编程习惯的方式来写,这种方式最好地使用了 Rust 的喜爱特性,但是我还没有讲到它们!
所以,敬请关注后续的 将 Rust 带入下一个层次 系列,并学习更多 Rust 编程语言的内容!
Rust 基础系列到此结束。欢迎你的反馈。