最近半年,我一直在学习Rust语言,从最开始的完全看不懂,到现在能写一些小项目,踩了不少坑,也积累了一些经验。Rust是一门很特别的语言,它的所有权、借用、生命周期这些概念,和其他语言完全不一样,学习曲线比较陡,但是一旦入门之后,你会发现它的设计非常精妙,写出来的代码既安全又高效。今天就来分享一下我学习Rust的经验和最佳实践,聊聊Rust到底是一门什么样的语言,为什么要学Rust,学习Rust的正确姿势是什么,有哪些常见的坑,以及一些实用的技巧和工具,希望能帮到想学习Rust的朋友,少走弯路。

一、Rust到底是一门什么样的语言

在说学习经验之前,先简单介绍一下Rust,让不了解的朋友有个基本的概念。

Rust是一门系统编程语言,由Mozilla研究院开发,第一版稳定版在2015年发布,到现在也就两年多的时间,是一门比较新的语言。Rust的设计目标是"安全、并发、实用",它想要在保证内存安全和线程安全的同时,达到和C/C++一样的性能,而且不需要垃圾回收器(GC)。

Rust最特别的地方,就是它的所有权系统(Ownership)和借用检查器(Borrow Checker),这是Rust保证内存安全的核心机制。在Rust里,每一个值都有一个所有者(owner),值在所有者离开作用域的时候会被自动释放,不需要手动管理内存,也不需要垃圾回收器。同时,Rust通过借用(borrowing)的机制,让你可以引用值而不获取所有权,但是借用有严格的规则,比如同一时间只能有一个可变引用,或者多个不可变引用,不能同时有可变和不可变引用,这些规则在编译的时候就会被检查,保证不会出现数据竞争、悬垂指针、空指针等问题。

除了所有权和借用,Rust还有很多其他的特性,比如模式匹配、trait(类似接口)、泛型、迭代器、闭包、宏等,这些特性让Rust既有系统编程语言的性能和控制力,又有高级语言的表达力和便利性。

Rust的工具链也非常好用,Cargo是Rust的包管理和构建工具,类似于Node.js的npm,Python的pip,但是功能更强大,集成了项目创建、构建、测试、文档生成、包发布等功能,用起来非常方便。crates.io是Rust的第三方包仓库,上面有很多优秀的库,可以直接引用。

总的来说,Rust是一门非常有特色的语言,它在系统编程领域开辟了一条新的道路,既没有C/C++的内存安全问题,也没有Java/Python等带GC语言的性能开销和停顿问题,是一门非常有前景的语言。

二、为什么要学Rust

很多人会问,现在编程语言这么多,有Python、JavaScript、Go、Java,为什么还要学Rust呢?我觉得主要有以下几个原因。

1. 内存安全,没有空指针、数据竞争等问题

这是Rust最大的优势。在C/C++里,内存管理是程序员的责任,很容易出现空指针、悬垂指针、内存泄漏、缓冲区溢出、数据竞争等问题,这些问题不仅会导致程序崩溃,还可能被黑客利用,造成安全漏洞。据统计,微软和谷歌的安全漏洞中,有大约70%是内存安全问题导致的。

而Rust通过所有权和借用检查器,在编译的时候就保证了内存安全和线程安全,不会出现这些问题。你写的Rust代码,只要能编译通过,就不会有内存安全问题,这是非常强大的。对于系统编程、网络编程、并发编程这些容易出内存问题的领域,Rust有天然的优势。

2. 性能优秀,和C/C++相当

Rust没有垃圾回收器,内存管理是在编译的时候通过所有权系统自动处理的,运行时没有额外的开销,所以性能非常优秀,和C/C++相当,甚至在某些场景下比C/C++还要快。这意味着你可以用Rust写操作系统、浏览器引擎、游戏引擎、数据库、网络服务等对性能要求很高的系统,而不用担心GC停顿或者性能问题。

现在已经有很多知名的项目在用Rust了,比如Mozilla的Servo浏览器引擎、Firefox的部分组件、Dropbox的文件同步引擎、Figma的多人协作后端、npm的包注册中心等,这些项目都证明了Rust的性能和可靠性。

3. 现代的语言特性,表达力强

Rust虽然是系统编程语言,但是它的语言特性非常现代,表达力很强,有模式匹配、trait、泛型、迭代器、闭包、宏、类型推导等高级特性,写起来不像C/C++那么繁琐,反而有点像高级语言。你可以用很简洁的代码表达复杂的逻辑,同时又能保证性能和安全。

比如Rust的迭代器和闭包,让你可以写出函数式风格的代码,非常简洁优雅;模式匹配让你可以很方便地处理枚举和各种情况;trait让你可以实现灵活的代码复用和抽象。这些特性让Rust不仅适合写底层系统,也适合写上层应用。

4. 优秀的工具链和社区

Rust的工具链非常好用,Cargo集成了项目管理、构建、测试、文档、发布等功能,用起来非常方便,比C/C++的make、cmake等工具好用太多了。rustup是Rust的版本管理工具,可以很方便地安装和切换不同版本的Rust。rustfmt是代码格式化工具,clippy是代码检查工具,这些工具都集成得很好,能帮你写出高质量的代码。

Rust的社区也非常活跃和友好,官方文档写得非常好,The Rust Programming Language这本书(俗称"the book")是我见过最好的编程语言入门书之一,通俗易懂,循序渐进。社区里的人也很友好,有问题在论坛或者GitHub上提问,很快就能得到回答。每年的Rust Conf也有很多高质量的分享。

5. 就业前景好,薪资高

虽然Rust还比较小众,用的公司还不是很多,但是Rust开发者的薪资普遍比较高,因为会Rust的人少,而需要Rust的岗位往往是比较核心的、对技术要求高的岗位,薪资自然就高。而且随着Rust越来越成熟,越来越多的公司开始采用Rust,未来的就业前景会越来越好。现在学习Rust,相当于提前布局,等Rust普及的时候,你已经是有经验的开发者了。

当然,不是所有人都需要学Rust,如果你只是做Web前端、移动端开发、数据分析等,Rust可能不是必须的。但是如果你对系统编程、底层技术感兴趣,或者想写高性能、高可靠的系统,Rust是一个非常好的选择,值得花时间学习。

三、学习Rust的正确姿势

Rust的学习曲线比较陡,特别是所有权、借用、生命周期这些概念,和其他语言完全不一样,很多人学了几天就放弃了,觉得太难了。我自己在学习的过程中也走了不少弯路,下面分享一下我觉得学习Rust的正确姿势。

1. 先读The Book,不要一上来就写代码

很多人学语言喜欢一上来就写代码,边写边查,但是Rust不适合这样,因为Rust的核心概念(所有权、借用、生命周期)和其他语言完全不一样,如果你不先理解这些概念,一上来写代码,会被编译器虐得很惨,各种编译错误,根本不知道为什么,很容易放弃。

我建议先认真读The Rust Programming Language这本书,也就是官方的"the book",这本书写得非常好,通俗易懂,循序渐进,把Rust的核心概念讲得很清楚。前几章一定要认真读,特别是关于所有权、借用、生命周期的章节,要理解透,不要跳过。读完一遍之后,对Rust的核心概念有了基本的理解,再开始写代码,就会顺利很多。

我自己一开始就是一上来就写代码,结果被各种编译错误搞疯了,后来停下来认真读了一遍the book,才恍然大悟,原来很多错误都是因为没有理解所有权和借用的规则。所以,先读书,再写代码,这是学习Rust的第一步,也是最重要的一步。

2. 理解编译器的错误提示,和编译器做朋友

