rust 程序,rust编程开发教程

软件开发 | Rust 基础系列 #8:编写里程碑 Rust 程序

到目前为止,我们已经讲解了包括 变量、可变性、常量、数据类型、函数、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::iostdin函数。如果我在程序的开头没有导入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 行)。这个数组包含了所有可以购买的水果的名称,以及字符串切片qquit,让用户可以传达他们是否希望退出。

用户可能不知道我们希望输入是什么样的。用户可能会输入“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 行,如果用户输入了一个无效的字符串,我将 continuemart循环。

对用户输入做出反应

现在,所有其他的状况都已经处理好了,是时候写一些代码来让用户从水果市场购买水果了,当用户希望退出时,程序也会退出。

因为你也知道用户选择了哪种水果,所以让我们问一下他们打算购买多少,并告诉他们输入数量的格式。

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!("nn总价是 {} 卢比。", 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相乘,而quantityf64。记住,Rust 没有隐式类型转换 😉

当水果名称和用户想要购买的数量被存下来之后,calc_price函数被调用来计算用户指定数量的水果的价格。这个函数接受水果名称和数量作为参数,并将价格作为f64返回。

当你看到 calc_price函数的内部时,你会发现它是许多人所说的包装函数。它被称为包装函数,因为它调用其他函数来完成它的脏活。

因为每种水果都有不同的最低订单数量,才能被认为是批发购买,为了确保代码在未来可以轻松维护,每种水果都有单独的函数负责计算价格。

所以,calc_price函数所做的就是确定用户选择了哪种水果,并调用相应的函数来计算所选水果的价格。这些水果特定的函数只接受一个参数:数量。这些水果特定的函数将价格作为f64返回。

现在,price_*函数只做一件事。它们检查订单数量是否大于被认为是批发购买的最低订单数量。如果是这样,quantity将会乘以水果的每千克批发价格。否则,quantity将会乘以水果的每千克零售价格。

由于乘法行末尾没有分号,所以函数返回乘积。

如果你仔细看看 calc_price函数中水果特定函数的函数调用,这些函数调用在末尾没有分号。这意味着,price_*函数返回的值将会被calc_price函数返回给它的调用者。

而且 calc_price函数只有一个调用者。这个调用者在mart循环的末尾,这个调用者使用这个函数返回的值来增加total的值。

最终,当 mart循环结束(当用户输入qquit时),存储在变量total中的值将会被打印到屏幕上,并且用户将会被告知他/她需要支付的价格。

总结

这篇文章中,我使用了之前讲解的 Rust 编程语言的所有主题来创建一个简单的程序,这个程序仍然在某种程度上展示了一个现实世界的问题。

现在,我写的代码肯定可以用一种更符合编程习惯的方式来写,这种方式最好地使用了 Rust 的喜爱特性,但是我还没有讲到它们!

Rust 基础系列到此结束。欢迎你的反馈。

(题图:MJ/6d486f23-e6fe-4bef-a28d-df067ef2ec06)

via: ***/milestone-rust-program/

本文由 LCTT原创编译,Linux中国荣誉推出

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:dandanxi6@qq.com

(0)
上一篇 2023年 8月 1日 下午12:12
下一篇 2023年 8月 1日 下午12:22

相关推荐

  • 分组发光字怎么设置

    很多朋友都不明白什么是分组发光字?要怎么样去理解分组发光字是怎么样做的?分组发光字难做吗? 橙子来给你讲解分组发光字有哪一些。 一般来说分组发光字是由两层或多层而形成的发光字。我们…

    2023年 7月 1日
  • 双十一淘宝满减活动怎么玩的

    双十一淘宝满减活动怎么玩 一年一度的双十一马上就要开始了,聚友们肯定早早就摩拳擦掌开始等待了,通常双十一的折扣力度是一年各种促销活动中最大的,那么今年淘宝双十一活动满200减多少?…

    2023年 4月 16日
  • 你的steam账号交易受限,你需要在steam商城消费5美元

    回声一分钟,游戏玩儿得明明白白。 大家好,我们是回声游戏。 视频加载中… 很多新的Steam玩家在准备与好兄弟一起联机玩游戏时,发现无法添加好友,提示Steam账户受限…

    2022年 12月 26日
  • 摄氏,华氏,开尔文,关于温度的小秘密,你知道多少

    最近天气是越来越热了,一看墙上挂的温度计显示为35摄氏度,这就需要打开空调降温,过一会儿后温度下降了,温度计显示温度为25摄氏度。 温度计隔离的特写 但是关于温度我们时常也有很多疑…

    2023年 7月 19日
  • 如何一元开通微博会员,新浪微博如何开通会员

    [闽南网] 新浪微博是一款很多伙伴在使用的娱乐实时资讯软件,可以观看到最新的社会动向,最近还有一元开通会员的活动,想要获取更多会员权益的伙伴,可以在这里看看一元会员开通方法,不要错…

    2023年 4月 5日
  • 淘宝商家再也看不到客户手机号了怎么办

    淘宝最近政策:今天起(9月1日)淘宝商家将无法看到消费者的手机号了。最近淘宝推出了一个全新的虚拟号码解决方案,将彻底切断商家与消费者之间的手机联系。 收件人真实手机号全程保密,此次…

    2023年 7月 6日
  • 今天520表白方法

    在当下的这个社会,如果你稍稍慢了一会,或许你就会错过很多,你可能错过的是你的女神,或是你错过了你的男神,如果不想错过彼此,那么就来看一下如何在520表白日表白吧。 1、直接用手机发…

    2022年 12月 20日
  • 铁路候补购票使用方法(铁路候补购票怎么开)

    每到逢年过节抢票的时候 大家各种舔着脸让不熟的朋友加速 早已心累多时 不过从今天起,就不用啦! 今天起,铁路12306网站(含手机客户端)在前期试点的基础上,将铁路候补购票服务扩大…

    2023年 9月 3日
  • qq音乐的歌曲怎么转换成mp3格式

    QQ音乐是目前市面上最流行的音乐播放器,涵盖了海量的音乐。有时候我们会担心长时间使用QQ音乐播放会消耗大量的流量,因此会将音乐下载至本地进行播放,或者传输至其他设备当中。但是QQ音…

    2023年 5月 8日
  • 母亲离家出走放弃孩子怎么办

    导语: 晚年之际,不被人重视是最大的悲哀。老年人们常感到被遗忘,犹如透明人,这种孤独令他们倍感苦闷。作为子女,关心老人的物质生活固然重要,但更应重视他们的心理需求。 66岁的杨大妈…

    2023年 9月 21日