添加Rust示例项目ex02,实现安全字符处理器命令行工具,展示所有权和生命周期特性

This commit is contained in:
程广 2025-07-02 18:04:00 +08:00
parent d02e229239
commit 52a1aa7bdf
7 changed files with 1075 additions and 0 deletions

380
examples/ex02/Cargo.lock generated Normal file
View File

@ -0,0 +1,380 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "clap"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "ex02"
version = "0.1.0"
dependencies = [
"anyhow",
"atty",
"clap",
"once_cell",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

14
examples/ex02/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "ex02"
version = "0.1.0"
edition = "2021"
description = "安全字符处理器命令行工具展示Rust的所有权和生命周期特性"
[dependencies]
clap = { version = "4.4", features = ["derive"] } # 命令行参数解析
anyhow = "1.0" # 错误处理
thiserror = "1.0" # 自定义错误类型
once_cell = "1.18" # 用于单例模式实现
serde = { version = "1.0", features = ["derive"] } # 序列化/反序列化
serde_json = "1.0" # JSON处理
atty = "0.2" # 检测终端类型

111
examples/ex02/src/config.rs Normal file
View File

@ -0,0 +1,111 @@
use std::collections::HashMap;
use std::sync::{OnceLock, Mutex};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
settings: HashMap<String, String>,
}
impl Config {
// 获取配置实例
pub fn get_instance() -> &'static Mutex<Config> {
static INSTANCE: OnceLock<Mutex<Config>> = OnceLock::new();
INSTANCE.get_or_init(|| Mutex::new(Config::default()))
}
// 从文件加载配置
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<()> {
let config_str = fs::read_to_string(path)?;
let new_config: Config = serde_json::from_str(&config_str)?;
let instance = Self::get_instance().lock().unwrap();
let mut instance = instance;
*instance = new_config;
Ok(())
}
// 获取配置项
pub fn get_setting(&self, key: &str) -> Option<&String> {
self.settings.get(key)
}
// 设置配置项
pub fn set_setting(&mut self, key: String, value: String) {
self.settings.insert(key, value);
}
// 保存配置到文件
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let config_str = serde_json::to_string_pretty(self)?;
fs::write(path, config_str)?;
Ok(())
}
}
// 默认配置
impl Default for Config {
fn default() -> Self {
let mut settings = HashMap::new();
settings.insert("case_sensitive".to_string(), "true".to_string());
settings.insert("max_word_length".to_string(), "100".to_string());
settings.insert("min_word_length".to_string(), "1".to_string());
Config { settings }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_singleton_pattern() {
let instance1 = Config::get_instance();
let instance2 = Config::get_instance();
// 验证两个实例是同一个
assert_eq!(
format!("{:p}", instance1),
format!("{:p}", instance2)
);
}
#[test]
fn test_config_operations() {
let instance = Config::get_instance();
let mut config = instance.lock().unwrap();
// 测试设置和获取配置
config.set_setting("test_key".to_string(), "test_value".to_string());
assert_eq!(
config.get_setting("test_key"),
Some(&"test_value".to_string())
);
}
#[test]
fn test_file_operations() -> Result<()> {
// 创建临时文件
let mut temp_file = NamedTempFile::new()?;
let test_config = r#"{"settings":{"test_key":"test_value"}}"#;
write!(temp_file, "{}", test_config)?;
// 测试加载配置
Config::load_from_file(temp_file.path())?;
let config = Config::get_instance().lock().unwrap();
assert_eq!(
config.get_setting("test_key"),
Some(&"test_value".to_string())
);
Ok(())
}
}

160
examples/ex02/src/main.rs Normal file
View File

@ -0,0 +1,160 @@
mod config;
mod processor;
mod utils;
use clap::{Parser, ValueEnum};
use anyhow::{Result, Context};
use std::path::PathBuf;
use std::process;
use crate::config::Config;
use crate::processor::{Operation, TextProcessor};
use crate::utils::{read_input, InputSource, safe_print};
/// 安全字符处理器 - 展示Rust所有权和生命周期特性的命令行工具
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// 要处理的文本(如果不提供,将从标准输入或文件读取)
#[arg(index = 1)]
text: Option<String>,
/// 处理操作类型
#[arg(short, long, value_enum, default_value = "analyze")]
operation: OperationType,
/// 输入文件路径
#[arg(short, long)]
file: Option<PathBuf>,
/// 配置文件路径
#[arg(short, long)]
config: Option<PathBuf>,
/// 是否显示详细输出
#[arg(short, long)]
verbose: bool,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
enum OperationType {
/// 转换为大写
Upper,
/// 转换为小写
Lower,
/// 移除重复字符
Dedup,
/// 统计单词
Count,
/// 分析文本
Analyze,
}
impl From<OperationType> for Operation {
fn from(op_type: OperationType) -> Self {
match op_type {
OperationType::Upper => Operation::ToUpper,
OperationType::Lower => Operation::ToLower,
OperationType::Dedup => Operation::RemoveDuplicates,
OperationType::Count => Operation::CountWords,
OperationType::Analyze => Operation::Analyze,
}
}
}
fn main() -> Result<()> {
// 解析命令行参数
let args = Args::parse();
// 如果提供了配置文件,加载配置
if let Some(config_path) = &args.config {
Config::load_from_file(config_path)
.with_context(|| format!("加载配置文件失败: {:?}", config_path))?;
if args.verbose {
println!("已加载配置文件: {:?}", config_path);
}
}
// 确定输入源
let input_source = match (&args.text, &args.file) {
(Some(text), _) => InputSource::Text(text),
(None, Some(file)) => InputSource::File(file.to_string_lossy().to_string()),
(None, None) => {
if atty::is(atty::Stream::Stdin) {
eprintln!("错误: 未提供输入。请提供文本参数、输入文件或通过管道传入内容。");
eprintln!("使用 --help 查看帮助信息。");
process::exit(1);
}
InputSource::StdIn
}
};
// 读取输入
let input = read_input(input_source)
.context("读取输入失败")?;
if args.verbose {
println!("输入长度: {} 字符", input.len());
}
// 处理文本
let processor = TextProcessor;
let operation: Operation = args.operation.into();
if args.verbose {
println!("执行操作: {:?}", operation);
}
// 使用生命周期标注来确保引用安全
let result = processor.process_text(&input, operation)
.context("处理文本失败")?;
// 输出结果
safe_print(&result);
Ok(())
}
// 演示生命周期和所有权的示例函数
// 这些函数不会被直接调用但展示了Rust的所有权系统
#[allow(dead_code)]
fn ownership_examples() {
// 示例1: 移动语义
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1); // 编译错误: s1的所有权已移动到s2
// 示例2: 借用
let s3 = String::from("world");
let len = calculate_length(&s3);
println!("'{}' 的长度是 {}", s3, len); // s3仍然有效
// 示例3: 可变借用
let mut s4 = String::from("hello");
change(&mut s4);
println!("修改后: {}", s4);
// 示例4: 生命周期
let string1 = String::from("长字符串");
{
let string2 = String::from("");
let result = longest(string1.as_str(), string2.as_str());
println!("最长的字符串是: {}", result);
}
}
// 借用示例
fn calculate_length(s: &String) -> usize {
s.len()
}
// 可变借用示例
fn change(s: &mut String) {
s.push_str(", world");
}
// 生命周期标注示例
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