Rust的编译器是出了名的严格,很多初学者被Rust的编译器虐得很惨,觉得编译器在和自己作对。但是实际上,Rust的编译器是你最好的朋友,它的错误提示非常友好,不仅会告诉你哪里错了,还会告诉你为什么错了,怎么改,甚至会给出修改建议。很多时候,你只要按照编译器的提示去改,就能把代码改对。

我建议,遇到编译错误的时候,不要烦躁,也不要跳过,认真读错误提示,理解为什么错了,然后按照提示去修改。Rust的错误提示真的写得非常好,比很多语言的编译器都友好,只要你耐心读,就能理解。而且,每解决一个编译错误,你对Rust的理解就加深了一步,这些错误都是你学习的机会。

我自己一开始也很讨厌Rust的编译器,觉得太严格了,后来慢慢发现,编译器指出的很多错误,确实是我代码里的问题,如果是在C/C++里,这些问题可能会导致运行时崩溃或者安全漏洞,而Rust在编译的时候就帮我找出来了,避免了以后出更大的问题。从那以后,我就和编译器做朋友了,遇到错误就认真分析,慢慢就觉得Rust的编译器真的很贴心。

3. 从小项目开始,不要一开始就写大项目

学习Rust的时候,不要一开始就写很大的项目,比如写一个Web框架、一个数据库,那样会很受挫,因为大项目涉及到很多复杂的概念和架构,初学者很难驾驭。建议从小项目开始,比如写一个命令行工具、一个小的算法实现、一个简单的HTTP服务器,通过这些小项目来熟悉Rust的语法和概念,等熟悉了之后,再写大一点的项目。

我自己学习的时候,是先写了一些小的算法题,比如LeetCode上的简单题,用Rust实现,熟悉基本的语法和标准库;然后写了一个命令行的待办事项工具,熟悉文件操作、错误处理、结构体等;然后写了一个简单的HTTP服务器,熟悉网络编程、并发、trait等。通过这些小项目,我慢慢熟悉了Rust,也建立了信心,后来再写大一点的项目就不那么吃力了。

4. 多用标准库,不要重复造轮子

Rust的标准库虽然不像Python那么丰富,但是也提供了很多常用的功能,比如集合类型(Vec、HashMap、HashSet等)、迭代器、文件操作、网络操作、并发原语(线程、通道、互斥锁等)、错误处理等。学习Rust的时候,要多熟悉标准库,尽量用标准库提供的功能,不要自己重复造轮子,这样既能提高效率,也能写出更地道的Rust代码。

比如,不要自己实现一个链表或者哈希表,标准库的Vec和HashMap已经非常好了,性能也很高;不要自己写线程池,标准库的线程虽然没有直接提供线程池,但是第三方库threadpool或者rayon已经实现得很好了;不要自己处理JSON,用serde和serde_json库,非常方便。

当然,如果你是为了学习,可以自己实现一些数据结构,但是在实际项目中,尽量用标准库或者成熟的第三方库,不要重复造轮子。

5. 学会用第三方库,善用crates.io

Rust的第三方库生态虽然还不如JavaScript、Python那么丰富,但是也已经有很多优秀的库了,crates.io上有超过一万个库,常用的功能基本都有。学习Rust的时候,要学会查找和使用第三方库,不要什么都自己写,这样能大大提高开发效率。

比如,做Web开发可以用actix-web或者rocket,做命令行工具可以用clap,做序列化可以用serde,做日志可以用log和env_logger,做错误处理可以用failure或者anyhow,做异步可以用tokio,做测试可以用proptest。这些库都非常成熟,文档也很好,用起来很方便。

查找第三方库的时候,可以用crates.io的搜索,也可以用lib.rs(一个更好的crates.io搜索界面),还可以看awesome-rust这个仓库,里面整理了很多优秀的Rust库和资源。选择库的时候,要看下载量、最近更新时间、文档质量、issue处理情况等,尽量选择成熟、活跃、文档好的库。

6. 多写多练,不要只看不写

