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::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!("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
相乘,而 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 基礎系列到此結束。歡迎你的反饋。
(題圖:MJ/6d486f23-e6fe-4bef-a28d-df067ef2ec06)
via: https://itsfoss.com/milestone-rust-program/
作者:Pratham Patel 選題:lkxed 譯者:Cubik65536 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive