我对 Rust 的第一印象
我喜欢 Rust 的哪些方面
我不喜欢 Rust 的地方
挑剔
结论
最初发表于deepu.tech。
所以我前一段时间开始学习 Rust,而且由于我关于我对 Go 的看法的帖子很受欢迎,所以我决定写下我对 Rust 的第一印象。
但与 Go 不同的是,我实际上并没有用 Rust 构建过任何实际应用,所以这里我的观点纯属个人观点,有些可能并不准确,因为我可能误解了某些内容。所以,请给予我一个 Rust 新手的尊重。如果您发现我在这里说的某些内容不准确,请告诉我。另外,一旦我开始更多地使用这门语言,我的印象可能会有所改变。如果真的改变了,我一定会更新这篇文章。
正如我在其他一些帖子中所说,我认为自己现在比年轻时更加务实。一些观点也源于务实的视角(至少我是这样认为的)。
我权衡了实用性、可读性和简洁性,而不是花哨的功能、语法糖和复杂性。此外,一些我不喜欢但觉得没什么大不了的事情,我会放在“挑剔”部分,而不是“不喜欢”部分,因为我认为这样更公平。
Rust 与 Go 等语言的一个不同之处在于,Rust 不进行垃圾收集,而且我知道许多语言特性/选择在设计时都考虑到了这一点。
Rust 主要面向过程式/命令式编程风格,但它也允许你进行一些函数式和面向对象编程风格。而这正是我最喜欢的混合风格。
那么,事不宜迟,让我们开始吧。
我喜欢 Rust 的哪些方面
我真正喜欢的东西,没有特别的顺序。
无垃圾收集
Rust 中你首先会注意到的一个特性,尤其是如果你之前使用过 Java 或 Golang 等支持垃圾回收机制的语言,那就是它缺乏垃圾回收机制。没错,Rust 中确实没有 GC,那么它是如何确保我的程序在给定的内存中高效运行,又是如何避免内存不足错误的呢?
Rust 中有一个叫做所有权 的东西,所以基本上 Rust 中的任何值都必须有一个变量作为其所有者(并且一次只能有一个所有者)。当所有者超出范围时,无论该值位于栈内存还是堆内存中,它都会被丢弃并释放内存。例如,在下面的示例中, 的值foo
在方法执行完成后立即被丢弃,而 的值bar
在代码块执行后立即被丢弃。
fn main() {
let foo = "value"; // owner is foo and is valid within this method
{
let bar = "bar value"; // owner is bar and is valid within this block scope
println!("value of bar is {}", bar); // bar is valid here
}
println!("value of foo is {}", foo); // foo is valid here
println!("value of bar is {}", bar); // bar is not valid here as its out of scope
}
因此,通过仔细地限定变量的作用域,我们可以确保内存使用得到优化,这也是为什么 Rust 允许您几乎在任何地方使用块作用域的原因。
此外,Rust 编译器还能帮你处理重复的指针引用等等。以下代码在 Rust 中无效,因为 Rustfoo
现在使用的是堆内存而不是栈,并且将引用赋给变量会被视为移动。如果需要深度复制(开销较大),则必须使用clone
执行复制而非移动的函数来执行。
fn main() {
let foo = String::from("hello"); // owner is foo and is valid within this method
{
let bar = foo; // owner is bar and is valid within this block scope, foo in invalidated now
println!("value of bar is {}", bar); // bar is valid here
}
println!("value of foo is {}", foo); // foo is invalid here as it has moved
}
所有权概念可能有点难以习惯,特别是因为将变量传递给方法也会移动它(如果它不是文字或引用),但考虑到它可以使我们免于 GC,我认为它是值得的,并且编译器会在我们犯错误时帮助我们。
默认不可变
变量默认是不可变的。如果要修改变量,必须使用mut
关键字 进行特殊标记。
let foo = "hello" // immutable
let mut bar = "hello" // mutable
变量默认按值传递,如果要传递引用,则必须使用&
符号。与 Golang 非常相似。
fn main() {
let world = String::from("world");
hello_ref(&world); // pass by reference. Keeps ownership
// prints: Hello world
hello_val(world); // pass by value and hence transfer ownership
// prints: Hello world
}
fn hello_val(msg: String) {
println!("Hello {}", msg);
}
fn hello_ref(msg: &String) {
println!("Hello {}", msg);
}
当你传递一个引用时,它仍然是不可变的,所以我们必须像下面一样显式地将其标记为可变的。这使得意外的改变变得非常困难。编译器还确保在一个作用域中只能有一个可变引用。
fn main() {
let mut world = String::from("world");
hello_ref(&mut world); // pass by mutable reference. Keeps ownership
// prints: Hello world!
hello_val(world); // pass by value and hence transfer ownership
// prints: Hello world!
}
fn hello_val(msg: String) {
println!("Hello {}", msg);
}
fn hello_ref(msg: &mut String) {
msg.push_str("!"); // mutate string
println!("Hello {}", msg);
}
模式匹配
Rust 对模式匹配具有一流的支持,这可用于控制流、错误处理、变量赋值等。模式匹配也可以用于语句、if
循环和函数参数。while
for
fn main() {
let foo = String::from("200");
let num: u32 = match foo.parse() {
Ok(num) => num,
Err(_) => {
panic!("Cannot parse!");
}
};
match num {
200 => println!("two hundred"),
_ => (),
}
}
泛型
我喜欢 Java 和 TypeScript 的一点是泛型。它使静态类型更加实用,并且更符合 DRY 原则。没有泛型的强类型语言(比如 Golang)用起来很烦人。幸运的是,Rust 对泛型提供了强大的支持。它可以用于类型、函数、结构体和枚举。更棒的是,Rust 在编译时会将泛型代码转换为特定代码,因此使用它们不会有任何性能损失。
struct Point<T> {
x: T,
y: T,
}
fn hello<T>(val: T) -> T {
return val;
}
fn main() {
let foo = hello(5);
let foo = hello("5");
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
静态类型和高级类型声明
Rust 是一门严格类型语言,拥有静态类型系统。它还拥有强大的类型推断功能,这意味着我们无需手动定义所有类型。Rust 还允许复杂的类型定义。
type Kilometers = i32;
type Thunk = Box<dyn Fn() + Send + 'static>;
type Result<T> = std::result::Result<T, std::io::Error>;
简洁明了的错误处理
Rust 的错误处理非常出色,有可恢复的错误和不可恢复的错误。对于可恢复的错误,你可以使用枚举的模式匹配Result
或简单的 expect 语法来处理。Rust 甚至提供了一个简写运算符来从函数中传递错误。不妨试试 Go。
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
// or
let f = match File::open("hello.txt") {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
元组
Rust 内置了对元组的支持,当您必须从函数返回多个值或想要解开一个值等时,这非常有用。
块表达式
在 Rust 中,几乎可以在任何地方使用具有独立作用域的块表达式。它还允许在块表达式、if 语句、循环等中为变量赋值。
fn main() {
let foo = {
println!("Assigning foo");
5
};
let bar = if foo > 5 { 6 } else { 10 };
}
漂亮的编译器输出
Rust 的编译错误输出是我见过最好的。我觉得没有比这更好的了。它真的太有用了。
内置工具
与许多现代编程语言一样,Rust 也提供了许多内置的标准工具,说实话,我认为这是我遇到过的最好的工具之一。Rust 内置了Cargo包管理器和构建系统。它是一款非常优秀的工具,能够处理所有常见的项目需求,例如编译、构建、测试等等。它甚至可以创建一个带有框架的新项目,并在全局和本地管理项目的包。这意味着你无需担心设置任何工具即可开始使用 Rust。我非常喜欢这个工具,它节省了大量的时间和精力。每种编程语言都应该拥有它。
Rust 还提供内置实用程序和断言来编写测试,然后可以使用 Cargo 执行。
在 Rust 中,相关功能被分组到模块中,模块被分组到 crate 中,crate 又被分组到包中。我们可以在一个模块中引用另一个模块中定义的项目。包由 cargo 管理。您可以在Cargo.toml
文件中指定外部包。可重用的公共包可以发布到crates.io注册表中。
甚至还有内置的离线文档,运行一下就能查看rustup docs
,rustup docs --book
真是太棒了。感谢 Mohamed ELIDRISSI指出这一点。
并发
Rust 对内存安全的并发编程提供了一流的支持。Rust 使用线程进行并发,并具有 1:1 的线程实现,即每个操作系统线程对应一个绿色线程。Rust 编译器在使用线程时保证内存安全。它提供诸如等待所有线程完成、使用move
闭包或通道共享数据(类似于 Go)等功能。它还允许您使用共享状态和同步线程。
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
宏和元编程
虽然我并非完全喜欢 Rust 中宏的方方面面,但这里的优点还是多于缺点。例如,注释宏就非常方便。但我不太喜欢过程宏。对于高级用户,你可以编写自己的宏规则并进行元编程。
特质
Traits 与 Java 中的接口同义,用于定义可在结构体上实现的共享行为。Traits 甚至可以指定默认方法。我唯一不喜欢的是它的间接实现。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct NewsArticle {
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
}
fn main() {
let article = NewsArticle {
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
必要时可以使用不安全的功能
- 在你知道自己在做什么的高级用例中很有用。在我看来,这是必要之恶。我喜欢它,因为它只能在一个
unsafe { }
块内执行,非常明确。如果不是这样,我会把它移到不喜欢的部分。 - 当你使用这些功能时,Rust 编译器无法保证内存和运行时的安全,你需要自行解决。因此,对于高级和经验丰富的用户来说,这绝对适用。
我不喜欢 Rust 的地方
我不太喜欢的东西,没有特别的顺序。
复杂
我不喜欢一门语言提供多种方法来实现相同的功能。这一点我认为 Golang 做得很好,它不会出现两种方法来实现相同的功能,因此人们在处理更大的代码库和代码审查时会更加轻松。而且,你不必总是思考最佳的实现方式。不幸的是,Rust 就是这样的,我并不喜欢它。在我看来,这会让语言变得更加复杂。
- 迭代方式太多 -> 循环、while、for、迭代器。
- 创建过程的方法太多了 -> 函数、闭包、宏
编辑:根据这里和Reddit上的讨论,我觉得我对复杂性的感知只会增加。感觉一旦你理解了所有细节,很多事情似乎需要花些时间才能理解。我敢肯定,如果你有 Rust 经验,这对你来说应该轻而易举,但这门语言确实相当复杂,尤其是函数和闭包在不同上下文中的行为方式,以及结构体中的生命周期等等。
同一上下文中的变量的阴影
Rust 可以让你做到这一点
{
let foo = "hello";
println!("{}", foo);
let foo = "world";
println!("{}", foo);
}
- 默认情况下是不可变的(我理解能够对不可变变量执行转换的理由,特别是在将引用传递给函数并取回它时)
- 在我看来,这会让人们无意中养成不良习惯,我宁愿将变量标记为可变,因为我也考虑了上述变异。
- 在 JavaScript 等语言中,你可能会意外地隐藏一个变量,就像你意外地改变一个变量一样
- 给人们一把枪来射脚
补充:我在这里和Reddit上看到很多评论解释了为什么这个功能很好。虽然我同意它在很多场景下都很有用,但修改功能也同样有用。我认为如果没有这个功能也完全没问题,人们仍然会喜欢 Rust,而且所有人都会支持没有这个功能的决定。所以我对此的看法没有改变。
函数不是一等公民
虽然可以将一个函数传递给另一个函数,但它们并不像 JavaScript 或 Golang 那样是一等公民。
您不能从函数创建闭包,也不能将函数分配给变量闭包在 Rust 中与函数是分开的,在我看来,它们与 Java 的 lambda 表达式非常相似。虽然闭包足以执行一些函数式编程模式,但如果能只使用函数来实现,让语言更简单一些,那就更好了。
编辑:天哪!这个观点在这里和Reddit上引发了很多讨论。所以看起来函数和闭包在上下文上既相似又不同,函数似乎也几乎像是一等公民,但如果你习惯于 Go 或 JavaScript 等函数更直接的语言,那么你将会陷入疯狂。Rust 中的函数似乎要复杂得多。许多评论的人似乎忽略了一个事实,即我的主要抱怨是,在大多数情况下,两个看起来和行为非常相似的构造(闭包和函数)会使事情变得更加复杂。至少在 Java 和 JS 中,有多个构造(箭头函数、lambda),这是因为它们是在很晚的时候添加到语言中的,而这些仍然是我在这些语言中不喜欢的东西。最好的解释来自 Yufan Lou 和 zesterer 。我不会将它从我不喜欢的东西中删除,因为我仍然不喜欢这里的复杂性。
特征的隐式实现
我不喜欢隐式实现,因为它更容易被滥用,而且更难理解。你可以在一个文件中定义一个结构体,然后在另一个文件中为该结构体实现一个 trait,这样会让它不那么明显。我更喜欢像 Java 那样,通过意图来实现,这样会更明显,更容易理解。虽然 Rust 的方式并不理想,但绝对比 Golang 更好,Golang 更加间接。
挑剔
最后,有些东西我仍然不喜欢,但我不认为它们是什么大问题。
-
我不明白为什么默认使用黛安 指出了差异的const
when 这个关键字let
是不可变的。它看起来更像是老式常量概念的语法糖。const
存在,这是有道理的。 - 块表达式和隐式返回样式有点容易出错,而且难以习惯,我更喜欢显式返回。而且,在我看来,它的可读性不太好。
- 如果你读过我之前关于 Go 的文章,你可能知道我不太喜欢结构体。Rust 中的结构体与 Golang 中的结构体非常相似。所以就像在 Go 中一样,很容易实现难以理解的结构体。但幸运的是,Rust 中的结构体看起来比 Go 好得多,因为它有函数,可以使用展开运算符、简写语法等等。此外,你可以创建元组形式的结构体。Rust 中的结构体更像 Java 的 POJO。如果在结构体中添加可选字段更容易实现,我会把这个移到“喜欢”部分。目前,你必须将内容包装在枚举中
Optional
才能做到这一点。还有生命周期 :( - 鉴于字符串是最常用的数据类型,如果能有一种更简单的方法来处理字符串(就像在 Golang 中一样),而不是使用 Vector 类型来处理可变字符串或使用切片类型来处理不可变字符串字面量,那就太好了。这会让代码变得比实际需要的更冗长。这更像是一种妥协,因为 Rust 没有垃圾回收机制,并且使用所有权的概念来管理内存。https ://doc.rust-lang.org/rust-by-example/std/str.html -编辑:在与robertorojasr讨论评论后,我已将这一点从“不喜欢”改为“挑剔”。
结论
我喜欢那些更注重简洁性,而不是花哨的语法糖和复杂功能的编程语言。在我的文章“我对 Golang 的思考”中,我解释了为什么我认为 Golang 对我来说太简单了。另一方面,Rust 则倾向于另一个极端。它虽然不像 Scala 那么复杂,但也不像 Go 那么简单。所以它介于两者之间,虽然不是最佳平衡点,但几乎接近最佳平衡点,可能与 JavaScript 的水平相当接近。
所以总的来说,Rust 中我喜欢的地方比不喜欢的地方多,这正是我对一门优秀编程语言的期望。另外,请记住,我并不是说 Rust 应该做任何不同的事情,或者有更好的方法来实现我所抱怨的事情。我只是说我不喜欢那些地方,但考虑到它的整体优势,我可以接受。此外,我完全理解为什么其中一些概念是这样的,这些主要是为了关注内存安全和性能而做出的权衡。
但是不要被你看到的东西所迷惑,在我看来,Rust 绝对不是你应该作为你的第一个编程语言开始的东西,因为它底层有很多复杂的概念和结构,但如果你已经熟悉编程,那么在你撞到门几次之后它应该不是问题:P
到目前为止,我可以说,即使没有用 Rust 实现真正的项目,我也更喜欢 Rust 而不是 Golang,并且对于系统编程用例和高性能要求,我可能会选择 Rust 而不是 Go。
注:在使用 Rust 之后,我的一些观点发生了改变。可以看看我关于此主题的新帖子。
如果您喜欢这篇文章,请点赞或留言。
封面图片来源:图片来自Link Clark、Rust 团队,遵循Creative Commons Attribution Share-Alike License v3.0 许可。
文章来源:https://dev.to/deepu105/my-first-impressions-of-rust-1a8o