Rust是一门实践性很强的语言,光看书是学不会的,一定要多写多练。看完the book之后,就要开始写代码,从小项目开始,不断地写,不断地练,在实践中加深对Rust的理解。很多概念,比如生命周期、trait对象、闭包捕获等,看书的时候可能觉得懂了,但是一写代码就发现还是不懂,这时候就要多写,多试,多思考,慢慢就懂了。

我自己的体会是,Rust的学习曲线虽然陡,但是有一个临界点,在达到临界点之前,你会觉得很难,各种编译错误,各种概念不懂,但是只要坚持写,写够一定量的代码,达到了临界点,就会突然觉得豁然开朗,很多之前不懂的概念一下子就懂了,写代码也顺畅了很多。所以,不要因为一开始难就放弃,坚持写,多写多练,过了那个临界点就好了。

四、Rust学习中常见的坑

学习Rust的过程中,有一些常见的坑,很多初学者都会遇到,我自己也踩过,下面分享一下,帮大家避开。

1. 和借用检查器作斗争

这是初学者最常见的问题,也是Rust最特别的地方。很多人写Rust代码,最大的障碍就是借用检查器,各种"cannot borrow as mutable because it is also borrowed as immutable"、"cannot move out of borrowed content"、"borrowed value does not live long enough"等错误,让人头大。

我的建议是,首先要理解借用的规则:同一时间,要么有一个可变引用,要么有多个不可变引用,不能同时有;引用的生命周期不能超过被引用值的生命周期;不能在有引用的时候移动值。理解了这些规则,再遇到错误的时候,对照规则分析,就能找到问题所在。

其次,遇到借用问题的时候,不要用unsafe去绕过,也不要随便用clone()来解决(虽然clone有时候是最简单的方法,但是会有性能开销),要先理解为什么错了,然后用正确的方式去解决,比如调整作用域、用副本、重构数据结构、用索引代替引用等。只有理解了借用的本质,才能真正掌握Rust。

最后,多写多练,借用检查器的问题,写多了就有感觉了,知道怎么写能通过编译,怎么写会报错,这是一个熟能生巧的过程。

2. 生命周期标注看不懂

生命周期是Rust里另一个让初学者头疼的概念,很多人看到函数签名里的'a、'b就晕了,不知道是什么意思,也不知道什么时候需要标注。

我的理解是,生命周期标注不是给编译器看的(编译器自己能推导),而是给人看的,用来告诉编译器和其他程序员,多个引用之间的生命周期关系,保证引用不会悬空。大部分时候,编译器可以自动推导生命周期,不需要你标注,但是在一些复杂的情况下,比如函数返回引用、结构体里有引用、多个引用之间有关系的时候,编译器推导不出来,就需要你手动标注。

学习生命周期的时候,不要一开始就纠结于复杂的标注,先理解生命周期的概念,然后在实际写代码的时候,遇到编译器要求标注的时候,再去学习怎么标注。慢慢地,你就会理解什么时候需要标注,怎么标注了。生命周期确实是Rust里比较难的概念,但是也没有想象中那么难,多写多练,慢慢就懂了。

3. 字符串处理太复杂

Rust的字符串处理也是初学者经常吐槽的地方,Rust有两种字符串类型,String和&str,还有各种转换,比如String转&str、&str转String、Vec<u8>转String等,初学者经常搞混,不知道该用哪个。

我的理解是,&str是字符串切片,是对一段字符串的引用,类似于其他语言里的字符串,但是是不可变的,而且不拥有数据;String是拥有所有权的字符串,是在堆上分配的,可以修改和增长。简单来说,当你需要拥有字符串的时候(比如作为结构体的字段、函数返回值、需要修改字符串),用String;当你只是引用字符串的时候(比如函数参数、只读访问),用&str。

两者之间的转换也很简单,String转&str用asstr()或者&*,&str转String用tostring()或者String::from()。熟悉了之后,就不会觉得复杂了。

