前几天一次突发奇想,促成了上周六的 Rust 培训。发帖的时候,觉得 Rust 还很小众,100 个名额绰绰有余,没想到报名非常踊跃,没多久就超过了六十。有些还一次性报了好几个人的名,然后微信发给我报名邮箱。我担心名额不够,连忙修改文章,把上限 100 人,改成了 75 人,省得超了。一顿操作之后,我觉得问题不大,就放心睡去。
谁料一觉醒来,报名人数已经多达 110,大大超过预期。既来之,则安之 —— 我为我的 zoom plan 上添了一个 large meeting pack —— 总不能让后来者吃亏吧。
由于想讲的内容很多,我把培训的长度定为四小时:我想塞进去足够丰富的内容,让 Rust 入门者除了了解 Rust 的能力外,还能学到品尝语言学习中不会触碰的东西。时间太短的话,意义不大,学不到太多东西;时间太长,我自己无所谓,反正有的是东西可以讲,但听众恐怕受不了。事实证明,我太高估我讲授内容的速度,四个小时的培训,我整整讲了五个小时。
我对于学东西和讲东西,喜欢走硬核道路和实用主义道路。其实一名优秀的开发者,只要对一门语言很硬核地掌握了,对各种基本概念知其然,也知其所以然,那么,学任何其他语言都会比较轻松。因为组成语言的要素是相同的,大家可以相互印证,融会贯通。比如传值和传引用是怎么运作的,数据分配在栈上和堆上都有什么不同,为什么有些东西要分配在栈上,有些只能分配在堆上,这些东西在一门语言身上学通了,另一门语言也是一样。所以我从一个值在一个 scope 下各种各样访问的方式讲起,从而推演出为什么所有权和借用规则要这么设计。
虽然,第一性原理大家都耳熟能详,唠个嗑还时不时能拿第一性原理出来凸显逼格,但有多少人在学习的时候会真正应用第一性原理呢?这就是硬核道路。
实用主义道路是所学的内容要和实际开发联系起来。大部分编程语言的教程,所给的例子是为了演示代码而演示,为了展示错误而展示,所以大多数时候,大家学完还会一知半解,且无法应用到自己的工作生活中。我能理解这些教程的无奈:几乎每本编程语言的入门都不得不假定学习对象没有太多基础,所以只好每个例子尽可能独立,简单,而并不关心其实用性。而我希望学习者能够直接在知识和实际使用中搭建起桥梁,这样,所解决的问题不是人造的问题,而是真实存在的场景。比如讲生命周期,我使用了 strtok
的例子。
所以在准备培训时,选取 live coding 的主题并不轻松:要保证硬核和实用性,例子还要足够容易理解。最终,我选择了这么几个例子:
Write
/ Read
trait,实现了 Default
trait,使用了 std 里的文件 IO,也展示了在 Rust 下如何做 unit testing。Drop
trait 仅仅打印了一下,没做任何和资源释放相关的事情),变成了展示 RAII 相关的代码。不过这是个不错的使用 Semaphore
的场景。Iterator
trait。不难,但这个例子很有助于理解 Trait with associate type。从培训的过程看,这几个例子还是很不错地串起了相关的知识点。有小伙伴培训后跟我反馈「博物馆门票」的例子和 RAII 关系不大,我表示赞同;还有小伙伴觉得 Actor 的实现让他茅塞顿开,原来 actor 可以用 channel 这样巧妙地实现。不少小伙伴都希望,类似 actor 这样的例子能够更多。
这次培训,还是暴露出来一些我在准备方面的不足:
本次培训的视频已经稍作剪辑处理(主要是把中间那段声音小的部分处理了一下,视频加速 1.2 倍),放在 B 站和 Youtube 上。你可以在 B 站或者 Youtube 搜索「喜欢历史的程序君」找到我的频道,或者直接搜索题目「程序君的 Rust 培训」,就可以查看本期视频了。视频内容包括:
00:00 - Rust 初体验
15:20 - Live coding: Value tour
30:00 - 所有权和借用规则
40:55 - 类型系统初探
45:35 - 错误处理
48:25 - Live coding: 用文件持久化数据结构
01:15:25 - Rust 开发效率,ecosystem 和其他语言互操作
01:20:50 - 学 Rust 的方法
01:25:35 - 生命周期
01:42:50 - Live coding: strtok
01:53:00 - 静态生命周期
01:57:40 - RAII live coding: 博物馆门票
02:22:35 - 类型系统和泛型编程
02:33:30 - Live coding:Fibonacci 遍历器 02:40:05 - Trait Object
02:45:35 - Generics
02:53:12 - Live coding:Event Encoder
03:06:44 - 并发 - 并发原语
03:12:18 - Mutex 是如何构建的
03:18:55 - Semaphore 和 Channel
03:24:00 - Live coding: naive actor(实现一个简单的 actor model)
03:49:15 - 并发 - async/await
03:52:20 - Rust Future 原理
04:00:10 - 最后的 Q&A
第二期 Rust 培训,需要部分第一期 Rust 培训学到的基础知识(起码,内存管理,数据类型,泛型编程这些都不会详细讲了)。内容会包括:网络编程(设计中心化网络和 p2p 网络),宏编程,FFI 和 unsafe。也会是 4 小时左右的包含大量 live coding 的 Rust 培训。预计 6 月中下旬开讲。大家可以关注我的公众号文章。
勘误:在讲 actor 例子之后的 Q&A,有小伙伴问道为什么 HandleCall
trait 要实现在 Request
上,能不能实现在 Actor
上,因为他觉得实现在 Actor
上似乎更正确。我当时回答说也可以实现在 Actor
上。其实不对。按照我的做法:
pub struct Actor<State, Request, Reply> {
receiver: mpsc::Receiver<ActorMessage<Request, Reply>>,
state: State,
}
pub trait HandleCall {
type State;
type Reply;
fn handle_call(&self, state: &mut Self::State) -> Result<Self::Reply, std::io::Error>;
}
Actor
是我定义的结构,HandleCall
是我定义的 trait,按照 coherence rule,只有我才能为 Actor
实现 HandleCall
。这就失去其灵活性了。而 Request
是 actor 调用者可以决定的类型,所以对 Request
实现才足够灵活。当然,这就意味着,每一种 Request
需要实现一次 HandleCall
。另一种做法是改变 HandleCall
trait:
pub trait HandleCall {
type Request;
type Reply;
fn handle_call(&mut self, request: Self::Request) -> Result<Self::Reply, std::io::Error>;
}
这样,可以在 actor 的 State
上实现 HandleCall
。感觉这样更加贴合 erlang gen_server 的做法。