跳转至

Basic Syntax

约 3220 个字 56 行代码 预计阅读时间 11 分钟

1 变量

1.1 命名惯例

条目 惯例
模块 Modules snake_case
类型 Types UpperCamelCase
特征 Traits UpperCamelCase
枚举 Enumerations UpperCamelCase
结构体 Structs UpperCamelCase
函数 Functions snake_case
方法 Methods snake_case
通用构造器 General constructors new or with_more_details
转换构造器 Conversion constructors from_some_other_type
宏 Macros snake_case!
局部变量 Local variables snake_case
静态类型 Statics SCREAMING_SNAKE_CASE
常量 Constants SCREAMING_SNAKE_CASE
类型参数 Type parameters UpperCamelCase,通常使用一个大写字母: T
生命周期 Lifetimes 通常使用小写字母: 'a'de'src

1.2 变量绑定

使用 let 关键字绑定一个变量,变量绑定默认是不可变的,如果需要可变绑定,使用 let mut 关键字如果后边不会改变的变量被声明为 mutable 的话,编译器会给出警告,如果在存在没有使用的变量的话也会给出警告,在变量名字之前加上单下划线就会忽略未使用的变量。

1.3 变量解构

使用 let 关键字还可以进行复杂变量的解构,这也就是从一个相对复杂的变量之中,匹配出这个变量的一部分内容。

1.4 所有权

1.4.1 基本规则

  • Rust 每一个值都被一个变量所拥有,这个变量被称为这个值的所有者;
  • 一个值同时只能被一个变量所拥有;
  • 当所有者离开作用域范围的时候,值就会被丢弃。

作用域和别的编程语言没有区别,可以参考块作用域。

1.4.2 所有权转移

对于以拷贝值的方式完成的赋值,没有所有权的转移。

let x = 5;
let y = x;

这段代码当然是通过拷贝值完成赋值的,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。当然没有所有权的转移。

let s1 = String::from("hello");
let s2 = s1;

String 类型和上面的整数类型很不一样,是由存储在栈中的堆指针、字符串长度和字符串容量共同组成的,总之指向了一个在堆上的空间。let s2 = s1; 这行代码会让 s1 被赋给 s2,而一个值同时只能被一个变量所拥有,所以 Rust 认为 s1 不再有效,在赋值完成之后就马上失效了。

这就是所有权转移,对应的操作是移动而不是拷贝,我们将对这个字符串的所有权从 s1 转移到了 s2s1 不指向任何数据,只有 s2 才有效。

如果不发生所有权转移,那么在两个变量同时同时离开作用域的时候,就会尝试释放相同的内存,这就会出现了二次释放的错误,会导致内存污染。而发生所有权转移后,如果还尝试使用旧的所有者 s1,Rust 就会禁止你使用无效的引用。

如果我们确实需要深度复制 String 堆上的数据,就要使用克隆/深拷贝let s2 = s1.clone(); 会在堆上分配一块新的内存,将 s1 的数据拷贝到新的内存中,这样就不会发生所有权转移了。

与深拷贝相对的是浅拷贝,正常的拷贝其实就是浅拷贝,浅拷贝发生在栈中,效率很高。

Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。

那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,这里可以给出一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy。如下是一些 Copy 的类型:

  • 所有基本类型;
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是
  • 不可变引用 &T但是注意:可变引用 &mut T 是不可以 Copy的

1.4.3 函数传值和返回

函数在传值的时候也会发生移动或者复制,相应的发生所有权的转移:

1
2
3
4
5
6
7
8
9
fn main(){
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s); // error: value borrowed here after move
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}

如果在 takes_ownership 函数后边尝试再使用 s ,就会出现所有权报错,因为 s 对于 String 的所有权在函数传值的时候已经移动给了 some_string,随后 take_owmership 结束的时候,some_string 的值内存被 drop 了,加上原本的 s 的所有权已经移动,所以 s 就无效了。如果函数调用完了还想使用 s,一种方法是传递 s.clone(),另一种方法是返回值:

fn main(){
    let mut s = String::from("hello");
    s = takes_ownership(s);
    println!("{}", s);  // no error
}

fn takes_ownership(some_string: String) -> String {
    println!("{}", some_string);
    some_string
}

这里利用了函数返回的时候也会发生所有权的转移,所以 some_string 的所有权在函数返回的时候又转移给了 ss 又可以使用了。但这里要求 s 是可变的,即便传来传去都是一个 String 类型,但是变量还是发生了变化。

1.5 引用和借用

Rust 也支持类似于使用指针和引用的方式简化传值的流程,利用借用/Borrowing这个概念完成上述目的。借用是指获取变量的引用。

常规引用是一个指针类型,指向了对象存储的内存地址。使用 & 进行引用,使用 * 进行解引用。

1
2
3
let x: i32 = 5;
let y: &i32 = &x;
assert_eq!(*y, x);

使用借用可以进行函数调用,并且维持所有权:

1
2
3
4
5
6
7
8
9
fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);     
    println!("The length of '{}' is {}.", s, len);
}

fn calculate_length(string: &String) -> usize {
    string.len()
}

我们首先创建了 s 的引用并且将其传入,这样,我们通过操纵引用来操纵 s,在函数调用结束的时候,string 离开作用域,但是它并不拥有任何值,所以不会发生什么。

上面创建的都是不可变引用,一直处于只读状态,也就是说,不能在 calculate_length 函数中修改 string 的值,比如 string.push_str("...");,如果需要修改,可以使用 &mut 创建可变引用:

let mut x: i32 = 1;
let y: &mut i32 = &mut x;
1
2
3
4
5
6
7
8
9
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);
}

fn change(string: &mur String) {
    string.push_str(" world!");
}

