系列文章目錄
【跟小嘉學(xué) Rust 編程】一、Rust 編程基礎(chǔ)
【跟小嘉學(xué) Rust 編程】二、Rust 包管理工具使用
【跟小嘉學(xué) Rust 編程】三、Rust 的基本程序概念
【跟小嘉學(xué) Rust 編程】四、理解 Rust 的所有權(quán)概念
【跟小嘉學(xué) Rust 編程】五、使用結(jié)構(gòu)體關(guān)聯(lián)結(jié)構(gòu)化數(shù)據(jù)
【跟小嘉學(xué) Rust 編程】六、枚舉和模式匹配
【跟小嘉學(xué) Rust 編程】七、使用包(Packages)、單元包(Crates)和模塊(Module)來管理項目
【跟小嘉學(xué) Rust 編程】八、常見的集合
【跟小嘉學(xué) Rust 編程】九、錯誤處理(Error Handling)
【跟小嘉學(xué) Rust 編程】十一、編寫自動化測試
【跟小嘉學(xué) Rust 編程】十二、構(gòu)建一個命令行程序
前言
本章是一個目前所學(xué)的很多技能的應(yīng)用,以及標準庫的探索,我們講構(gòu)建一個命令行程序工具來練習(xí)現(xiàn)在已經(jīng)學(xué)習(xí)過的一些Rust的技能。我們將構(gòu)建自己的版本的命令行工具:grep(Globally search a Regular Expression and print)。
主要教材參考 《The Rust Programming Language》
一、如何接受命令行參數(shù)
1.1、創(chuàng)建項目
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
1.2、需求介紹
minigrep 能夠接受兩個命令行參數(shù):文件名和要搜索的字符串。也就是說我們希望使用cargo run的時候,可以使用如下的方式。
cargo run searchstring example-filename.txt
在 Crates.io 上會有一些現(xiàn)場的庫幫助我們接受命令行參數(shù)(clap)。不過我們現(xiàn)階段使用標準庫。
1.3、讀取參數(shù)值
為了能夠接受命令行參數(shù)的值,我們需要使用 rust 標準庫提供的函數(shù)。該函數(shù)返回一個命令行參數(shù)的迭代器(iiterator),迭代器我們將會在下一章詳細講解。我們只需要知道在迭代器上有一個方法 collect 可以將其轉(zhuǎn)換為一個集合。
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("{:?}", args);
}
需要注意 args 函數(shù) 在其任何參數(shù)包含 無效Unicode 字符時會panic。 如果你需要接受包含無效Unicode字符的參數(shù),使用 std::env::args_os
代替。該函數(shù)返回 OsString值而不是 String 值。
Vector 的第一個參數(shù)是二進制文件的名稱。
1.4、將參數(shù)值保存進變量
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("Searching for {}", query);
println!("In file {}", filename);
}
二、讀取文件(使用 fs 模塊讀取文件)
use std::{env, fs};
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("Searching for {}", query);
println!("In file {}", filename);
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
fs::read_to_string(filename)
方法打開文件,返回包含內(nèi)容的Result<String>
。
三、模塊化與錯誤處理
我們上述代碼 main 函數(shù)有著多個職責,通常函數(shù)只負責一個功能會更加簡潔并且易于維護。在開發(fā)的時候重構(gòu)是一個最佳時間,重構(gòu)少量代碼要容易的多。
3.1、代碼中存在的問題
我們最初的代碼存在下面四個問題:
- 1、 main 現(xiàn)在進行了兩個功能:解析參數(shù)并且打開文件。但是當函數(shù)承擔了更多責任,會更加難易推導(dǎo),難以測試,并且難以在不破壞其他部分的情況下做出修改。
- 2、query 和 flename 是程序中過的配置i變了,而 contents 則用來執(zhí)行程序邏輯。當變量越來越多的時候便會難以追蹤分析每個變量的目的,最好能夠講配置變量組織進一個結(jié)構(gòu)。這樣就能夠使他們的目的更加明確;
- 3、如果打開文件失敗 ,我們使用 expect 來打印錯誤信息,不過這種錯誤信息并不明確,讀取文件失敗的原因有很多種:例如文件不存在,或者沒有打開文件的權(quán)限等,無論那種情況,這并沒有給予使用者具體的信息
- 4、我們不停的使用 expect 來處理不同的錯誤,如果用戶沒有指定足夠的的參數(shù)來運行程序,他們會從 rust 中得到 一個
index out of bounds
錯誤,而這并不能明確解釋問題。如果所有的錯誤處理都位于一處,這樣將來的維護者需要在修改錯誤處理邏輯時只需要考慮這一處代碼。
3.2、二進制項目的關(guān)注分離
main 函數(shù)負責多個任務(wù)的組織問題在許多二進制項目中很常見。所以 Rust 社區(qū)開發(fā)出一類在 main 函數(shù)開始變得龐大時進行二進制程序的關(guān)注分離的指導(dǎo)性過程。這些過程有如下步驟:
- 將程序拆分成 main.rs 和 lib.rs 并將程序的邏輯放入 lib.rs 中。
- 當命令行解析邏輯比較小時,可以保留在 main.rs 中。
- 當命令行解析開始變得復(fù)雜時,也同樣將其從 main.rs 提取到 lib.rs 中。
經(jīng)過這些過程之后保留在 main 函數(shù)中的責任應(yīng)該被限制為:
- 使用參數(shù)值調(diào)用命令行解析邏輯
- 設(shè)置任何其他的配置
- 調(diào)用 lib.rs 中的 run 函數(shù)
- 如果 run 返回錯誤,則處理這個錯誤
這個模式的一切就是為了關(guān)注分離:main.rs 處理程序運行,而 lib.rs 處理所有的真正的任務(wù)邏輯。因為不能直接測試 main 函數(shù),這個結(jié)構(gòu)通過將所有的程序邏輯移動到 lib.rs 的函數(shù)中使得我們可以測試他們。僅僅保留在 main.rs 中的代碼將足夠小以便閱讀就可以驗證其正確性。讓我們遵循這些步驟來重構(gòu)程序
3.3、提取參數(shù)解析器
use std::{env, fs};
fn main() {
let args: Vec<String> = env::args().collect();
let (query,filename) = parse_config(&args);
println!("Searching for {}", query);
println!("In file {}", filename);
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
3.4、使用結(jié)構(gòu)來組織配置變量
use std::{env, fs};
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
struct Config {
query: String,
filename: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config { query, filename }
}
我們需要注意 我們定義的 Config 包含擁有所有權(quán)的String值,我們返回來引用 args 中的 String值的字符串切片 slice。 main函數(shù)的args變量是參數(shù)值的所有者并只允許 parse_config 方法借用他們。這意味著 Config 嘗試獲取args 中的值的所有權(quán)將違反 Rust的借用規(guī)則。
還有許多不同的方式可以處理 String 的數(shù)據(jù),而最簡單但有些不太高效的方式是調(diào)用這些值的 clone 方法。這會生成 Config 實例可以擁有的數(shù)據(jù)的完整拷貝,不過會比儲存字符串數(shù)據(jù)的引用消耗更多的時間和內(nèi)存。不過拷貝數(shù)據(jù)使得代碼顯得更加直白因為無需管理引用的生命周期,所以在這種情況下犧牲一小部分性能來換取簡潔性的取舍是值得的。
由于其運行時消耗,許多 Rustacean 之間有一個趨勢是傾向于避免使用 clone 來解決所有權(quán)問題。
在關(guān)于迭代器的章節(jié)中,我們將學(xué)習(xí)如何更加有效率的處理這種情況,不過現(xiàn)在復(fù)制字符串取得進展是沒有問題的。因為只會進行一次這樣的拷貝,而且文件名和要搜索的字符串都比較短。
3.5、創(chuàng)建 Config 的構(gòu)造函數(shù)
use std::{env, fs};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String])-> Config{
let query = args[1].clone();
let filename = args[2].clone();
Config { query, filename }
}
}
3.6、修復(fù)錯誤處理
3.6.1、改善錯誤提示
對于錯誤,我們可以使用 panic!,但是 panic!更趨向于程序上的問題,而不是使用上的問題,我們應(yīng)該使用Result 枚舉來處理錯誤。
use std::{env, fs, process};
const ARGS_LENGTH:usize= 3;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}", err);
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String])-> Result<Config, &'static str>{
if args.len() < ARGS_LENGTH {
return Err("not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Ok( Config { query, filename })
}
}
3.6.2、業(yè)務(wù)邏輯處理:run方法
use std::{env, fs, process};
const ARGS_LENGTH:usize= 3;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}", err);
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String])-> Result<Config, &'static str>{
if args.len() < ARGS_LENGTH {
return Err("not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Ok( Config { query, filename })
}
}
fn run(config: Config) {
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents);
}
3.6.3、run 函數(shù)返回 Result 錯誤
use std::{env, fs, process, error::Error};
const ARGS_LENGTH:usize= 3;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}", err);
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
println!(" Application error: {}", e);
process::exit(1);
}
}
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String])-> Result<Config, &'static str>{
if args.len() < ARGS_LENGTH {
return Err("not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Ok( Config { query, filename })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
3.7、將代碼拆分
3.7.1、lib.rs
use std::{fs, error::Error};
const ARGS_LENGTH:usize= 3;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String])-> Result<Config, &'static str>{
if args.len() < ARGS_LENGTH {
return Err("not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Ok( Config { query, filename })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
3.7.2、main.rs
use std::{env, process};
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}", err);
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = minigrep::run(config) {
println!(" Application error: {}", e);
process::exit(1);
}
}
四、測試驅(qū)動開發(fā)(TDD)
4.1、search 方法編寫和測試
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut result = Vec::new();
for line in contents.lines(){
if line.contains(query) {
result.push(line);
}
}
println!("{:?}", result);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick, three.
Duct tape.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
}
4.2、在 run 函數(shù)中使用 search 函數(shù)
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
五、處理環(huán)境變量
use std::{fs, error::Error, env};
const ARGS_LENGTH:usize= 3;
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
impl Config {
pub fn new(args: &[String])-> Result<Config, &'static str>{
if args.len() < ARGS_LENGTH {
return Err("not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok( Config { query, filename , case_sensitive})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents, config.case_sensitive) {
println!("{}", line);
}
Ok(())
}
pub fn search<'a>(query: &str, contents: &'a str, case_sensitive: bool) -> Vec<&'a str> {
let mut result = Vec::new();
if case_sensitive {
let query_ignore_sensitive = query.to_lowercase();
for line in contents.lines(){
if line.to_lowercase().contains(&query_ignore_sensitive) {
result.push(line);
}
}
return result;
} else {
for line in contents.lines(){
if line.contains(&query) {
result.push(line);
}
}
return result;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick, three.
Duct tape.";
assert_eq!(
vec!["safe, fast, productive."],
search(query, contents)
);
}
}
六、標準輸出和標準錯誤
6.1、標準輸出:stdout
println!() 宏就是把輸出信息輸出到標準輸出
6.2、標準錯誤:stderr
eprintln!() 宏就是把輸出信息輸出到標準錯誤文章來源:http://www.zghlxwxcb.cn/news/detail-644257.html
use std::{env, process};
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
eprintln!("Problem parsing arguments:{}", err);
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
println!("case_sensitive: {}", config.case_sensitive);
if let Err(e) = minigrep::run(config) {
eprintln!(" Application error: {}", e);
process::exit(1);
}
}
總結(jié)
以上就是今天要講的內(nèi)容文章來源地址http://www.zghlxwxcb.cn/news/detail-644257.html
- 主要講解了一個項目的編寫過程
到了這里,關(guān)于【跟小嘉學(xué) Rust 編程】十二、構(gòu)建一個命令行程序的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!