rust 学习笔记: 开发一个 TUI 应用
之前少量学习 rust, 其中的思想和设计方式对我影响很大. 我开始统一使用大驼峰和蛇形命名, 使用三段式版本号, 在 c++ 中实现 Result<T>, 大量使用 lambda 表达式试图模仿闭包, 尝试使用组合配合模板类代替 “特征” …
然后我发现, 以上这些东西, 在其他语言中真的很难实现.
例如想在 c++ 中实现双模板类, 写法简直复杂到家了, 并且每次构造一个返回值对象也会有一长串. 例如想实现 “特征”, 让不同类能快捷实现相同接口, 解决了各种预编译模板的问题, 最后用的时候, 发现 IDE 根本不知道你实现了什么共同接口.
可能还是我太菜了, 于是灰溜溜回来继续真正使用 rust, 多写一些东西, 在过程中学习.
上一个使用 rust 写的项目, 用的是 tauri 架构, 内容是 “ADB 工具箱”. 可是技术栈包括了网页前端, 外部依赖有 webview, 一个太不熟悉了, 一个又太重了.
上上个使用 rust 写的项目, 用的是 iced, 一个 rust 语言驱动的 gui 框架, 还不是正式版, 确实好用, 但是最终显示效果稍微有些问题: 缩放窗口会导致显示混乱, 以及显示文本稍多的时候, 性能影响就会很大.
所以这次试试更熟悉的内容(指 ADB), 和更成熟更轻量的框架(指 TUI).
知识点 (未整理)
()是基础类型的一种, 称为 “单元类型”. 它的值只有一种可能 —-(). 它代表 “无” 的意思, 类似于 c++ 的void.闭包形参
|...|: Rust 的闭包使用|参数|来接收框架/库传入的上下文对象 (例如ratatui的Terminal或Frame), 它们是实打实的输入参数, 而非 C++ 的捕获列表.表达式与
break value: Rust 中绝大部分代码块都是表达式 (包含返回值).loop {}也是表达式, 可以通过break 返回值;的形式跳出循环并把这个返回值作为整个循环的计算结果(例如break Ok(());).闭包传参的本质:框架传给闭包的通常是对象的可变借用 (
&mut Frame),而非所有权。闭包执行期间,你拥有画布的“独占修改权”,闭包结束后归还给框架。Rust 的显式哲学:Rust 不支持像 Kotlin 那样的隐式上下文 (
it或隐藏的this)。必须显式声明闭包参数名,这不仅是为了避免嵌套时的命名冲突,更是为了让借用检查器(Borrow Checker)能清晰地追踪生命周期。控制反转 (IoC, Inversion of Control): 一种设计模式. 传统的代码是我们主动调用库函数; 而控制反转是框架掌握主流程的控制权 (如初始化, 事件循环, 清理资源), 通过闭包或接口在特定时机回调我们的代码. 这被称为 “好莱坞原则” (Don’t call us, we’ll call you). 这种模式在 Rust 中特别适合用来确保资源 (如终端句柄) 被安全地分配和回收.
impl块中的函数分类与 C++ 对比:fn f(): 关联函数 (类似 C++ 静态函数)。只能用C::f()调用,不支持c.f()。fn f(&self): 方法 (类似 C++const成员函数)。不可变借用。fn f(&mut self): 方法 (类似 C++ 普通成员函数)。可变借用。fn f(self): 方法 (Rust 独有,夺取所有权)。调用后原对象被销毁/消耗。
- 全限定语法 (UFCS): 带有
self的方法也可以用命名空间调用,如C::f(&c)或C::f(&mut c),常用于解决不同特征 (Trait) 方法同名时的冲突问题,或作为函数指针传递。
派生宏 (Derive Macro): 如果只是想让结构体或枚举实现像
Debug,Clone,Default这种基础特征, 且不需要自定义内部逻辑, 可以直接在上方添加#[derive(Debug)]。编译器会在编译时自动生成你手写的那些匹配和输出代码,大幅减少模板代码。自定义特征实现:
#[derive(Trait)]只是宏在替你写代码。如果你需要自定义输出格式(例如隐藏敏感信息、改变显示文本),直接去掉#[derive(...)]并手动编写impl Trait for Type { ... }即可。DebugvsDisplay:Debug({:?}) 面向开发者,用于调试,通常用宏生成;Display({}) 面向最终用户,必须手动实现impl std::fmt::Display。函数重载: Rust 不支持传统的基于参数列表的函数重载 (如 C++ / Java)。
Rust 中替代重载的常见方案:
- 不同的函数名:最直接的方式,如
new()和with_capacity()。 - 泛型和特征 (Traits):使用
impl Into<T>等,使函数可以接收不同类型但能转换到目标类型的参数 (如同时接收&str和String)。 - 枚举 (Enum):如果参数组合差异很大,传入一个包含多种变体的 Enum。
- 建造者模式 (Builder Pattern):用于替代具有大量可选参数的重载。
- 宏 (Macro):如果需要可变长参数,通常使用宏(如
println!)。
- 不同的函数名:最直接的方式,如
避免“布尔盲症 (Boolean Blindness)”:在函数参数中传递
true/false会导致调用处可读性极差。Rust 的最佳实践是使用具有明确语义的枚举 (Enum) 代替 bool 参数。如果是多个配置项,应传递配置结构体 (Config Struct) 或使用建造者模式 (Builder Pattern)。Enum 爆炸问题:并不是所有的
bool都要替换成 Enum。- 何时保留 bool:如果函数名已经让布尔值的含义 100% 毫无歧义(例如单纯的 Setter:
set_enabled(true),或状态切换:set_fullscreen(true)),直接用bool没问题。 - 何时改用 Enum:当布尔值代表一种“模式”或“策略”(例如
Sort(true)应该变成Sort(Direction::Asc))。 - 终极解决方案:Rust 社区解决“参数太多”或“Enum 太多”的终极手段是 建造者模式 (Builder Pattern)。把拆散的参数全部收拢到一个配置对象的链式调用中,这是最地道的 Rust 风格。
- 何时保留 bool:如果函数名已经让布尔值的含义 100% 毫无歧义(例如单纯的 Setter:
链式调用与建造者模式实现: 核心原理是方法的返回值是
Self(夺取所有权并返回自己) 或者&mut Self(返回自身的可变引用)。- 如果是单纯修改自身属性,通常返回
&mut Self。 - 对于复杂的特定操作(如翻页),通常会返回一个带生命周期的临时中间结构体 (Builder/Command),在这个中间结构体上实现链式调用,最后通过执行动作的方法(如
.next())消耗掉这个中间体,从而避免污染主结构体(App)的成员。
- 如果是单纯修改自身属性,通常返回
标记待办事项 (TODO):
todo!()宏: Rust 官方提供的标准占位符. 它可以无视类型检查 (返回!类型, 可转化为任何类型), 保证代码能通过编译, 但在运行到此处时会发生panic并报错.unimplemented!()宏: 机制与todo!()完全一样, 但语义上偏向于 “此功能有意留空或尚未实现”, 而todo!()偏向于 “我很快就会来写”.// TODO: ...注释: 用于不需要阻断程序运行的普通备忘记录.
迭代器方法
mapvsfor_each:map(): 用于转换数据。它接收一个闭包,将元素映射为新值。它是惰性的 (Lazy),仅仅返回一个新的迭代器。如果不调用collect()等消耗型方法,map里的闭包绝对不会执行。for_each(): 用于副作用 (Side Effects)。它接收一个闭包执行操作(如打印、修改外部变量),没有返回值(返回())。它是立即执行的 (Eager) 消费者。- 最佳实践:需要产生新集合用
map;只需要执行动作而不需要新集合用for_each或普通的for循环。
Enum 的相等性比较:
#[derive(PartialEq)]: 要使用==比较两个 Enum 实例,必须派生PartialEq特征。如果 Enum 有包裹的数据(如Shell(i32)),则内部数据也必须支持PartialEq,比较时会连同内部数据一起严格对比。matches!宏: 如果你只关心它是哪个变体 (Variant),不关心里面包裹的具体数据(例如只要是Shell页面就高亮,不管uid是多少),使用matches!(变量, 模式)是最优雅的。例如:matches!(self.page, Page::Shell(_))。
rust 学习笔记: 开发一个 TUI 应用