Rust — 语言基础

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 关键字来声明常量。constlet 仍有区别:

  • 常量 const 就不需要加 mut 了;
  • 编译器会对常量做特殊优化:编译完成后就已经确定它的值;
  • const 必须标注类型。

变量遮蔽(shadowing) #

Rust 允许使用 let 声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的。

数据类型 #

Rust 的数据类型主要分为基本类型和复合类型,此外还有指针类型等。

  1. 基本数据类型:

    • 布尔型:bool,表示真或假。
    • 整数型:i8i16i32i64i128,分别表示有符号 8 位、16 位、32 位、64 位、128 位整数;u8u16u32u64u128,分别表示无符号 8 位、16 位、32 位、64 位、128 位整数。
    • 浮点型:f32f64,分别表示单精度浮点数和双精度浮点数。
    • 字符型:char,表示 Unicode 字符。
  2. 复合数据类型:

    • 数组:[T; N],表示由 N 个类型为 T 的元素组成的数组。
    • 元组:(T1, T2, ..., Tn),表示由 n 个不同类型的值组成的有序集合。
    • 结构体:struct,表示由多个具有不同类型的命名字段组成的数据结构。
    • 枚举:enum,表示由多个具有不同类型的变体构成的类型。
  3. 指针类型:

    • 引用:&T,表示对 T 类型的引用。
    • 可变引用:&mut T,表示对 T 类型的可变引用。
    • 智能指针:Box<T>,表示对 T 类型的堆分配的指针。
  4. 其他类型:

    • 字符串类型:str,表示字符串切片类型。
    • 动态字符串类型:String,表示可变的字符串类型。
    • 切片类型:[T],表示对 T 类型的切片。
    • 函数类型:fn,表示函数类型。

有些情况下编译器可以推导,但是复杂的情况也无法推导,需要明确指定类型。

整数类型 #

默认使用 i32

isizeusize 类型使用场景

注意整型溢出问题。

浮点类型 #

默认使用 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),它们是一种底层的机制,提供了对内存的直接访问。与引用不同,裸指针没有所有权或生命周期的概念,因此需要手动管理它们的生命周期和所有权,并且需要谨慎处理空指针和悬垂指针等问题,以避免安全漏洞和未定义行为的发生。

下面是一些使用裸指针的常见情况:

  1. 使用 *const T*mut T 类型的裸指针来访问内存中的数据。这些指针可以通过解引用运算符 * 来访问它们指向的数据。例如:

    let x = 42;
    let ptr: *const i32 = &x as *const i32; // 将 `x` 的地址转换为 `*const i32` 类型的指针
    let value = unsafe { *ptr }; // 通过解引用运算符访问指针指向的数据
    

    注意,在使用裸指针访问数据时,需要使用 unsafe 关键字来标记这段代码的上下文是不安全的。这是因为编译器无法保证指针的有效性和正确性,因此需要程序员自行承担风险和责任。

  2. 使用 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 关键字来标记上下文。

  3. 使用 std::ptr 模块提供的函数来操作指针,例如 std::ptr::null 函数可以返回一个空指针,std::ptr::read 函数可以从指针中读取数据,std::ptr::write 函数可以将数据写入指针中,std::ptr::copy 函数可以复制指针所指向的数据等。这些函数也需要使用 unsafe 关键字来标记上下文。

虽然裸指针在一些场景下非常有用,但是使用不当会导致程序出现安全漏洞和未定义行为。因此,在编写 Rust 代码时,应尽量避免使用裸指针,而是使用引用和安全的高级抽象来访问内存和处理数据。

指针和引用的区别 #

在 Rust 中,引用和指针是有区别的。引用和指针都可以用于传递参数,但是它们在语义上有很大的不同。

首先,指针是一种底层的机制,它提供了对内存的直接访问,并且可以进行指针运算等操作。指针可能会存在空指针和悬垂指针等危险情况,需要手动管理内存的生命周期和所有权。

相比之下,Rust 的引用是一种高级抽象,它提供了一种安全且简洁的方式来访问内存,同时也保证了内存的安全性和正确性。引用的生命周期和所有权是由 Rust 的借用检查器自动管理的,因此不会出现空引用或悬垂引用等危险情况。

另外,Rust 的引用还可以分为可变引用和不可变引用两种类型,从而允许对数据进行读写和只读访问的区分。这种类型系统的限制能够防止数据竞争和并发访问等问题,从而提高了程序的可靠性和稳定性。

综上所述,Rust 的引用比指针更加安全和高级,同时也更加方便和易于使用。它提供了一种安全且高效的方式来访问内存,并且避免了手动管理内存生命周期和所有权等问题。

总结 #

Rust 很多地方都是显式表达,非常明确,好处就是清晰严谨,带来的问题就是理解成本很高,写代码时可能费头发,但是运行时应该会非常理想。

本文共 4820 字,上次修改于 Apr 25, 2023
相关标签: Rust, 内存管理