囚于Rust的七日谈
这是第114514次尝试学Rust时所做笔记。迄今为止进度最大的一次(每次都是)。因为还没有成功经验,所以写点失败经验。
官网提供了三条学习路径:The Book、Rustlings、Rust By Example,但后两者都依赖The Book。Rustlings指明建议边读边练(同步课后练习册?),Rust By Example有不少The Book和参考文档的链接需要参照。所以最后世界线收束,不管怎样都逃不掉读The Book。
不知The Book的受众面目,引言说需要一门语言基础,却又大费篇幅写些编程科普,后续章节概念提早引入,读得我有点精神恍惚。
- 到底是哪门入门语言没有while循环?
- 每章平均有五个“将在后续章节详解”。
- 一张图能说清楚的事花了海量文字描述。
- HashMap不保序这种重要事项又只有一句话带过。
我觉得陡峭学习曲线或许真有The Book的功劳。对有经验的用户明明有更好的教学方法,却还是吊在大砖头书上。我一点都不想读砖头,结果兜兜转转还是不得不回来读砖头。说不定新手直接读The Book反而更容易。但在读完之后,我觉得读The Book是值得的。有经验的用户可以选择速读:遇到完全理解的代码块,跳过附近的说明。对需要学习Rust的用户来说,The Book是迟早要读的,因此读得最少的方法就是直接读它。
进度参考:
- 第一天:第1~4章(起步,案例:猜数字游戏,基本语句,所有权)
- 第二天:第5~7章(结构体,枚举 & 模式匹配,包、目标、模块)
- 第三天:第8~10章(常用容器,错误处理,泛型、接口、生命周期)
- 第四天:第11~14章(测试,案例:单文件内搜索,迭代器、闭包,cargo的更多用法)
- 第五天:第15~17章(智能指针,多线程,面向对象)
- 第六天:第18~19章(模式匹配、高级功能)
- 第七天:第20章(案例:多线程服务器)、附录
两个Rust特有的词汇crate = 目标 = target,trait = 接口 = interface好像没有创新的必要。我没看出来trait和传统interface有什么本质区别,独立命名空间、为已有类型补充实现、方法的默认实现都只是让接口变得更好用。后两点有点mixin的味道。
Rust真的在竭尽全力把所有可能发生的错误都前推到编译阶段。比如,不能对浮点数排序:
let mut nums = vec![1.14, 5.14, 19.19, 81.0];
nums.sort();
error[E0277]: the trait bound `{float}: Ord` is not satisfied
--> src/playground.rs:3:10
|
3 | nums.sort();
| ^^^^ the trait `Ord` is not implemented for `{float}`
|
= help: the following other types implement trait `Ord`:
i128
i16
i32
i64
i8
isize
u128
u16
and 4 others
错误〔错0277〕:「浮□」不满足接口「有序」
→ 源/操场.蟹 410行10列
帮助:有其他类型实现了接口「有序」:整128、整16、整32、整64、整8、整下标、自128、自16,后略4个
浮点数无序,怎么敢的?其实Rust是对的:浮点数由于NaN的存在而没有全序。也就是说,必须指定具体要如何对NaN排序。这时我就立刻想到偷整数顺序,直接把浮点数按位排序,导致负数排在正数后面。
nums.sort_by_key(|x| x.to_bits());
更常规的做法是明确指定浮点数的顺序。f64::total_cmp按−NaN < −∞ < −0 < +0 < +∞ < +NaN的顺序排序。
nums.sort_by(f64::total_cmp);
这其实是可能存在的概念上的错误,而不是程序执行会有什么异常。除了减少内存安全问题,Rust也在试图用它的类型系统挽救程序员对基础类型古怪特性的认知不足,避免产生不可预知的结果。
String强制为UTF-8编码,没有默认迭代方法也是为了拯救字节 = 字符不可磨灭的烙印。本意在减少对编码认知错误带来的问题,但作为系统级编程语言,把UTF-8视为至高无上的文本编码属实有点奇怪,遇到无效的UTF-8直接崩溃听上去更不安全。
在读The Book的过程中常有超前预判的情况发生:
- §10.1定义了二维泛型点结构
Point<T>
,并实现impl<T> Point<T>
后,提前用f64::hypot实现了下文的distance_from_origin。 - §10.2介绍了
&impl Trait
语法后,立即对此表示疑惑并提出下文的<T: Trait> &T
语法能表达相同类型。 - §13.2在Iterator的
type Item;
中简短提到了接口关联类型,发现其与泛型接口不同(这一点至少在§15.2才提到)并猜测关联类型具有唯一性。 - §15.5对println!修改stdout却不需要可变借用stdout提出疑问,随即在IntelliSense文档窗口中发现println!会对stdout加锁,继而预测到§16.3介绍的Mutex是一种线程安全的RefCell。
这种现象表明Rust的语法和语言功能具有一定的可预测性。
不要尝试在入门时编写链表和树等数据结构。不同于其他语言,只有精通了Rust才写得出链表。
不要用面向C++用户的教程中入门Rust,除非你真的会C++。这是说,即使你会C或Java,也不要尝试。C++的许多概念异于常见语言,概念命名非主流,和其他语言相比简直是两个世界,将带来双倍的困惑。
不要从嵌入式入门Rust。嵌入式指针对单片机、操作系统、WebAssembly等的底层开发。虽然Rust是少数底层到可用于嵌入式开发的语言,但是它并没有适合到可以以此入门。
先前尝试以微控制器(STM32)、复古游戏机(GBA)、自制操作系统这类嵌入式目标兴趣导向入门Rust,看来是非常行不通的。Rust和C++一样,以内存分配器存在为前提设计。甚至,至少现在稳定版本的Rust还认为内存很大,分配失败就意味着程序终结。系统级编程语言的目标是极致性能,Rust的选择似乎不那么适合嵌入式开发,但除了低配置的嵌入式,要追求极致性能的场合又有多少呢……
桌面平台的最佳使用案例是渲染引擎。但执意要嵌入的话,WebAssembly是一个平衡点。目前WebAssembly因为缺乏垃圾回收能力,把能用且稳定的语言排除到了只剩C、C++、Rust,要选哪个就很明显了。
只有语言强制要求,才能对他人的代码作出假设。系统级编程语言天生就要面临与硬件的交互作为输入输出的手段,所以Rust并没有完全严格地禁止随意操作:函数内可以自由地包含unsafe块。如果整个程序都由unsafe块和unsafe函数构成,就与C++无异。从这一点上来说,Rust的安全性和C++一样,甚至不如许多脚本语言。
Rust有ADT enum,C++有tagged union;Rust有Drop接口,C++有析构函数;Rust有Box,C++有unique_ptr;Rust有Arc,C++有shared_ptr。Rust有的安全措施,C++也有,只是没有或需要配置自动检查工具。Rust的安全操作是默认选项,危险操作需要标出;C++默认放任不管,安全操作反倒需要opt-in。Rust不是什么系统级Haskell,Rust只是默认检查得更多,用户安全意识更强罢了。
即使这样也足够了。在世界的反面,数不清的库将运行时提供的console和fetch装入混沌,以用户未可知的方式维护和篡改着内部和外部状态,是拜JavaScript文化所赐。Rust生态的安全性也来自Rust书对unsafe的轻描淡写,用户自发地避开unsafe,库自豪地表明未使用unsafe,未曾想过到头来只用安全子集也能做到如此之多。
我有一个极端的想法:从一切皆被标记为unsafe的Rust开始的教程,可以让C++用户获得熟悉感,随后逐步建立起安全的屏障。这是否会让这门陌生的语言更具亲和力,从年年不变的最难学神坛榜首退下?但在想象了一下Rust书的第二节写着“在Rust语言中,unsafe fn是定义函数的关键字,函数必须以unsafe fn开头”的世界后,我还是断然决定留在这个世界。正如尽管可变值是一众标榜为函数式的语言不得不品鉴的一环,“函数式”仍象征着值不可变一样,unsafe虽然是Rust不可或缺的一部分,却正是对它最少的使用造就了今天的Rust。