另外,Rust的字符串是UTF-8编码的,不能像C语言那样直接按索引访问字符,因为UTF-8是变长编码,一个字符可能占1到4个字节,直接按字节索引可能会切到字符中间。如果你需要按字符访问,可以用chars()方法迭代字符,或者用第三方库。这一点和其他语言不太一样,需要注意。

4. 错误处理不习惯

Rust的错误处理和其他语言不一样,没有异常,而是用Result<T, E>类型来表示可能失败的操作,用?操作符来传播错误。很多初学者不习惯这种方式,要么到处用unwrap(),导致程序panic,要么不知道怎么处理错误。

我的建议是,不要随便用unwrap(),unwrap()意味着你认为这个操作一定不会失败,如果失败了就会panic,在实际项目中这是很危险的。除非你确定这个操作不会失败,或者在原型开发、测试代码中,否则不要用unwrap()。

正确的做法是,用?操作符把错误传播到调用方,或者用match/if let来处理错误。?操作符非常方便,它会在Result是Err的时候直接返回错误,是Ok的时候取出值,让错误处理的代码非常简洁。在函数之间传播错误的时候,尽量用?,不要自己写一大堆match。

另外,对于自定义错误类型,可以用failure或者anyhow库来简化错误处理,这些库提供了方便的错误类型和转换,让错误处理更简单。

5. 模块系统和crate结构搞不清

Rust的模块系统也比较特别,有crate、mod、use、pub等概念,还有文件和模块的对应关系,初学者经常搞不清,不知道怎么组织代码,怎么引用其他模块。

我的建议是,先理解crate的概念,一个crate就是一个编译单元,要么是一个库(library),要么是一个可执行文件(binary)。一个项目可以有多个crate,但是通常一个项目就是一个crate。在一个crate里,可以用mod来定义模块,模块可以嵌套,也可以对应到文件或者目录。

组织代码的时候,简单的项目可以都写在main.rs或者lib.rs里,稍微大一点的项目,可以按功能分成不同的模块,每个模块一个文件,用mod声明,用use引用。公开的项(函数、结构体、枚举等)要加pub关键字,不然其他模块访问不到。

Rust 2018 edition简化了模块系统,use路径更直观了,如果你用的是最新版的Rust,模块系统会好用很多。建议用最新版的Rust,享受新特性带来的便利。

五、一些实用的技巧和工具

最后,分享一些我在学习和使用Rust过程中发现的实用技巧和工具,希望能帮到大家。

1. 用好rustfmt和clippy

rustfmt是Rust的官方代码格式化工具,能自动格式化你的代码,让代码风格统一,符合Rust的规范。clippy是Rust的官方代码检查工具,能检查出代码中的常见问题、性能问题、风格问题等,并给出优化建议。这两个工具都非常好用,集成在Cargo里,只要运行cargo fmt和cargo clippy就能用。

我建议,每次写完代码都运行一下cargo fmt,格式化代码,保持代码风格统一;定期运行cargo clippy,检查代码中的问题,按照建议优化代码。clippy真的非常强大,能检查出很多你自己发现不了的问题,比如可以用更简洁的写法、有性能问题的代码、可能的bug等,用了之后代码质量会提高很多。

2. 用rustup管理Rust版本

rustup是Rust的官方版本管理工具,可以很方便地安装、切换、更新不同版本的Rust(stable、beta、nightly),还可以管理不同的目标平台(比如交叉编译到Windows、ARM等)。用rustup安装Rust非常简单,一条命令就能搞定,而且以后更新也很方便,运行rustup update就行。

我建议用rustup来安装和管理Rust,不要用系统的包管理器(比如apt、brew)安装,因为系统包管理器里的Rust版本往往比较旧,而且不好切换版本。用rustup可以随时切换stable、beta、nightly版本,非常方便。

3. 用好文档和源码

