Rust代码高亮还没支持,后面会修复这个问题
因为是速通,不能涵盖到语言所有特性,但是保证能写出能跑的代码。详细教程可以看Rust官方出的The Rust Programming Language。
阅读建议
有一定的C/C++
基础,了解 指针,Struct,OOP的基本概念以及用法。边读边写代码。
包管理
类似于npm
, pipenv
Rust 用 Cargo
进行包管理,常用指令如下:
新建项目:
cargo new
运行完后,会在项目文件根目录生成一个TOML
文件,用过npm的话应该会很熟悉:
[package]
name = "ProjectName"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
运行项目:
cargo new
过后会生成src/main.rs
,main.rs
是项目的入口文件,里面会初始化自带一个Hello World
程序,现在咱尝试运行它
cargo build # 编译
当然你也能用rustc
进行古法编译。具体见文档
编译后的binary在./target/debug/
目录下。
如果不想这么麻烦,cargo还提供一个指令能一键运行:
cargo run # 直接运行
如果你项目已经写完了准备发布,可以用cargo build --release
来优化编译,缺点是比普通build慢。
Debug:
cargo check # 查bug
这个指令因为跳过了编译成可执行文件的步骤,所以比cargo run/build
快,debug的时候用很合适
安装依赖:
这里先介绍一个额外的概念crate
,crate
是 Rust 源代码文件的集合。crate
可以是二进制的,也就是一个可执行文件。crate
也可以是一个库(library),其中包含了用于其他程序的代码,不能单独执行(类似 .so
, .dll
, .h
)。
比如现在我们要装一个生成随机数的library crate:,咱先编辑Cargo.toml
加一个依赖:
[dependencies]
rand = "0.8.5"
然后重新用cargo build
重新编译项目,done!
更新依赖:
更新项目中的所有依赖:
cargo update
为什么用cargo:
对于网上任意一个rust项目,如果你想在本地运行,现在不需要手动装依赖、解决版本冲突...只需要三个命令直接秒了:
git clone example.org/someproject
cd someproject
cargo build
基本语法
咱写个基本的猜数字游戏(Guessing Game)来快速过一遍基本语法,这个游戏用python写出来长这样:
import random
magic_number = random.randint(1, 10)
while True:
guess = input("Your Guess: ")
if guess < magic_number:
print("too small")
elif guess > magic_number:
print("too large")
else:
print("you win")
break
接下来我们用Rust实现同样的功能,我们先实现获取用户输入:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
相信大家看到如上代码应该不会特别陌生,但有两个地方需要着重说一下:
let mut guess = String::new(); // 定义变量
在Rust中,变量初始情况都是immutable的(类似const),如果想定义变量就得像如上一样加个mut
关键字。
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
这段代码通过stdin获取用户输入,并传递给&mut guess
这个reference,没错,Rust里的&
也表示Reference。
错误处理:
read_line(&mut guess)
的返回值是Result
,Result
是一个enum
= {Ok, Err}
,Ok
代表指令成功执行,Err
表示出错。除此之外Result
里还有一个方法expect
,如果 Result
的这个实例是 Ok
,expect 将获取 Ok 所保存的返回值并将该值返回给您。在本例中,该值是用户输入中的字节数;如果是Err
,程序就会crash然后打印你给expect(msg)
方法传的信息。
println!
类似于printf
,支持如下语法:
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
现在咱的猜谜游戏能接收用户输入了,下一步生成magic number:
let secret_number = rand::thread_rng().gen_range(1..=100);
gen_range
方法接受一个范围表达式(range expression)作为参数,并生成在该范围内的随机数。这里使用的范围表达式类型采用了 start..=end 的形式,包含了下界和上界,因此我们需要指定 1..=100 来请求一个介于 1 和 100 之间的数字。
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
这段代码虽然看着陌生,但其实很好理解。guess.cmp(&secret_number)
比较guess
和secret_number
并返回enum Ordering
。match
类似其他语言的switch
,block内讨论三个不同case。
现在感觉都没毛病了,但是如果run代码的话会报错。
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected struct `String`, found integer
| |
| arguments to this function are incorrect
从如上报错信息可以看出是数据类型对不上的原因。Rust是一个强类型语言,guess
是String
,而secret_number
是i32
既是int32
,所以不能放在一起比大小,正确做法是用parse
给guess
进行类型转换
let guess: u32 = guess.trim().parse().expect("Please type a number!");
字符串上的 parse
方法能将一个字符串转换为另一种类型。在这里,我们使用它将一个字符串转换为一个数字类型(因为let guess: u32
这里指定了类型)。
现在一个基本的猜数字游戏已经做好了,为了能让玩家能够多次尝试,我们还得写个for循环
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
到这里程序就写完了!接下来开始优化程序,比如处理异常输入:
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
其中Ok(num)/Err(_)
类似于解包操作,获取enum
里面带的message,Err(_)
的_
和Python一样,代表这个解包的数据不重要。
最终程序
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Ownership
说到Rust,大家想到的第一个词应该改是safety,但为啥它很安全呢,原因就是因为它Ownership概念引入,先来看定义:
Ownership is a set of rules that govern how a Rust program manages memory
Ownership 是一系列用于Rust做内存管理的规则
在C
里面,我们用malloc/calloc/realloc
等命令声明内存,当内存不在被使用时,我们需要手动free
掉。如果不free
就会内存溢出。同样在管理heap上的数据结构(比如实现一个linklist),稍有不注意就会segfault... 为了解决这个问题,Rust引入了Ownership这个概念,让内存错误不再是个问题。
References and Borrowing
先来看代码:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
没错,这段代码和C/C++
很像,用&s1
表示变量s1
的reference。在Rust里,每当你给方法传递参数,参数就属于(owned by)这个方法了,当方法运行结束后就会自动把这个参数drop掉。所以如果你这么写的话:
let s1 = String::from("hello");
let len = calculate_length(s1);
println!("{s1}");
fn calculate_length(s: String) {
// do something
}
程序就会报错,为什么呢,因为let len = calculate_length(s1)
已经把s1
的ownership给了calculate_length
这个方法,当这个方法运行结束后,s1
就会被自动回收,这个时候如果再println!("{s1}")
就会找不到s1
这个变量。
让我们回到最开始的代码,如果我们用reference的话calculate_length(&s1);
就会没上述问题,因为Rust管这叫借(borrow),当方法去借一个变量的话就不会take ownership了。
是reference,但又不完全是:
不同于C
,Rust里:传入方法的reference在普通情况下不能被方法修改,比如如下代码就会报错:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
为什么说是在普通情况下呢,因为就像变量能被定义为mut
,reference也可以被定义为可变的:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
在上述代码中,我们除了把s
变成mut
,还把reference也变成&mut s
。这个时候传入change
就能更改s
的值了。
mut reference的限制:
对于mutable reference,Rust对其有一个限制:对于一个变量,如果你已经有一个关于它的mut reference,在同一个作用域(scope)里就不能再有第二个了,比如如下代码就违反了这个限制,代码会报错:
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
Rust之所以这么设计是为了能在如下情况避免Data Race:
- 两个或更多指针同时访问相同的数据
- 至少有一个指针正在用于写入数据。
- 没有使用机制来同步对数据的访问。
上文说到: 同一个scope里就不能再有第二个了...
,那如果不在同一个scope还能能定义多个mut reference吗,答案是肯定的:
// 不会报错
let mut s = String::from("hello");
{
let r1 = &mut s;
} // 在这里,r1的作用域结束了,因此我们可以创建一个新的引用。
let r2 = &mut s;
Rust同样也不允许统一变量的mut reference和immut reference同时出现,比如如下代码会报错:
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
let r3 = &mut s; // 寄!
println!("{}, {}, and {}", r1, r2, r3);
如果把上述代码改一下,代码还会报错吗?
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);
答案是不会的!为啥呢,因为编译器发现在执行完println!("{} and {}", r1, r2);
后r1, r2
就再没出现过,所以它就能足够聪明得把r1, r2
在这行过后drop掉。最后跑到let r3
这行的时候r1,r2
已经影响不到r3
啦。
ownership转移:
当方法返回的变量最后return出去的话,这个变量的ownership就会转移到call它的地方。
fn no_dangle() -> String {
let s = String::from("hello");
s
}
fn main() {
let result = no_dangle();
println!("{}", result);
}
Slice
现在你准备写一个功能:给一个string,需要返回这个string的第一个单词,你可以这么写:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word = 5
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
看着没毛病,但有个小问题,如果你后面对s
进行了修改,之前定义的word
就会失去意义
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 清空s
// 靠,word还是等于5
}
如果是写小项目的话这样的问题还很容易发现,但如果是大项目的话就说不准了。这个时候用Rust的Slice结构就能很好的解决这个问题:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
这么写的话word
就会从一个赋值过后永远不会变的量变成了reference,也就是说能实时根据s
变化而变化了。现在问题又来了,如果在main
最后call s.clear()
会怎样?
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
word.clear();
println!("the first word is: {}", word);
}
答案是会报错,因为clear()
这个方法的实现需要用到word
的mut reference,然而之前已经定义了word
这个immutable reference。
其他类型的Slice:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
Struct
像很多语言,Rust也有定义Struct
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// 必须是mutable,Rust不允许只指定部分元素为mutable
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
在build_user
里面,因为 username, email
的parameter name和field name一样,所以方法可以简写成:
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
类似JavaScript, Rust也支持解包操作,比如如上代码你已经定义了一个user1
,如果还想定义user2
且user2
只有email
字段和user1.email
不一样的话就能简写成:
fn main() {
// --snip--
let user2 = User {
email: String::from("[email protected]"),
..user1
};
}
Tuple Struct:
比如你想用C
实现对一个Pixel的定义,可能会这么写:
struct Color {
int32_t r;
int32_t g;
int32_t b;
};
但其实r,g,b
压根儿没必要定义出来,因为大伙都知道Pixel是由这个三个元素组成的,这个时候你就能用到Rust的Tuple Struct来简写这样的数据结构了:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Unit-Like Struct
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Debug:
如果我们想打印struct用于Debug的话(不只是struct能用),Rust提供了一个接口能很容易的实现:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
你也能用一个更吊的macro:dgb!()
,和print不同,dgb能获取调试内容的ownership进而支持用户能写出width: dbg!(30 * scale)
的代码,区别2是dgb的调试信息会写到stderr
而不是stdout
。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
Method:
和很多新语言一样,Rust支持给Struct定义方法(Method),这里我们先回顾一下方法和函数的区别
方法 (Method):
- "方法"通常用于面向对象编程(例如Java、C++等),它是与特定对象或类相关联的功能。方法是一种操作,可以在对象上执行,通常可以访问该对象的属性或状态,并且通常与对象的行为相关。
- 方法是面向对象编程中的一部分,它们是类的成员,因此它们可以通过类的实例来调用。
函数 (Function):
- "函数"是一段独立的可执行代码,通常不与特定对象或类相关联。函数可以独立存在,接受输入参数(如果需要)并返回一个结果。函数可以在程序中的任何地方被调用,而不需要依赖于特定的对象。
- 函数是通用的编程概念,可以在不同编程范式(如面向对象、过程式编程等)中使用。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
看到这儿,大家可能会对self.width * self.height
产生疑惑,为啥指针不用->
而是直接.
呢。得益于Rust的automatic referencing and dereferencing 特性,编译器能自动处理转换reference符号。也就是说如下两行是一样的:
p1.distance(&p2);
(&p1).distance(&p2);
关联函数(Associated Function):
有些时候方法的实现并不需要self参数(既 square(&self, size: u32)
)类似静态方法,你能这么写:
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
impl其他用法:
如下写法都是OK的:
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Enum
Rust的枚举类型写法是这样的:
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
当然如上是常规写法,Rust的enum支持你给里面的元素定义不同的数据类型以及赋值:
enum Message {
Quit, // 普通enum元素
Move { x: i32, y: i32 }, // name filed
Write(String), // 字符串类型
ChangeColor(i32, i32, i32), // 三个类型
}
fn main() {
let quit_message = Message::Quit;
let move_message = Message::Move { x: 10, y: 20 };
let write_message = Message::Write(String::from("Hello, Rust!"));
let color_message = Message::ChangeColor(255, 0, 0);
}
除此之外你还可以将各种类型的数据放入枚举的变体中,例如字符串、数值类型或结构体。甚至可以包含另一个枚举!
可选参数Option:
在Rust标准库中,Optional定义为一个枚举类型:
enum Option<T> {
None,
Some(T),
}
类似于C++的泛型,下一篇文章会介绍
使用方法和其他enum一样:
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
这么定义有什么用呢,比如你现在这么写代码就会报错:
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
原因是因为y
之前被定义为一个Optional的数据,如果在let sum = x + y;
之前y
没被定义那不是寄了,所以Rust如此考量是为了安全。
Enum和Match一起使用:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
上述写法是完全OK的但是不优雅,因为None => None
完全没必要写出来,Optional这个enum只有两个field还好,如果enum贼大,但你又只想math其中一小部分就难搞了。所以Rust提供了 Catch-all 的语法(其实就是default):
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // Catch All
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
在Rust里面,Match必须讨论到所有可能性,如下代码就是错的:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
// 没讨论是None的情况
}
}
if let:
这个操作符能简化Match的写法,比如如下代码:
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (),
能简写成:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
看到这里可能有人会问为啥不直接if Some(max) == config_max
,这里千万要注意Some(max)
并不是一个变量,而是解包操作!
结语
相信大家看到这里已经对Rust有了一个初步的理解。然而除了这些内容咱还有很多知识点没能探讨到,比如:泛型,多线程,智能指针,错误处理等等...这些内容我会放在未来的“Rust后日谈”文章探讨,敬请期待!
呃呃