系列文章目錄
【跟小嘉學 Rust 編程】一、Rust 編程基礎
【跟小嘉學 Rust 編程】二、Rust 包管理工具使用
【跟小嘉學 Rust 編程】三、Rust 的基本程序概念
【跟小嘉學 Rust 編程】四、理解 Rust 的所有權概念
【跟小嘉學 Rust 編程】五、使用結構體關聯結構化數據
【跟小嘉學 Rust 編程】六、枚舉和模式匹配
【跟小嘉學 Rust 編程】七、使用包(Packages)、單元包(Crates)和模塊(Module)來管理項目
【跟小嘉學 Rust 編程】八、常見的集合
【跟小嘉學 Rust 編程】九、錯誤處理(Error Handling)
【跟小嘉學 Rust 編程】十一、編寫自動化測試
【跟小嘉學 Rust 編程】十二、構建一個命令行程序
文章目錄
- 系列文章目錄
- @[TOC](文章目錄)
- 前言
- 一、如何接受命令行參數
- 1.1、創建項目
- 1.2、需求介紹
- 1.3、讀取參數值
- 1.4、將參數值保存進變量
- 二、讀取文件(使用 fs 模塊讀取文件)
- 三、模塊化與錯誤處理
- 3.1、代碼中存在的問題
- 3.2、二進制項目的關注分離
- 3.3、提取參數解析器
- 3.4、使用結構來組織配置變量
- 3.5、創建 Config 的構造函數
- 3.6、修復錯誤處理
- 3.6.1、改善錯誤提示
- 3.6.2、業務邏輯處理:run方法
- 3.6.3、run 函數返回 Result 錯誤
- 3.7、將代碼拆分
- 3.7.1、lib.rs
- 3.7.2、main.rs
- 四、測試驅動開發(TDD)
- 4.1、search 方法編寫和測試
- 4.2、在 run 函數中使用 search 函數
- 五、處理環境變量
- 六、標準輸出和標準錯誤
- 6.1、標準輸出:stdout
- 6.2、標準錯誤:stderr
- 總結
文章目錄
- 系列文章目錄
- @[TOC](文章目錄)
- 前言
- 一、如何接受命令行參數
- 1.1、創建項目
- 1.2、需求介紹
- 1.3、讀取參數值
- 1.4、將參數值保存進變量
- 二、讀取文件(使用 fs 模塊讀取文件)
- 三、模塊化與錯誤處理
- 3.1、代碼中存在的問題
- 3.2、二進制項目的關注分離
- 3.3、提取參數解析器
- 3.4、使用結構來組織配置變量
- 3.5、創建 Config 的構造函數
- 3.6、修復錯誤處理
- 3.6.1、改善錯誤提示
- 3.6.2、業務邏輯處理:run方法
- 3.6.3、run 函數返回 Result 錯誤
- 3.7、將代碼拆分
- 3.7.1、lib.rs
- 3.7.2、main.rs
- 四、測試驅動開發(TDD)
- 4.1、search 方法編寫和測試
- 4.2、在 run 函數中使用 search 函數
- 五、處理環境變量
- 六、標準輸出和標準錯誤
- 6.1、標準輸出:stdout
- 6.2、標準錯誤:stderr
- 總結
前言
本章是一個目前所學的很多技能的應用,以及標準庫的探索,我們講構建一個命令行程序工具來練習現在已經學習過的一些Rust的技能。我們將構建自己的版本的命令行工具:grep(Globally search a Regular Expression and print)。
主要教材參考 《The Rust Programming Language》
一、如何接受命令行參數
1.1、創建項目
$ cargo new minigrepCreated binary (application) `minigrep` project
$ cd minigrep
1.2、需求介紹
minigrep 能夠接受兩個命令行參數:文件名和要搜索的字符串。也就是說我們希望使用cargo run的時候,可以使用如下的方式。
cargo run searchstring example-filename.txt
在 Crates.io 上會有一些現場的庫幫助我們接受命令行參數(clap)。不過我們現階段使用標準庫。
1.3、讀取參數值
為了能夠接受命令行參數的值,我們需要使用 rust 標準庫提供的函數。該函數返回一個命令行參數的迭代器(iiterator),迭代器我們將會在下一章詳細講解。我們只需要知道在迭代器上有一個方法 collect 可以將其轉換為一個集合。
use std::env;fn main() {let args: Vec<String> = env::args().collect();println!("{:?}", args);
}
需要注意 args 函數 在其任何參數包含 無效Unicode 字符時會panic。 如果你需要接受包含無效Unicode字符的參數,使用 std::env::args_os
代替。該函數返回 OsString值而不是 String 值。
Vector 的第一個參數是二進制文件的名稱。
1.4、將參數值保存進變量
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)
方法打開文件,返回包含內容的Result<String>
。
三、模塊化與錯誤處理
我們上述代碼 main 函數有著多個職責,通常函數只負責一個功能會更加簡潔并且易于維護。在開發的時候重構是一個最佳時間,重構少量代碼要容易的多。
3.1、代碼中存在的問題
我們最初的代碼存在下面四個問題:
- 1、 main 現在進行了兩個功能:解析參數并且打開文件。但是當函數承擔了更多責任,會更加難易推導,難以測試,并且難以在不破壞其他部分的情況下做出修改。
- 2、query 和 flename 是程序中過的配置i變了,而 contents 則用來執行程序邏輯。當變量越來越多的時候便會難以追蹤分析每個變量的目的,最好能夠講配置變量組織進一個結構。這樣就能夠使他們的目的更加明確;
- 3、如果打開文件失敗 ,我們使用 expect 來打印錯誤信息,不過這種錯誤信息并不明確,讀取文件失敗的原因有很多種:例如文件不存在,或者沒有打開文件的權限等,無論那種情況,這并沒有給予使用者具體的信息
- 4、我們不停的使用 expect 來處理不同的錯誤,如果用戶沒有指定足夠的的參數來運行程序,他們會從 rust 中得到 一個
index out of bounds
錯誤,而這并不能明確解釋問題。如果所有的錯誤處理都位于一處,這樣將來的維護者需要在修改錯誤處理邏輯時只需要考慮這一處代碼。
3.2、二進制項目的關注分離
main 函數負責多個任務的組織問題在許多二進制項目中很常見。所以 Rust 社區開發出一類在 main 函數開始變得龐大時進行二進制程序的關注分離的指導性過程。這些過程有如下步驟:
- 將程序拆分成 main.rs 和 lib.rs 并將程序的邏輯放入 lib.rs 中。
- 當命令行解析邏輯比較小時,可以保留在 main.rs 中。
- 當命令行解析開始變得復雜時,也同樣將其從 main.rs 提取到 lib.rs 中。
經過這些過程之后保留在 main 函數中的責任應該被限制為:
- 使用參數值調用命令行解析邏輯
- 設置任何其他的配置
- 調用 lib.rs 中的 run 函數
- 如果 run 返回錯誤,則處理這個錯誤
這個模式的一切就是為了關注分離:main.rs 處理程序運行,而 lib.rs 處理所有的真正的任務邏輯。因為不能直接測試 main 函數,這個結構通過將所有的程序邏輯移動到 lib.rs 的函數中使得我們可以測試他們。僅僅保留在 main.rs 中的代碼將足夠小以便閱讀就可以驗證其正確性。讓我們遵循這些步驟來重構程序
3.3、提取參數解析器
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、使用結構來組織配置變量
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 包含擁有所有權的String值,我們返回來引用 args 中的 String值的字符串切片 slice。 main函數的args變量是參數值的所有者并只允許 parse_config 方法借用他們。這意味著 Config 嘗試獲取args 中的值的所有權將違反 Rust的借用規則。
還有許多不同的方式可以處理 String 的數據,而最簡單但有些不太高效的方式是調用這些值的 clone 方法。這會生成 Config 實例可以擁有的數據的完整拷貝,不過會比儲存字符串數據的引用消耗更多的時間和內存。不過拷貝數據使得代碼顯得更加直白因為無需管理引用的生命周期,所以在這種情況下犧牲一小部分性能來換取簡潔性的取舍是值得的。
由于其運行時消耗,許多 Rustacean 之間有一個趨勢是傾向于避免使用 clone 來解決所有權問題。
在關于迭代器的章節中,我們將學習如何更加有效率的處理這種情況,不過現在復制字符串取得進展是沒有問題的。因為只會進行一次這樣的拷貝,而且文件名和要搜索的字符串都比較短。
3.5、創建 Config 的構造函數
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、修復錯誤處理
3.6.1、改善錯誤提示
對于錯誤,我們可以使用 panic!,但是 panic!更趨向于程序上的問題,而不是使用上的問題,我們應該使用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、業務邏輯處理: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 函數返回 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);}
}
四、測試驅動開發(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 函數中使用 search 函數
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(())
}
五、處理環境變量
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!() 宏就是把輸出信息輸出到標準錯誤
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);}
}
總結
以上就是今天要講的內容
- 主要講解了一個項目的編寫過程