Rust的文档非常好,标准库的文档写得很详细,每个函数、每个类型都有说明和示例,而且文档是可以直接运行的(文档测试)。第三方库的文档一般也都写得不错,托管在docs.rs上。学习Rust的时候,要养成查文档的习惯,遇到不懂的类型或者函数,先查文档,文档里一般都有详细的说明和示例。

除了文档,Rust的源码也非常值得一读,标准库的源码质量很高,写得很优雅,是学习Rust的好材料。遇到不懂的地方,可以去看源码,看看标准库是怎么实现的,既能解决问题,又能学到好的写法。在docs.rs上看文档的时候,可以直接跳转到源码,非常方便。

4. 用cargo check快速检查

cargo build会编译整个项目,生成可执行文件,比较慢。如果你只是想检查代码有没有编译错误,不需要生成可执行文件,可以用cargo check,它只做类型检查和借用检查,不生成代码,速度比cargo build快很多。在开发过程中,可以频繁地用cargo check来检查代码,快速反馈,等代码写得差不多了,再用cargo build或者cargo run来编译运行。

很多编辑器和IDE的Rust插件(比如VS Code的rust-analyzer)就是用cargo check或者类似的机制来做实时检查的,能在你写代码的时候就提示错误,非常方便。建议用一个好的编辑器或者IDE,配合Rust插件,实时检查代码,提高开发效率。

5. 写测试,用cargo test

Rust内置了测试框架,不需要额外的测试库,直接用#[test]属性就能写单元测试,用cargo test就能运行所有测试。Rust的测试还支持文档测试,就是文档里的示例代码会被当作测试运行,保证文档和代码一致。

我建议,写Rust代码的时候,养成写测试的习惯,特别是对于核心逻辑,要写单元测试,保证代码的正确性。Rust的测试写起来很简单,运行也很快,cargo test会自动编译并运行所有测试,给出测试结果。有了测试,重构代码的时候就有信心了,不用担心改坏了。

6. 参与社区,多交流

Rust的社区非常友好和活跃,有问题可以在Rust用户论坛(users.rust-lang.org)、Rust subreddit(r/rust)、Stack Overflow上提问,一般很快就能得到回答。也可以关注Rust的官方博客、This Week in Rust周报,了解Rust的最新动态和优秀的文章、项目。

如果遇到了问题,自己解决不了,不要闷头苦想,可以去社区提问,Rust社区的人都很热心,会耐心解答。同时,也可以回答别人的问题,帮助别人的同时,也能加深自己的理解。参与社区,多交流,是学习Rust的好方法。

六、写在最后

Rust语言初探最佳实践:我总结了这些经验。

以上就是我学习Rust半年来的经验和最佳实践,从Rust是什么,为什么要学Rust,到学习Rust的正确姿势,到常见的坑,再到实用的技巧和工具,做了一个比较全面的分享。

Rust确实是一门比较难学的语言,特别是所有权、借用、生命周期这些概念,和其他语言完全不一样,学习曲线比较陡,很多人学了几天就放弃了。但是我觉得,Rust是值得花时间学习的,它不仅是一门实用的编程语言,能让你写出高性能、高可靠的系统,更重要的是,学习Rust会改变你对编程的理解,让你对内存管理、并发、类型系统有更深刻的认识,这些认识会让你在使用其他语言的时候也受益。

如果你对系统编程、底层技术感兴趣,或者想写高性能、高可靠的系统,不妨花点时间学习Rust,不要因为一开始难就放弃,坚持下去,过了那个临界点,你会发现Rust的世界非常精彩,你会爱上这门语言的。

当然,我也只是一个Rust的初学者,还有很多东西不懂,分享的经验也不一定完全正确,欢迎大家指正和交流。学习Rust是一个长期的过程,我也在不断学习和进步中,希望以后能分享更多更深入的Rust经验。

最后,用一句话结尾:"Rust是一门会让你痛苦一时,但是受益终身的语言。"愿每一个想学Rust的朋友,都能坚持下来,享受Rust带来的乐趣和成长。