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 所有权转移¶
对于以拷贝值的方式完成的赋值,没有所有权的转移。
这段代码当然是通过拷贝值完成赋值的,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。当然没有所有权的转移。
String
类型和上面的整数类型很不一样,是由存储在栈中的堆指针、字符串长度和字符串容量共同组成的,总之指向了一个在堆上的空间。let s2 = s1;
这行代码会让 s1
被赋给 s2
,而一个值同时只能被一个变量所拥有,所以 Rust 认为 s1
不再有效,在赋值完成之后就马上失效了。
这就是所有权转移,对应的操作是移动而不是拷贝,我们将对这个字符串的所有权从 s1
转移到了 s2
。s1
不指向任何数据,只有 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 函数传值和返回¶
函数在传值的时候也会发生移动或者复制,相应的发生所有权的转移:
如果在 takes_ownership
函数后边尝试再使用 s
,就会出现所有权报错,因为 s
对于 String
的所有权在函数传值的时候已经移动给了 some_string
,随后 take_owmership
结束的时候,some_string
的值内存被 drop
了,加上原本的 s
的所有权已经移动,所以 s
就无效了。如果函数调用完了还想使用 s
,一种方法是传递 s.clone()
,另一种方法是返回值:
这里利用了函数返回的时候也会发生所有权的转移,所以 some_string
的所有权在函数返回的时候又转移给了 s
,s
又可以使用了。但这里要求 s
是可变的,即便传来传去都是一个 String
类型,但是变量还是发生了变化。
1.5 引用和借用¶
Rust 也支持类似于使用指针和引用的方式简化传值的流程,利用借用/Borrowing这个概念完成上述目的。借用是指获取变量的引用。
常规引用是一个指针类型,指向了对象存储的内存地址。使用 &
进行引用,使用 *
进行解引用。
使用借用可以进行函数调用,并且维持所有权:
我们首先创建了 s
的引用并且将其传入,这样,我们通过操纵引用来操纵 s
,在函数调用结束的时候,string
离开作用域,但是它并不拥有任何值,所以不会发生什么。
上面创建的都是不可变引用,一直处于只读状态,也就是说,不能在 calculate_length
函数中修改 string
的值,比如 string.push_str("...");
,如果需要修改,可以使用 &mut
创建可变引用:
这里创建的就是可变引用了,可以通过引用来更改变量的值。但是对于可变引用, Rust 存在着一些限制:
- 在同一个作用域之中,一个数据只能存在一个可变引用;
- 可变引用和不可变引用不能同时存在;
这样做的目的是避免产生数据竞争,以及防止不可变引用的值被可变引用所改变。数据竞争可由以下行为造成:
- 两个或更多的指针同时访问同一数据;
- 至少有一个指针被用来写入数据;
- 没有同步数据访问的机制。
另外,引用的作用域 s
从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }
。
以及如果存在引用,且后面用到了这个引用,则被引用的即使是 mut
的,也不能被修改,例如:
我们也应该注意悬垂引用/Dangling Reference,悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。Rust 编译器会在编译时检测到悬垂引用并且报错。下面是一个悬垂引用的例子:
1.6 遮蔽¶
2 类型和值¶
Rust 的类型可以分为两类:基本类型和符合类型。基本类型意味着其是一个最小化原子类型,无法解构为其他类型,有以下几种:
- 数值类型:有符号整数
i8
,i16
,i32
,i64
,i128
,isize
, 无符号整数u8
,u16
,u32
,u64
,u128
,usize
, 浮点数f32
,f64
; - 布尔类型:
bool
, 字面量为true
和false
; - 字符类型:
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("...")
来追加字面量:
基于上面的代码,下面介绍切片:切片就是对 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 函数¶
上面是典型的函数定义,下面是几个需要注意的点:
- 使用关键词
fn
定义一个函数; - 必须显示指定参数类型,除了返回单元类型
()
,因为这种情况下编译器会自动推断返回类型,都要显式指定返回类型; - 中途返回使用
return
关键字,带不带分号都可以; - 以语句为最后一行代码的函数,返回值是
()
; - 永远不返回的函数类型为
!
,一般用于一定会抛出 panic 的函数,或者无限循环的函数。 - 由于函数也返回值,所以函数调用也是一个表达式,可以用在赋值语句的右边。
3.3 控制流¶
3.4 简单的宏¶
宏在编译过程中会扩展为 Rust 代码,并且可以接受可变数量的参数。它们以 !
结尾来进行区分。Rust 标准库包含各种有用的宏。
println!(format, ..)
在标准输出中打印一行字符串;format!(format, ..)
的用法与println!
类似,但并不打印,它以字符串形式返回结果;dbg!(expression)
会记录表达式的值并返回该值;todo!()
用于标记尚未实现的代码段。如果执行该代码段,则会触发 panic;unreachable!()
用于标记无法访问的代码段。如果执行该代码段,则会触发 panic;assert_eq!(left, right)
用于断言两个值是否相等;