Rust 是一种很棒的语言,它结合了系统编程的强大功能和现代语言特性,可用于 Web 开发和区块链。
然而,在学习 Rust 时,一个阻碍是熟悉它的语法。
在本文中,我会尽力提供一些示例,让你对这些语法感到舒适。
入门:变量和类型
让我们从基础开始:变量。
默认情况下,Rust 变量是不可变的。如果你习惯了像 Python 或 JavaScript 这样的允许变量改变的语言,这可能听起来很奇怪。
fn main() { let x = 5; // x 默认是不可变的 // x = 6; // 取消注释会导致编译错误 let mut y = 5; // y 是可变的 y = 6; // 这里没有问题 }
注意 let
关键字吗?这是你在 Rust 中声明变量的方式。如果你想改变一个变量,可以用 mut
关键字使其可变。
类型注释
Rust 拥有出色的类型推断:编译器通常知道你的变量类型。
但有时,你需要自己指定类型:
// 这里,我们明确地表示 z 是一个 32 位整数 let z: i32 = 10;
Rust 的类型系统是它的一个巨大优势,所以尽早熟悉它是值得的。
函数
如果你使用过其他语言,Rust 中的函数看起来会很熟悉。但有一些语法上的怪癖需要注意。
fn add(a: i32, b: i32) -> i32 { a + b // 没有分号表示这是返回值 }
注意我们使用 ->
来定义函数的返回类型。另外,这里没有 return
关键字;如果你省略分号,Rust 默认返回最后一个表达式。
一旦习惯了,这还是不错的。
所有权和借用
好了,这里事情变得有趣了。Rust 的所有权模型让它与众不同,但一开始可能会有些棘手。
让我们看另一个例子。
所有权
在 Rust 中,每个值都有一个变量作为它的所有者。
当所有者超出作用域时,值就被丢弃。这是 Rust 避免内存泄漏的方式。
fn main() { let s1 = String::from("hello"); let s2 = s1; // String 的所有权被移动到 s2,s1 现在无效 // println!("{}", s1); // 这会导致编译时错误 }
在这里,s1 在被移动到 s2 之后不再拥有 String 的所有权。
如果你在那之后尝试使用 s1,Rust 不会让你这样做。
借用
但如果你想使用一个值而不获取它的所有权呢?
这就是借用的用武之地。你可以通过使用引用来借用一个值:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // 我们在这里借用了 s1 println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
在这个例子中,&s1
是 s1 的 引用
。calculate_length 函数临时借用了 s1 而不获取所有权。函数完成后,s1 仍然有效。这非常酷。
生命周期
生命周期是 Rust 用来追踪引用有效时间的方式。
一开始可能会有些混乱,但它们对于安全内存管理至关重要。
让我们看看一个非常基本的例子,来熟悉一下。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
这里,'a
是一个生命周期参数。这意味着引用 x 和 y 的生命周期至少要和返回值一样长。这确保我们不会返回一个已经被丢弃的引用。
模式匹配
Rust 的 match
语句就像一个强大的 switch
。它是我最喜欢的语言部分之一,因为它非常强大和表达力丰富。
fn main() { let number = 7; match number { 1 => println!("One!"), 2 => println!("Two!"), 3 | 4 | 5 => println!("Three, Four, or Five!"), 6..=10 => println!("Between Six and Ten!"), _ => println!("Anything else!"), } }
match
语句将一个值与多个模式进行比较,并运行第一个匹配模式的代码。_
是一个捕获所有模式,当你想处理任何未明确匹配的情况时非常有用。
用模式匹配进行解构
你还可以用 match
来解构复杂的数据类型,比如元组或枚举。
fn main() { let pair = (2, 5); match pair { (0, y) => println!("First is zero and y is {}", y), (x, 0) => println!("x is {} and second is zero", x), _ => println!("No zeroes here!"), } }
这只是冰山一角。
match
可以做更多的事情,但这应该给你一个坚实的基础。
错误处理
Rust 没有异常。相反,它使用 Result
和 Option
类型来处理错误。最初可能感觉有点冗长,但它比未检查的异常更安全。
fn main() { let result = divide(10, 2); match result { Ok(v) => println!("Result is {}", v), Err(e) => println!("Error: {}", e), } } fn divide(a: i32, b: i32) -> Result<i32, String> { if b == 0 { Err(String::from("Division by zero")) } else { Ok(a / b) } }
这里,Result
是一个可以是 Ok
(成功)或 Err
(错误)的类型。这强制你处理成功和失败的情况,这对于编写健壮的代码非常有用。
?
操作符
为了使错误处理更方便,Rust 提供了 ?
操作符。它是传播错误的简写。
fn main() -> Result<(), String> { let result = divide(10, 0)?; // 如果 divide 返回 Err,它会立即从函数返回 println!("Result is {}", result); Ok(()) }
这是 Rust 的说法,“如果有错误,就返回它。”
高级语法:Traits、泛型等
现在我们已经掌握了基础知识,让我们深入探讨更高级的主题。
Traits:接口(有点像)
Traits 有点像其他语言中的接口。它们定义了不同类型可以实现的共享行为。
trait Summary { fn summarize(&self) -> String; } struct Article { title: String, content: String, } impl Summary for Article { fn summarize(&self) -> String { format!("{}: {}", self.title, self.content) } }
在这里,我们定义并实现了 Article 结构体的 Summary
trait。现在,任何 Article 都可以被总结。Traits 对于编写通用和可重用的代码非常强大。
泛型:编写灵活的代码
泛型让你可以编写适用于任何数据类型的函数和类型。
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest }
这个函数适用于任何可以比较的类型 T。PartialOrd
部分是 trait 约束,这意味着 T 必须实现 PartialOrd
trait,允许进行顺序比较。
实用技巧:编写惯用的 Rust
-
使用
rustfmt
:Rust 有一个内置的格式化工具,可以保持你的代码整洁。只需在你的项目目录中运行cargo fmt
。 -
使用 Rust Analyzer:这个强大的 IDE 扩展提供代码补全、重构等功能。它就像一个精通 Rust 的助手。
-
Clippy:这是一个 Rust 的 linter,捕捉常见错误并提供改进建议。运行
cargo clippy
看看它发现了什么。
结论
这篇简短的文章是为了让你对 Rust 更加熟悉。
我有一系列关于这些具体主题的免费视频。
你可以在这里查看。