我如何从零开始学习一门编程语言
从 2019 年脱离 C++ 竞赛而转向计算机工程开发开始,我进入了一个向全栈开发发展的阶段。在我曾经的 About Page 中我可以像报菜名一样列出数十门掌握的语言。你可能觉得,哇,这好酷,但事实上掌握多门语言并不是一件难事。并不是天资聪颖,只需要掌握诀窍。2024 年 3 月我撰写了第一个 Rust 程序,4 月我在我的 GitHub 发布了第一个完全由 Rust 构成的程序 AH-dark/qiandaobot-rs,而到了 9 月份我已经开始使用 Rust 开发大型分布式 Web 业务系统。由此看来,我可以将先前在 Go、PHP、C++ 的经验迁移到 Rust 上。
思维模式
“迁移”是一个平常的概念,但大多数人不知道怎么实现它。在我看来,迁移在于保留相似的、替换不同的。比如 Go 和 Rust 都使用组合(inheritance)而非继承(composition),这是相似的地方;而 Rust 中我们使用 trait
,相比于 Go 的 interface
、Java 和 PHP 的抽象类,这是不同的地方。那么我们可以借鉴 Go 中对组合的设计思路到 Rust 中,同时对于 trait
的使用通过巨量的案例来熟悉。
当然,关键字、表象设计都是很简单的,你可以去看文档、看书、看一些学习资料(比如 Rustlings)来学习。但对于一些设计模式、编程范式、语言哲学,你需要通过实践来体会。比如 Rust 的 Ownership,它很大程度上逼迫我从面向对象的设计思路转向函数式编程的思路,让我们举一个简单的例子:
import java.util.concurrent.CompletableFuture;
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.getUserById(id);
}
public void listenUser(User user) {
CompletableFuture.runAsync(() -> {
this.userRepository.createUserListener(user);
}).exceptionally(ex -> {
System.err.println("An error occurred while processing user: " + ex.getMessage());
return null;
});
}
}
这是一个常见的 Java 代码,我们通过构造函数注入 UserRepository
。UserService
提供了一个 getUserById
方法,用于获取用户信息;同时提供了一个 listenUser
方法,用于监听用户信息。后者是非阻塞的,因为它使用 CompletableFuture
来包裹阻塞的 createUserListener
方法。对于 Go ,虽然不提供 class 概念,但我们也可以实现类似的:
type UserService struct {
userRepository UserRepository
}
func NewUserService(userRepository UserRepository) *UserService {
return &UserService{userRepository: userRepository}
}
func (svc *UserService) GetUserById(id int) User {
return svc.userRepository.GetUserById(id)
}
func (svc *UserService) ListenUser(user User) {
go func() {
svc.userRepository.CreateUserListener(user)
}()
}
这是一个 Go 代码,我们通过构造函数注入 UserRepository
。UserService
提供了一个 GetUserById
方法,用于获取用户信息;同时提供了一个 ListenUser
方法,用于监听用户信息。后者使用 go
关键字来开启一个新的 goroutine 来执行 CreateUserListener
方法。好了,你熟悉了这种设计模式,那你很自然的就写出了 Rust 的代码:
struct UserService {
user_repository: UserRepository,
}
impl UserService {
fn new(user_repository: UserRepository) -> Self {
Self { user_repository }
}
fn get_user_by_id(&self, id: i32) -> User {
self.user_repository.get_user_by_id(id)
}
fn listen_user(&self, user: User) {
tokio::spawn(async move {
self.user_repository.create_user_listener(user).await;
});
}
}
我们通过 new
函数注入 UserRepository
。UserService
提供了一个 get_user_by_id
方法,用于获取用户信息;同时提供了一个 listen_user
方法,用于监听用户信息。后者使用 tokio::spawn
来开启一个新的异步任务来执行 create_user_listener
方法。你觉得你完成了优秀的迁移。
但是问题在于,它是无法编译的,因为 listen_user
的异步调用中使用了 self
,而 Rust 无法保证 self
的生命周期始终大于异步任务的生命周期。在这个时候你就需要进行设计范式的思维转变。比如我们可以把 Self 更改为引用计数,即 Arc<Self>
,这样就可以保证 self
不会提前于异步任务被销毁。
但你可能面临下一个问题,如果你需要对 self 进行修改,那么你就需要使用 Mutex
来保证线程安全,即 Arc<Mutex<Self>>
。但 Rust 不允许你在 impl
块的函数中使用 Arc<Mutex<Self>>
,它只支持一些内置的类型包裹,这时你就需要使用额外的函数来实现。
这只是一个简单的例子,相似的问题我遇到过很多.比如 Go 实现链路追踪基于 context.Context
,而 Rust 实现链路追踪需要操作一个线程内全局变量,或者使用 tracing::instrument
宏。还有 Python 实现 IM 软件基于异步模型,但 TypeScript 需要同步的事件模型。
这些都是不同的设计模式和思维方法,你需要通过实践来体会。在看了足够多的案例、文档的基础上,自己思考,得到解决方法,理解解决方法。
探索 & 实践
学而不思则罔,思而不学则怠。实践是重要的,你可以通过写一些简单的程序来熟悉语言的基本语法,但这并不足以让你掌握一门语言。更多的需要由浅至深,由简单到复杂,由小到大的项目来实践。比如我学习 Rust 时,从一个简单的 Telegram Bot 开始,到一个复杂些的 Bot,再到自己创造分布式 cronjob 系统1,再到一个大型的分布式 Web 业务系统。这样的实践让我熟悉了 Rust 的生态、设计模式、编程范式。
你可能觉得,这样的项目太大了,我无法完成。但实际上,你根本就没有完成项目的必要。你只需要完成项目的一部分,比如一个模块、一个功能、一个特性,并用这个模块来提高你对某一部分的理解。至于项目大部分的 CRUD 内容,就放弃吧,你日后的想法还会有更高的价值,不要浪费时间在这些入门作品上。
我的习惯是,在每一个项目用一些新的技术栈,搭配我了解的,来实现一个新的功能。比如在 Universal Payment 中我选用了熟悉的 sea-orm、actix-web,但我尝试了新的 openidconnect、volo、lapin,这样我就可以在熟悉的基础上学习新的东西。相比于完全陌生的项目,这样的项目更容易让你学到东西而不是被淹没,同时也能让你更快地上手新技术栈。
在迁移上也是如此,比如从 Java 到 Kotlin,你可以逐渐重构系统的一部分。我在一个 Go 的 Telegram Bot 微服务系统引入了 Rust,先从一个简单的模块开始逐渐迁移整个系统。这样的迁移方式让我更加熟悉 Rust 的生态、设计模式、编程范式,因为你的经验可以和自己相对应,你知道你在 Go 是怎么想的而到了 Rust 又怎么想,这样的对比有助于你通过原本的经验来理解新的语言。
相比于纸上谈兵,探索和实践是更重要的。你可以通过阅读文档、书籍、博客来了解一门语言的基本语法,但只有通过实践,你才能真正掌握一门语言。在实践中,你会遇到各种各样的问题,这些问题会让你更加深入地了解一门语言,同时也会让你更加熟练地使用这门语言。当然,探索、实践需要你真正热爱计算机、热爱这门语言,无关于耐心、毅力,你需要真正喜欢这个过程,而不是为了完成一个任务而去完成。
开源社区
开源社区是一个很好的地方,你可以在这里了解大家是怎么做的,来避免闭门造车。我在社区广泛发布 Issue 和 PR,这让我了解到了很多新的东西,比如 openidconnect
库对于大量泛型的使用以及对 serde serializer 和 deserializer 的定义方式,还有利用 Rust 强大的权限系统保护内部对象的方法。通过和社区的交流以及和其他人的合作,你可以更快地把握新的技术、新的设计模式,同时矫正自己的错误。
包括 Go 的 Fx,也很大地启发了我对于语言的理解。Fx 是一个依赖注入框架,它让我了解到了 Go 的依赖注入是如何实现的,同时也让我了解到对于大型应用程序,如何确保 initialization 的清洁和可维护性。你不会在一个课本上看到 Fx,甚至社区的大半部分也不会用到 Fx,但在探索的过程中遇到它让我受益匪浅。
总结
这么看来,掌握一门语言需要的是热爱、探索、思维改变。作为一个仅活了 17 年却学了 10 年以上程序设计的人,我觉得这是最重要的。Computer Science 是一个很大的领域,你不可能掌握所有的东西,但你可以在其中享受探索、Debug、讨论所带来的乐趣。我希望这篇文章能够帮助到你,让你更好地掌握一门新的编程语言。
Footnotes
-
AH-dark/distributed-scheduler 是我在 Rust 中实现的一个分布式 cronjob 系统,上述的类生命周期问题就是在这个项目中遇到的。 ↩