View File

@ -0,0 +1,228 @@
use std::collections::{HashMap, HashSet};
use anyhow::Result;
use thiserror::Error;
use crate::config::Config;
#[derive(Debug, Error)]
pub enum ProcessorError {
#[error("无效的输入文本")]
InvalidInput,
#[error("处理操作失败: {0}")]
ProcessingFailed(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Operation {
ToUpper,
ToLower,
RemoveDuplicates,
CountWords,
Analyze,
}
impl std::str::FromStr for Operation {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"upper" => Ok(Operation::ToUpper),
"lower" => Ok(Operation::ToLower),
"dedup" => Ok(Operation::RemoveDuplicates),
"count" => Ok(Operation::CountWords),
"analyze" => Ok(Operation::Analyze),
_ => Err(format!("未知的操作类型: {}", s)),
}
}
}
pub struct TextProcessor;
impl TextProcessor {
// 处理文本的主函数,根据操作类型调用不同的处理函数
pub fn process_text<'a>(&self, input: &'a str, operation: Operation) -> Result<String> {
if input.is_empty() {
return Err(ProcessorError::InvalidInput.into());
}
match operation {
Operation::ToUpper => Ok(self.to_upper(input)),
Operation::ToLower => Ok(self.to_lower(input)),
Operation::RemoveDuplicates => Ok(self.remove_duplicates(input)),
Operation::CountWords => Ok(self.count_words(input)),
Operation::Analyze => Ok(self.analyze_text(input)),
}
}
// 转换为大写
pub fn to_upper(&self, text: &str) -> String {
text.to_uppercase()
}
// 转换为小写
pub fn to_lower(&self, text: &str) -> String {
text.to_lowercase()
}
// 移除重复字符(保持顺序)
pub fn remove_duplicates(&self, text: &str) -> String {
let mut seen = HashSet::new();
let mut result = String::with_capacity(text.len());
for c in text.chars() {
if seen.insert(c) {
result.push(c);
}
}
result
}
// 统计单词数量
pub fn count_words(&self, text: &str) -> String {
let config = Config::get_instance().lock().unwrap();
let case_sensitive = config.get_setting("case_sensitive")
.map(|s| s == "true")
.unwrap_or(true);
let min_length = config.get_setting("min_word_length")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1);
let max_length = config.get_setting("max_word_length")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(100);
let mut word_counts = HashMap::new();
// 分割文本为单词并计数
for word in text.split_whitespace() {
let word = word.trim_matches(|c: char| !c.is_alphanumeric());
if word.len() < min_length || word.len() > max_length {
continue;
}
let key = if case_sensitive {
word.to_string()
} else {
word.to_lowercase()
};
*word_counts.entry(key).or_insert(0) += 1;
}
// 格式化输出结果
let mut result = String::new();
result.push_str("单词统计结果:\n");
let mut words: Vec<(&String, &i32)> = word_counts.iter().collect();
words.sort_by(|a, b| b.1.cmp(a.1));
for (word, count) in words {
result.push_str(&format!("{}: {}\n", word, count));
}
result
}
// 分析文本(字符频率、平均单词长度等)
pub fn analyze_text(&self, text: &str) -> String {
let char_count = text.chars().count();
let byte_count = text.len();
let line_count = text.lines().count();
let word_count = text.split_whitespace().count();
// 字符频率分析
let mut char_freq = HashMap::new();
for c in text.chars() {
*char_freq.entry(c).or_insert(0) += 1;
}
// 计算平均单词长度
let avg_word_length = if word_count > 0 {
let total_word_length: usize = text.split_whitespace()
.map(|w| w.chars().count())
.sum();
total_word_length as f64 / word_count as f64
} else {
0.0
};
// 格式化输出结果
let mut result = String::new();
result.push_str("文本分析结果:\n");
result.push_str(&format!("字符数: {}\n", char_count));
result.push_str(&format!("字节数: {}\n", byte_count));
result.push_str(&format!("行数: {}\n", line_count));
result.push_str(&format!("单词数: {}\n", word_count));
result.push_str(&format!("平均单词长度: {:.2}\n", avg_word_length));
result.push_str("\n最常见的字符:\n");
let mut chars: Vec<(&char, &i32)> = char_freq.iter().collect();
chars.sort_by(|a, b| b.1.cmp(a.1));
for (i, (c, count)) in chars.iter().take(10).enumerate() {
let display_char = if c.is_whitespace() {
match **c {
' ' => "空格".to_string(),
'\n' => "换行".to_string(),
'\t' => "制表符".to_string(),
_ => format!("Unicode({})", **c as u32),
}
} else {
c.to_string()
};
result.push_str(&format!("{}. '{}': {} ({:.2}%)\n",
i + 1,
display_char,
count,
(**count as f64 / char_count as f64) * 100.0
));
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_upper() {
let processor = TextProcessor;
assert_eq!(processor.to_upper("hello world"), "HELLO WORLD");
}
#[test]
fn test_to_lower() {
let processor = TextProcessor;
assert_eq!(processor.to_lower("HELLO WORLD"), "hello world");
}
#[test]
fn test_remove_duplicates() {
let processor = TextProcessor;
assert_eq!(processor.remove_duplicates("hello"), "helo");
assert_eq!(processor.remove_duplicates("aabbcc"), "abc");
}
#[test]
fn test_count_words() {
let processor = TextProcessor;
let result = processor.count_words("hello world hello");
assert!(result.contains("hello: 2"));
assert!(result.contains("world: 1"));
}
#[test]
fn test_analyze_text() {
let processor = TextProcessor;
let result = processor.analyze_text("hello world\nhello");
assert!(result.contains("字符数: 17"));
assert!(result.contains("行数: 2"));
assert!(result.contains("单词数: 3"));
}
}

179
examples/ex02/src/utils.rs Normal file
View File

@ -0,0 +1,179 @@
use std::fs;
use std::io::{self, Read};
use std::path::Path;
use anyhow::{Result, Context};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InputError {
#[error("文件不存在: {0}")]
FileNotFound(String),
#[error("读取文件失败: {0}")]
ReadError(#[from] io::Error),
#[error("无法处理输入: {0}")]
ProcessingError(String),
}
/// 表示文本输入的来源
pub enum InputSource<'a> {
/// 从文件读取
File(String),
/// 从标准输入读取
StdIn,
/// 直接使用字符串
Text(&'a str),
}
/// 从不同来源读取文本
pub fn read_input(source: InputSource) -> Result<String> {
match source {
InputSource::File(path) => {
read_from_file(&path)
.with_context(|| format!("读取文件 '{}' 失败", path))
},
InputSource::StdIn => {
read_from_stdin()
.context("从标准输入读取失败")
},
InputSource::Text(text) => {
Ok(text.to_string())
},
}
}
/// 从文件读取文本
pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<String> {
let path_str = path.as_ref().to_string_lossy().to_string();
if !path.as_ref().exists() {
return Err(InputError::FileNotFound(path_str).into());
}
fs::read_to_string(path)
.map_err(|e| InputError::ReadError(e).into())
}
/// 从标准输入读取文本
pub fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
}
/// 安全地打印结果
pub fn safe_print(text: &str) {
println!("{}", text);
}
/// 检查文本是否为有效的UTF-8
pub fn is_valid_utf8(bytes: &[u8]) -> bool {
String::from_utf8(bytes.to_vec()).is_ok()
}
/// 安全地处理文件路径
pub fn sanitize_path(path: &str) -> String {
// 移除可能导致路径遍历的模式
let path = path.replace("..", "");
// 规范化路径分隔符
#[cfg(target_os = "windows")]
let path = path.replace("/", "\\");
#[cfg(not(target_os = "windows"))]
let path = path.replace("\\", "/");
path
}
/// 创建一个带有生命周期的文本引用
pub struct TextRef<'a> {
content: &'a str,
}
impl<'a> TextRef<'a> {
/// 创建一个新的文本引用
pub fn new(content: &'a str) -> Self {
TextRef { content }
}
/// 获取内容
pub fn content(&self) -> &'a str {
self.content
}
/// 获取内容的一部分
pub fn substring(&self, start: usize, end: usize) -> &'a str {
let start = start.min(self.content.len());
let end = end.min(self.content.len());
if start >= end {
return "";
}
// 确保我们在字符边界上切割
let mut char_indices = self.content.char_indices();
let start_idx = char_indices
.find(|(i, _)| *i >= start)
.map(|(i, _)| i)
.unwrap_or(self.content.len());
let end_idx = char_indices
.find(|(i, _)| *i >= end)
.map(|(i, _)| i)
.unwrap_or(self.content.len());
&self.content[start_idx..end_idx]
}
}
/// 实现Drop trait用于演示资源清理
impl<'a> Drop for TextRef<'a> {
fn drop(&mut self) {
// 在实际应用中,这里可能会执行一些清理操作
// 这里只是为了演示Drop trait的使用
println!("TextRef被释放长度为: {}", self.content.len());
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_read_from_file() -> Result<()> {
let mut temp_file = NamedTempFile::new()?;
write!(temp_file, "测试内容")?;
let content = read_from_file(temp_file.path())?;
assert_eq!(content, "测试内容");
Ok(())
}
#[test]
fn test_is_valid_utf8() {
assert!(is_valid_utf8("你好".as_bytes()));
assert!(!is_valid_utf8(&[0xFF, 0xFE, 0xFD]));
}
#[test]
fn test_sanitize_path() {
assert_eq!(sanitize_path("../dangerous/path"), "/dangerous/path");
}
#[test]
fn test_text_ref() {
let text = "Hello, world!";
let text_ref = TextRef::new(text);
assert_eq!(text_ref.content(), "Hello, world!");
assert_eq!(text_ref.substring(0, 5), "Hello");
assert_eq!(text_ref.substring(7, 12), "world");
}
}

3
examples/ex02/test.txt Normal file
View File

@ -0,0 +1,3 @@
Hello Rust!
This is a test file.
Multiple lines for testing.