这里创建的就是可变引用了,可以通过引用来更改变量的值。但是对于可变引用, Rust 存在着一些限制:

  • 在同一个作用域之中,一个数据只能存在一个可变引用;
  • 可变引用和不可变引用不能同时存在;

这样做的目的是避免产生数据竞争,以及防止不可变引用的值被可变引用所改变。数据竞争可由以下行为造成:

  • 两个或更多的指针同时访问同一数据;
  • 至少有一个指针被用来写入数据;
  • 没有同步数据访问的机制。

另外,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

以及如果存在引用,且后面用到了这个引用,则被引用的即使是 mut 的,也不能被修改,例如:

我们也应该注意悬垂引用/Dangling Reference,悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。Rust 编译器会在编译时检测到悬垂引用并且报错。下面是一个悬垂引用的例子:

1
2
3
4
fn dangle() -> &String {
    let s = String::from("hello");
    &s  // this function's return type contains a borrowed value
}       // but there is no value for it to be borrowed from.

1.6 遮蔽

2 类型和值

Rust 的类型可以分为两类:基本类型和符合类型。基本类型意味着其是一个最小化原子类型,无法解构为其他类型,有以下几种:

  • 数值类型:有符号整数 i8, i16, i32, i64, i128, isize, 无符号整数 u8, u16, u32, u64, u128, usize, 浮点数 f32, f64
  • 布尔类型:bool, 字面量为 truefalse
  • 字符类型:char,用单引号括起来的 Unicode 字符;
  • 单元类型:(),只有一个值 ()main 函数的返回值就是 (),这玩意其实就是一个零长度的元组。

复合类型是由其他类型组合而成的,最典型的就是结构体 struct,有以下几种:

  • 字符串
  • 元组
  • 结构体
  • 枚举
  • 数组

2.1 数值类型

序列是生成连续的数值的

要显式处理溢出,可以使用标注怒对原始数字类型提供的这些方法:

  • wrapping_*:在所有模式下都按照补码循环溢出规则处理;
  • overflowing_*:返回该值和一个指示是否发生溢出的布尔值;
  • saturating_*:限定计算后的结果不超过目标类型的最大值或最小值;
  • checked_*:如果溢出则返回 None

2.2 布尔类型

2.3 单元类型

2.4 字符类型

2.5 字符串

字符串大抵分为两种,被硬编码到程序代码之中的不可变的字面量 str,和用堆动态分配内存的可变的 String 类型。在语言级别来说,其实只有一种字符串类型 str,并且一般是以引用形式 &str 出现的,存储的时候是一个指针和字符串长度。String 类型是标准库提供的一个字符串类型,它是一个可变的、可增长的、具有所有权的 UTF-8 编码的字符串类型。

2.5.1 String 和切片

使用 String::from 方法将一个字符串字面量转换为 String 类型,这里的 :: 是一种调用操作符,这里表示调用 String 模块中的 from 方法,由于 String 类型的变量 s 存储在堆上,因此它是动态的,如果 s 是 mut 的,可以通过 s.push_str("...") 来追加字面量:

1
2
3
let mut s = String::from("Hello");
s.push_str(" world!");
println!("{}", s);

基于上面的代码,下面介绍切片:切片就是对 String 类型之中某一部分的引用,类型就是 &str,通过 [begin..off_the_end] 指定引用范围,这个范围是左闭右开的(参考 C++ 的尾后迭代器),这和别的编程语言一样。我们可以认为这个语法其实就是数值类型一节中范围的语法,所以 [begin..=end] 就生成了一个闭区间的范围。

2.6 元组

2.7 结构体

2.8 枚举

2.9 数组

3 语句、函数和控制流

3.1 语句与表达式

简单说来:

  • 带分号的就是一个语句,不带分号的就是一个表达式;
  • 能返回一个值的就是一个表达式,表达式会在求值后返回该值,语句会执行一些操作但是不返回值,let 就是一个经典的语句,只负责绑定变量和值,但是不返回值;
  • 表达式可以是语句的一部分,let a = 1; 就是一个语句,1 其实就是一个表达式;
  • 函数调用是表达式,因为返回了一个值,就算不返回值,就会隐式的返回一个 ()
  • 用花括号括起来的能返回值的代码块是一个表达式,代码块的类型和值就是最后一个表达式的类型和值,如果最后一个表达式是一个分号结尾的语句,那么代码块的类型就是 ()

3.2 函数

1
2
3
fn add(x: i32, y: i32) -> i32 {
    x + y
}

上面是典型的函数定义,下面是几个需要注意的点:

  • 使用关键词 fn 定义一个函数;
  • 必须显示指定参数类型,除了返回单元类型 (),因为这种情况下编译器会自动推断返回类型,都要显式指定返回类型;
  • 中途返回使用 return 关键字,带不带分号都可以;
  • 以语句为最后一行代码的函数,返回值是 ()
  • 永远不返回的函数类型为 !,一般用于一定会抛出 panic 的函数,或者无限循环的函数。
  • 由于函数也返回值,所以函数调用也是一个表达式,可以用在赋值语句的右边。

3.3 控制流

3.4 简单的宏

宏在编译过程中会扩展为 Rust 代码,并且可以接受可变数量的参数。它们以 ! 结尾来进行区分。Rust 标准库包含各种有用的宏。

  • println!(format, ..) 在标准输出中打印一行字符串;
  • format!(format, ..) 的用法与 println! 类似,但并不打印,它以字符串形式返回结果;
  • dbg!(expression) 会记录表达式的值并返回该值;
  • todo!() 用于标记尚未实现的代码段。如果执行该代码段,则会触发 panic;
  • unreachable!() 用于标记无法访问的代码段。如果执行该代码段,则会触发 panic;
  • assert_eq!(left, right) 用于断言两个值是否相等;