讲讲我作为TypeScript开发人员学习Rust的经验吧,希望对你有帮助
像许多开发人员一样,我通过专注于网络技术开始了我的编程生涯。我相信这是一个很好的起点,JavaScript,互联网的语言,以及许多,是一个令人难以置信的多才多艺的选择。
随着我对JavaScript等高级语言的经验越来越丰富,我也对它们的工作原理越来越感兴趣:它们正在做出哪些选择和权衡,以及更高层次的抽象的好处和成本是什么。
对我来说,更深入理解的最好方法之一是学习低级编程语言。毕竟,这些是通常解析和解释我们的JavaScript代码的语言。例如,V8引擎(由Google Chrome和Node.js使用)和WebKit(由Safari和Bun使用)都是用C++编写的。但是,尽管C++是低级编程的中流砥柱,但C++不是我选择的语言......
在伟大的低级编程语言中,Rust对我来说是最令人兴奋的。去年,Rust连续第八年成为Stack Overflow年度调查中最受赞赏的编程语言。
该语言承诺在与C和C++相同的联盟中运行时性能,但具有严格的类型系统、许多内存安全功能和更主动的错误处理方法------因此您可以避免垃圾收集器的开销,避免内存泄漏的风险,而内存泄漏在C等语言中更容易创建。
Rust用途广泛,具有面向对象和函数式编程的范式,连续第三年,它一直是Web汇编中最受欢迎的语言,甚至已成为Linux内核中的重要语言。Rust也在JavaScript领域掀起波澜,在那里它一直被用来构建重要项目,如Deno,以及最近的LLRT(亚马逊无服务器函数的低延迟运行时)。
如何学习Rust
与任何其他编程语言一样,我认为学习Rust的最佳方式是尝试用该语言编程一些东西。
然而,Rust的初始学习曲线似乎比我近年来尝试过的其他语言更陡峭,所以在开始使用该语言之前,值得花更长的时间浏览入门材料。
Rust组织的网站有很好的建议。现在,这些是The Book、Rustlings课程和Rust by Example。我还推荐Rust by Practice,这是一个类似于Rustlings的互动课程。
在YouTube上,NoBoilerplate频道帮助我对这种语言感到兴奋,并且是解释一般Rust概念的绝佳来源。如果您对托管Rust感兴趣,AWS有一篇关于在其平台上增加对Rust支持的博客文章。
本文的其余部分不是Rust的初学者指南。如果这就是你要找的,我建议你点击上面的链接。相反,与我每天专业使用的语言TypeScript相比,我分享了开发人员使用Rust体验的一些最显著差异的想法。
编译器
Rust编译器经常被引用为Rust最好的部分之一,然而,对于初学者来说,它也可能感觉是最令人讨厌的部分之一!
来自TypeScript,我惊讶于编译器在多大程度上改变了编码体验。像许多开发人员一样,我通常会避开适当的调试工具,而倾向于自由记录值。但在Rust中,只有当编译器满意时,您才能记录值。
在某些情况下,这被证明是令人沮丧的:例如,在为反序列化步骤编写严格类型之前,我想记录一个请求的JSON有效负载。(后来,我了解到这可以用theserdeserde_json::Value类型来完成)。
但一般来说,努力满足编译器意味着,通常,当我运行代码时,它会按照我的预期工作。这里的权衡似乎很清楚。至少对于初学者来说,运行代码确实需要更长的时间,但当该代码确实运行时,它更安全、更可预测、性能更高。你在写作阶段投入了更多的工作,但出现错误或性能记忆问题的几率似乎更低------在大型、不断增长的项目中,这些好处感觉越来越重要。
来自错误消息不太有用的语言的开发人员可能会发现,他们已经准备好快速扫描错误。但到目前为止,我发现Rust编译器错误非常好,经常确切地告诉你需要做什么才能运行代码------我对语言及其类型越有经验,我就越能理解编译器试图告诉我什么!
类型系统
我知道不是每个JavaScript开发人员都喜欢TypeScript------例如,请参阅这篇(不)著名的博客文章------但我无法想象在没有类型的情况下编写大型JavaScript应用程序。然而,TypeScript有其弱点,我认识到拥有一种类型为一流公民的语言有很多好处。Rust的类型系统经常被誉为其最佳功能之一。
也就是说,Rust的类型系统对我来说是新的部分不是Rust独有的,而是几乎所有低级语言的特征。例如,像其他低级语言一样,Rust允许我们非常具体地说明我们希望变量占用多少内存空间。如果我们知道一个数值将始终是0到255之间的整数,我们可以将其分配给长度为8位的u8。或者,如果我们的数字可以高于255,但我们知道是否会低于65,535,那么我们可以将其分配给16位u16类型,等等。
然而,在某些方面,Rust确实比其他低级语言走得更远。例如,它提供了至少八种字符串类型,而不是C的一个char[]类型,这有助于我们避免脚枪。(尽管如此,不要害怕,因为大多数用例都由&str和String覆盖!)
当然,TypeScript没有提供接近这种粒度水平的地方,因为根据设计,JavaScript不希望我们担心内存管理,因此为我们分配内存。这为我们节省了一份工作,但效率较低,因为JavaScript引擎必须在程序运行时动态分配内存。在小规模上,这没有什么区别。但在大型应用程序中,更高效和有目的的内存分配可以使程序占用更少的内存占用空间。
内存分配
在TypeScript中,我们在Javascript(一种不读取我们类型的语言)之上叠加类型注释,每当构建TypeScript代码时,它们都会被删除。
在Rust中,与其他类型为一流公民的语言一样,类型注释不仅仅是注释,它为该特定类型分配内存,并向我们保证该值将具有给定类型。
例如,如果我们将i8类型传递给下面的parse方法,它为small_int保留了8位内存。
let small_int = "127".parse::<i8>().unwrap();
parse方法甚至可以从变量类型中推断类型,因此我们也可以编写:
let small_int: i8 = "127".parse().unwrap();
在这种情况下,如果我们试图超越给定类型允许的内存,编译器也会对我们大喊大叫。因此,如果我们尝试将字符串"128"解析为i8,我们将无法编译。
比较TypeScript,其中类型标记只是标记。它们不会更改底层类型或分配的内存。下面,TypeScript期望x是一个字符串。但在底层的JavaScript中,它将是一个数字。
const x = 10 像字符串一样未知;
这个例子对TypeScript编译器有点不公平;我们正在使用unknown作为转义舱口来强行分配错误的类型!
然而,这是一个很容易抓住的简单例子。在现实世界的应用程序中,当处理更复杂的数据类型或从第三方获取的数据时,TypeScript更容易歪曲现实。
错误处理
让我们再次以将字符串转换为整数为例。这一次,让我们想象一下我们的整数是由用户提供的字符串,因此我们无法再保证我们可以正确解析它。
let parsed_int = submitted_str.parse::<i32>().unwrap();
在这里,我们使用unwrap来获得成功解析的值。但这种方法通常不鼓励。相反,Rust为我们提供了Result枚举,这迫使我们手动处理错误。
We can still cause our program to panic with thepanic!macro, but we can pass a custom error message which will help us quickly understand what went wrong:
添加图片注释,不超过 140 字(可选)
或者我们可以返回一个默认值------在这种情况下是0:
添加图片注释,不超过 140 字(可选)
还有一个简写方法:unwrap_or_default。
当然,这种行为在JavaScript中是可能的,但区别在于,在JavaScript中,您必须选择加入,而在Rust中,您必须使用unwrap来选择退出。
或者,换句话说,在JavaScript中,你必须有意识地处理错误。而在Rust中,你被迫要么处理错误,要么有意识地决定你只关心成功的道路。
可选值
Rust使用类似的方法来处理可选值。在TypeScript中,我们可以使用方便的?指示一个值可能undefined。
添加图片注释,不超过 140 字(可选)
这个TypeScript代码将毫无问题地编译,即使我们有返回我们不想要的东西的风险!
但是,如果我们使用Option在Rust中编写类似的东西,我们会收到一个编译时错误。
添加图片注释,不超过 140 字(可选)
The code above warns us that we cannot useOptioninside ourformat!macro --- preventing us from returning something unexpected. Instead, we are forced to handle this possibility. Here's one solution, usingmatch:
添加图片注释,不超过 140 字(可选)
再一次,这在TypeScript中是可以实现的------而且它更简洁。但两种语言之间的关键区别在于,在TypeScript中,开发人员有需要识别潜在的问题,因此我们最终不会返回"Hello undefined"但在Rust中,除非我们处理名称不可用的情况,否则我们的代码将无法编译。
在这样的简单例子中,很难认识到更冗长的方法的好处,因为很容易看出可能会出错什么。但是,如果您曾经开发过大型应用程序,很明显,Rust的选择退出方法可以使我们免于许多潜在的事故。
所有权和借贷
最后,我想谈谈所有权和借贷,这些概念在像Rust这样的低级语言中比像TypeScript这样的高级语言更有意义。
在TypeScript中,我们需要意识到我们是突变值还是克隆值。
添加图片注释,不超过 140 字(可选)
在上面的TypeScript代码中,sort将数组原位突变,更改原始值。但toSorted创建一个克隆,我们可以将其分配给一个新的变量,并保持原始数组不变。
一般来说,像toSorted这样的非破坏性方法在TypeScript等语言中通常是首选,因为跟踪突变变量可能很棘手,除非对内存或性能有明显的好处,否则通常认为最好完全避免这样做。
然而,Rust允许我们更深入地了解突变或克隆值,其好处是,一旦值实现其光荣的目的,我们可以更有效地使用内存,也可以更轻松地释放内存。
首先,默认情况下,所有变量都是不可变的,并且必须使用mut关键字明确标记为可变。
此代码抛出一个错误:
让foo = 10; foo += 10;
此代码不:
让mut foo = 10; foo += 10;
这感觉大致等同于JavaScript中的let与const。胸围锈走得更远。
例如,在JavaScript中,某些变量类型,如数组,总是可变的。即使我们使用const实例化它们,我们也可以push、pop和重新分配索引。在Rust中,我们需要mut才能做到这一点:
let mut nums: Vec<i32> = vec![1,2,3,4,5]; nums.push(6);
Rust还允许我们将值的所有权从一个变量转移到另一个变量。以以下为例:
让nums:Vec<i32> = vec![1,2,3,4,5]; let doubles: Vec<i32> = nums.into_iter().map(|n| n * 2).collect(); dbg!(nums); // 这个抛出 dbg!(双打);
此代码抛出,因为intointo_iter方法创建了一个"消耗迭代器";换句话说,从nums夺走所有权,并赋予它todoubles。因此,我们不能调用dbg!(nums)在doubles创建后。
如果我们想保持对nums访问权限,而是克隆其值,我们可以使用iter方法而不是into_iter。重要的是,Rust给了我们一个选择,转移所有权的能力可以帮助我们提高内存分配的效率。
We can also move simple values. In the code below, when our str variable is used as an argument for calculate_length , it is no longer accessible.
fn main() { let str = String::from("Hello world!"); let len = calculate_length(str); dbg!(str); // 这扔 } fn calculate_length(s: String) -> usize { s.len() }
在这里,我们可以通过使用ampersand&传递对我们字符串的引用来解决这个问题,而不是传递字符串本身。我们还需要更新函数参数才能获得引用:
fn main() { let str = String::from("hello"); let len = calculate_length(&str); dbg!(str,len); } fn calculate_length(s: &String) -> usize { s.len() }
要反其道而行之,并取消引用一个值,我们可以使用星号*。这些功能共同帮助我们安全高效地控制内存,正因为如此,Rust不需要依赖垃圾收集器,这使我们能够解锁更高的性能水平,而没有C等语言的风险,这使开发人员有更大的风险来了解他们在做什么!
总的来说,我学习和使用Rust的早期经验是非常积极的。入门感觉比我近年来尝试过的其他语言更困难,但我相信学习Rust已经让我更加了解我每天使用的高级语言的基本工作,我很高兴能在个人项目中更多地使用它。
如果您是Rust的新手或对学习这种语言感到好奇------特别是来自更高层次的语言------那么希望您觉得这篇文章有用。当然,这只是一个粗略的观点,有很多主题,如特征和寿命,超出了文章的范围。