Rust 是一门注重安全(safety)、速度(speed)和并发(concurrency)的现代系统编程语言。Rust 通过内存安全来实现以上目标,但不使用垃圾回收机制(garbage collection, GC)。
Rust 某种程度上被看作是替换 C++ 的一种语言,有时你会发现 Rust 或多或少会有一些 C++ 的影子或思想。
Hello World #
fn main() {
let name = "World";
println!("Hello, {}!", name);
}
感叹号,异常处理,Don’t Panic 哲学。
Rust 使用 {}
来作为格式化输出占位符,其它语言可能使用的是 %s
,%d
,%p
等,由于 println!
会自动推导出具体的类型,因此无需手动指定。
Rust 使用 ;
分号结尾每行代码。
变量 #
使用 let
声明不可变的变量,可以指定类型,也可以省略,编译器会推断。
let a;
a = "hello world";
// 或者
let a = "hello world";
如果希望指定变量是可变的,需要在 let
后面再加上 mut
关键字。
变量绑定 #
同样的写法,在其他语言中叫变量赋值,通俗易懂,但是在 Rust 中叫变量绑定。绑定强调内存对象的所有权,就是把一个对象绑定给一个变量。
关键字 let
强调变量不可变,变量不可变在其他语言中也很常见,比如 Python 中的字符串就是不可变类型,但是我们仍然可以给一个变量赋值不同的字符串,因为 Python 解释器会帮我们创建一个新的对象赋值给变量,但是,在 Rust 中这样是不允许的,我们不能给一个变量重复赋值多次,这也就是叫绑定的意义了。
比如下面这段代码,编译器就会报错 cannot assign twice to immutable variable
。
fn main() {
let name = "World";
println!("Hello, {}!", name);
name = "Foo";
println!("Hello, {}!", name);
}
这种错误是为了避免无法预期的错误发生在我们的变量上:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。
如果知道变量在未来需要改变,可以加上 mut
关键字,这样就显示声明了变量的可变性,这不就比 Python 高级了。
解构 #
很多语言都有解构,Rust 的语法可能更独特一些。
let (a, mut b): (bool,bool) = (true, false);
println!("a = {:?}, b = {:?}", a, b);
不使用的变量 #
Rust 中如果创建了一个变量但是又不使用它,或是计划在未来使用,Rust 编译器会给没有使用的变量报错。如果不希望报错,可以在变量前面加一个下划线 _xxx
显示声明。
常量 #
Rust 也有 const
关键字来声明常量。const
和 let
仍有区别:
- 常量
const
就不需要加mut
了; - 编译器会对常量做特殊优化:编译完成后就已经确定它的值;
const
必须标注类型。
变量遮蔽(shadowing) #
Rust 允许使用 let
声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的。
数据类型 #
Rust 的数据类型主要分为基本类型和复合类型,此外还有指针类型等。
基本数据类型:
- 布尔型:
bool
,表示真或假。 - 整数型:
i8
、i16
、i32
、i64
、i128
,分别表示有符号 8 位、16 位、32 位、64 位、128 位整数;u8
、u16
、u32
、u64
、u128
,分别表示无符号 8 位、16 位、32 位、64 位、128 位整数。 - 浮点型:
f32
、f64
,分别表示单精度浮点数和双精度浮点数。 - 字符型:
char
,表示 Unicode 字符。
- 布尔型:
复合数据类型:
- 数组:
[T; N]
,表示由 N 个类型为 T 的元素组成的数组。 - 元组:
(T1, T2, ..., Tn)
,表示由 n 个不同类型的值组成的有序集合。 - 结构体:
struct
,表示由多个具有不同类型的命名字段组成的数据结构。 - 枚举:
enum
,表示由多个具有不同类型的变体构成的类型。
- 数组:
指针类型:
- 引用:
&T
,表示对 T 类型的引用。 - 可变引用:
&mut T
,表示对 T 类型的可变引用。 - 智能指针:
Box<T>
,表示对 T 类型的堆分配的指针。
- 引用:
其他类型:
- 字符串类型:
str
,表示字符串切片类型。 - 动态字符串类型:
String
,表示可变的字符串类型。 - 切片类型:
[T]
,表示对 T 类型的切片。 - 函数类型:
fn
,表示函数类型。
- 字符串类型:
有些情况下编译器可以推导,但是复杂的情况也无法推导,需要明确指定类型。
整数类型 #
默认使用 i32
isize
和 usize
类型使用场景
注意整型溢出问题。
浮点类型 #
默认使用 f64
注意避免比较浮点数的相等性。
浮点数计算起来不精确。
总之需要格外小心浮点数的使用。
NaN #
所有跟 NaN
交互的操作,都会返回一个 NaN
,而且 NaN
不能用来比较。可以使用 is_nan()
方法判断。
序列 #
let a = 1..5; // 1 到 4,不包含 5
for i in 'a'..='z' {
println!("{}",i);
}
字符串(str) #
字符串使用双引号加内容来表示。
字符(char) #
所有的 Unicode
值都可以作为 Rust 字符,字符使用单引号表示,占用 4 个字节。
布尔类型 #
fn main() {
let t = true;
if t {
println!("人生苦短,");
}
}
单元类型 #
在 Rust 编程语言中,单元类型(Unit Type)是一种特殊的数据类型,通常表示为 ()
。它只有一个可能的取值,也就是一个空元组。空元组表示不包含任何值或信息,类似于其他编程语言中的 void
类型。
在 Rust 中,单元类型通常用于表示函数没有返回值,或者在其他上下文中表示某些操作只是执行了某些副作用,而不需要返回任何值。例如,以下是一个返回单元类型的函数的示例:
fn print_hello() {
println!("Hello");
}
在上面的代码中,print_hello
函数并没有返回任何值,它的返回类型是单元类型 ()
。当调用 print_hello
函数时,它只是在屏幕上打印了一个消息,而不返回任何值。
总之,单元类型在 Rust 中是一个表示不包含任何值或信息的特殊类型,用于表示函数没有返回值或某些操作只是执行了副作用。
枚举(enum) #
使用关键字 enum
创建一个枚举。
控制流 #
位运算 #
if else #
for in #
while #
loop #
loop
就是一个简单的无限循环,你可以在内部实现逻辑通过 break
关键字来控制循环何时结束。
函数 #
有时可以省略 return
fn add(i: i32, j: i32) -> i32 {
i + j
}
函数名规范使用蛇形命名法,例如 fn add_two() -> {}
。
语句和表达式 #
Rust 需要明确区分语句(Statement)和表达式(Expression)的区别,在处理一些 Rust 的报错时非常有用。
语句是一个具体的操作,但是没有返回值,表达式会进行求值,并需要返回值,不能包含分号,如果表达式不返回任何值,会隐式地返回一个()
。
(至此每行代码加不加分号好像都是一个需要思考的问题了。。)
永不返回(!) #
这种叫发散函数。
传值还是传引用 #
在 Rust 中,函数的参数可以通过传值(pass by value)或者传引用(pass by reference)的方式进行传递,具体取决于参数的类型和函数的定义。
如果一个参数的类型实现了 Copy
trait,那么它会被按值传递,这意味着它的值会被复制到函数的栈帧中,并且对参数值的任何修改都不会影响到原始的值。例如,对于 i32
类型的参数,它就是一个 Copy
类型,因此会被按值传递。
如果一个参数的类型没有实现 Copy
trait,那么它会被按引用传递,这意味着函数接收的是指向参数值的引用,而不是实际的值。这种方式允许函数直接访问和修改原始值,而不是复制一份副本,因此可以更高效地处理大型数据结构。例如,对于字符串类型 String
,它没有实现 Copy
trait,因此会被按引用传递。
此外,还可以使用 &mut
关键字来表示可变引用,从而允许函数修改原始值。例如,一个函数可以接收一个可变引用来修改一个数组的元素,而不需要返回一个新的数组。
导入包 #
关键字 use
标准库 #
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}");
}
三方包 #
依赖 Cargo,需要在 Cargo 项目的 Cargo.toml
中指定三方包的版本。
[dependencies]
rand = "0.8.5"
面向对象 #
Rust 在面向对象的设计上并不像 Java 或 C++ 那样严格,相反,Rust 和 Golang 类似,Rust 使用结构体和枚举类型来定义数据结构,并通过 trait(类似于接口)实现多态性,这使得 Rust 也可以实现类似于面向对象编程的封装、继承和多态等概念。
所有权(Ownership) #
Rust 在垃圾回收机制和手动管理内存分配和释放的内存管理方案中,Rust 都没有选择,而是选择了通过所有权来管理的第三种方案,这种方案的检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失,妙哉。
所有权模型规定每个值都有一个所有者(owner),这个所有者负责分配和释放这个值所占用的内存。当所有者超出作用域时,这个值将自动被释放。
移动(Move) #
克隆(深拷贝) #
拷贝(浅拷贝) #
如果一个类型拥有 Copy
特征,一个旧的变量在被赋值给其他变量后仍然可用。Rust 中任何基本类型的组合都可以 Copy
,不需要分配内存或某种形式资源的类型是可以 Copy
的。
引用和借用 #
在Rust中,引用(Reference)和借用(Borrowing)都是为了在不传递所有权的情况下访问数据。
引用是指向某个值的指针,而借用是在使用值时向其请求临时访问权,这样就可以在不转移所有权的情况下访问该值。简单来说,引用是指向一个值的指针,而借用是借用一个值的访问权。
在 Rust 中,引用使用 &
符号来表示,而借用使用 &mut
符号表示。引用是不可变的,意味着无法修改指向的值,而借用则可以是可变的,也就是可以修改指向的值。
例如,以下是使用引用和借用的示例:
fn main() {
let mut x = 5;
let y = &x; // 创建一个不可变引用
let z = &mut x; // 创建一个可变借用
*z += 1; // 修改可变借用的值
println!("x = {}", x); // 输出 "x = 6"
}
在这个例子中,我们创建了一个变量 x
,然后创建了一个不可变引用 y
和一个可变借用 z
。我们可以使用 *z
来修改 x
的值,因为我们拥有 x
的可变引用。最后,我们打印出 x
的值,它现在应该是 6
。
需要注意的是,在 Rust 中,引用和借用有一些严格的规则,以确保在编译时检测出所有可能的内存错误。因此,需要特别小心地使用引用和借用。
泛型 #
生命周期 #
指针 #
在 Rust 中,指针被称为裸指针(raw pointer),它们是一种底层的机制,提供了对内存的直接访问。与引用不同,裸指针没有所有权或生命周期的概念,因此需要手动管理它们的生命周期和所有权,并且需要谨慎处理空指针和悬垂指针等问题,以避免安全漏洞和未定义行为的发生。
下面是一些使用裸指针的常见情况:
使用
*const T
或*mut T
类型的裸指针来访问内存中的数据。这些指针可以通过解引用运算符*
来访问它们指向的数据。例如:let x = 42; let ptr: *const i32 = &x as *const i32; // 将 `x` 的地址转换为 `*const i32` 类型的指针 let value = unsafe { *ptr }; // 通过解引用运算符访问指针指向的数据
注意,在使用裸指针访问数据时,需要使用
unsafe
关键字来标记这段代码的上下文是不安全的。这是因为编译器无法保证指针的有效性和正确性,因此需要程序员自行承担风险和责任。使用
std::mem::transmute
函数将一个指针转换为另一个指针。这个函数可以用于将不同类型的指针进行类型转换,或者将指针转换为整数类型等。例如:let x = 42; let ptr1: *const i32 = &x as *const i32; let ptr2: *const u8 = unsafe { std::mem::transmute(ptr1) }; // 将 `ptr1` 转换为 `*const u8` 类型的指针
注意,这个函数也需要使用
unsafe
关键字来标记上下文。使用
std::ptr
模块提供的函数来操作指针,例如std::ptr::null
函数可以返回一个空指针,std::ptr::read
函数可以从指针中读取数据,std::ptr::write
函数可以将数据写入指针中,std::ptr::copy
函数可以复制指针所指向的数据等。这些函数也需要使用unsafe
关键字来标记上下文。
虽然裸指针在一些场景下非常有用,但是使用不当会导致程序出现安全漏洞和未定义行为。因此,在编写 Rust 代码时,应尽量避免使用裸指针,而是使用引用和安全的高级抽象来访问内存和处理数据。
指针和引用的区别 #
在 Rust 中,引用和指针是有区别的。引用和指针都可以用于传递参数,但是它们在语义上有很大的不同。
首先,指针是一种底层的机制,它提供了对内存的直接访问,并且可以进行指针运算等操作。指针可能会存在空指针和悬垂指针等危险情况,需要手动管理内存的生命周期和所有权。
相比之下,Rust 的引用是一种高级抽象,它提供了一种安全且简洁的方式来访问内存,同时也保证了内存的安全性和正确性。引用的生命周期和所有权是由 Rust 的借用检查器自动管理的,因此不会出现空引用或悬垂引用等危险情况。
另外,Rust 的引用还可以分为可变引用和不可变引用两种类型,从而允许对数据进行读写和只读访问的区分。这种类型系统的限制能够防止数据竞争和并发访问等问题,从而提高了程序的可靠性和稳定性。
综上所述,Rust 的引用比指针更加安全和高级,同时也更加方便和易于使用。它提供了一种安全且高效的方式来访问内存,并且避免了手动管理内存生命周期和所有权等问题。
总结 #
Rust 很多地方都是显式表达,非常明确,好处就是清晰严谨,带来的问题就是理解成本很高,写代码时可能费头发,但是运行时应该会非常理想。