20 KiB
3_1变量
变量(使用let
关键字定义)默认是不可改变的(immutable)。为什么要默认让它不可改变呢?因为不应该改变的变量有可能被其它部分的代码改变其值。
使变量可变的方法是使用mut
关键字。
不可改变的变量与常量(使用const
关键字)的区别:常量的值只能来自于常量表达式,变量的值还可来自于函数返回值、或运行时才能计算出的值。
可改变的变量与隐藏(使用let
关键字)的区别:隐藏的本质是创建了一个新变量,而可改变的变量还是原来那个变量。
3_2数据类型
标量
整型:有符号数以i
开头,无符号数以u
开头。长度有8、16、32、64、128四种。特别地isize
和usize
这两种整数类型的长度取决于计算机架构,如64架构上它们就是64位的。
浮点型:有两种类型f32
、f64
。
布尔型:只有一种类型bool
。只有两个值true
和false
。
字符型:只有一种类型char
。长度为4字节。
复合类型
元组:多类型值复合,长度固定。使用括号和逗号定义。元组元素的访问方法为通过模式匹配解构、使用点号。
数组:单一类型的复合,长度固定。使用中括号和逗号定义。数据元素的访问方法为通过数组索引访问。
let a = [1, 2, 3]; // a是包含1,2,3三个数的数组
let a: [i32; 3] = [1, 2, 3]; // a是包含三个数的数组,且元素类型为i32
let a = [0; 3]; // 数组a包含三个元素,且这三个元素的值都是0
3_3函数
以fn
关键字定义函数。
参数的类型必须显式地指定。
语句是不返回值的指令,以分号结尾;表达式计算并产生一个值,结尾没有分号。
函数如有非空的返回值,要用箭头->
声明其类型。return
用以指明返回值,如没有return
关键字则最后一个表达式的值为返回值。
函数如有空的返回值,则表示为-> ()
,意思是返回一个没有元素的元组。-> ()
常常被省略。
函数如果不返回,则表示为-> !
,这种函数叫发散函数(diverging function)。
3_5控制流
// if表达式
if 条件 {
} else {
}
// 使用else if处理多重条件
if 条件 {
} else if 条件 {
} else {
}
// 可在let语句中使用if,以实现C语言中?:运算的效果
// 使用loop实现循环,通过break返回
// 使用while实现循环
while 条件 {
}
// 使用for实现循环
for element in a.iter() {
}
4所有权
所有权是rust用以管理内存的方式。
栈和堆是数据在内存中的两种组织形式。栈中的数据是大小固定的、有序的,而堆中的数据是大小不定的、散乱的。
所有权规则是说:一个值只能对应一个变量;当变量离开作用域,则值就要被消灭。
rust会在作用域 的末尾,自动调用drop函数,在堆上回收作用域失效的变量的内存。
当多个指针指向堆中的同一个数据时,默认为移动,即只有一个指针有效,这样drop函数只要释放有效指针指向的堆中数据即可,避免了二次释放。
如果确实需要复制,而不是移动,则需要使用clone方法。
当数据在栈上时,对数据的操作将会是复制,而不是移动。
一个变量进入函数即被视为离开作用域,当函数退出时函数内的变量被视为离开作用域。
函数返回的值如被另一个变量所有,则不会被drop函数清理掉。
引用与借用
如果只想使用值而不获得它的所有权,则需要&
符号进行引用。这实际上是用对指针的所有权来代替对值的所有权。
在函数参数里使用引用,就被称为借用。
引用默认不可修改。不可变引用可以有多个。
可以在定义和引用的时候都使用mut
关键字,来使引用可修改。可变引用只能有1个。
悬垂引用是不被允许的,编译器会报错。即引用必须总是有效的。
slice
不光可以引用一个整体,还可以引用整体中的一个部分,这就是slice。
字符串slice用[starting_index..ending_index]
标识。字符串字面值就是一个字符串slice。
数组slice类似于字符串slice,只是它的元素是数字。
5结构体
使用struct
关键字来定义结构体。
结构体与元组都可以包含不同的数据类型,它们的区别是:结构体里的元素有名字,而元组里的元素没有名字;结构体里的元素无序,而元组里元素有序。
要想结构里某个字段可变,整个结构体必须是可变的。
给结构体里的字段赋值,如果变量与字段重名,可以使用简化写法,即只写一个名字。
给结构体里字段赋值,如果某些值与其它的实例相同,可使用..
从其它实例来创建这个实例。
结构体里的字段也可以没有名字,此时叫元组结构体。
可以使用impl
关键字给结构体定义方法,这样结构体看上去就像一个对象了。方法的第一个参数总是self
,代表了调用该方法的结构体自身。
方法也可以有其它参数,定义方式同函数。
当第一个参数不是self
时,它就不是这个结构体的方法,而是这个结构体的关联函数。
使用方法要用.
,而使用关联函数要用::
。
即可以在一个impl
块里定义多个方法,也可以把不同的方法定义在不同的impl
块中。
6枚举与模式匹配
定义枚举
使用enum
关键字来定义枚举类型,使用::
来创建它的成员的实例。
枚举类型的实例可以直接附加数据,这样就可以不用额外的结构体把类型和数据结合到一起了。
枚举类型的成员可以是任意类型的数据,如字符串、数字类型、结构体、甚至是另一个枚举。如果用结构体来做,将会出现一堆结构体,没有枚举这样干净、清爽。
枚举类型也可以用impl
关键字来定义方法。
Option
是一个定义在Rust标准库中的枚举类型,其成员Some
和None
可以不需要前缀而直接使用。这个枚举类型说的是一个值只能有存在和不存在两种状态。其它语言中的空值NULL
可能引发系统漏洞,而使用Option
枚举则可方便rust编译器的检查,从而避免使用空值带来的不稳定因素。
enum Option<T> {
Some(T),
None,
}
match运算符
match
把一个值和一系列模式相比较,并根据匹配到的模式执行相应的代码。其作用类似于C语言里的switch
语句。
match
的模块里用=>
把模式和要运行的代码分开,每个分支之间用逗号分隔。
枚举类型的成员还可以是另一个枚举类型,这样match
就可以枚举类型里的枚举类型了。
当match
匹配Option
枚举类型的时候,就可以对一个值有效和无效两种情况分别处理了。
match
的匹配方式是穷尽式的,即所有可能的情况都必须列出来。如果有某些情况不想显式地列出来,可以用_
模式来指代它们。感觉_
模式类似于C语言switch
语句的default
选项。
if let控制流
如果只关心match
里的一个分支,可用if let
代替match
以使代码在形式上简洁。
if let
使用等号来分割模式和表达式。
7模块系统
控制复杂性的方法,就是把复杂问题拆分成多个简单的问题。一个大的程序是由多个更小一点的代码片断组成的,就像搭积木一样。Rust可以用的模块系统(积木)有:
- Crates - 完成自己独立功能的一些代码。我觉得大致可理解为用户写的用于实现特定功能的代码。
- 包 - 也是一些crate。它是用户的工具箱。
- 模块和use - 模块是对一个crate内部进行分组。
use
用于把路径引入作用域。 - 路径 - 模块树中一个项的位置。
包和crate
包的内容:
- Cargo.toml - 告诉rust编译器如何构建这个包。
- 作为库的crate - 只能有一个。如果存在的话即文件src/lib.rs,此时它是根crate。
- 二进制的crate - 可以有多个。如果存在src/main.rs,说明这是用户代码,此时它是根crate。
当src目录下lib.rs和main.rs同时出现,我觉得应该把main.rs视为根crate。
模块
用mod
关键字定义模块。模块里还可以包含模块。
模块之间形成树状的结构,树根叫crate
(隐式的),根crate即为src/main.rs
或src/lib.rs
。
路径
路径里用::
分割各层模块。
绝对路径从crate根开始,以crate名或crate
开头。
相对路径从当前模块开始,以self
、super
、或当前模块名开头。
模块中的所有项都默认为私有的,无法使用路径来访问这些私有的内容。若想使某个项或模块公有,需要使用pub
关键字。
super
在路径中用以指定当前模块的上级模块。
如果把一个结构体用pub
关键字定义为共有的,它里面的字段也仍然是私有的。如果一个结构体里存在私有字段,则需要一个公共的关联函数才能构造它的实例。
如果把一个枚举类型定义为共有的,则它的所有成员都将变成公有的。
use关键字
use把一个路径引入作用域。这个路径即可以是绝对路径,也可以是私有路径。路径指代的即可以是一个模块,也可以是一个项。
不建议把某个项直接引入作用域,这样会难以找到这个项来自于哪里,并且有可能产生名字的冲突。
使用use
把同名类型引入同一个作用域的解决办法:使用as
来指定别名。这样名字就不冲突了。
use
导入的路径默认是私有的,可以用pub
关键字使其公有化。
对于外部包,先要在Cargo.toml里列出,再使用use
引入才有效。
如果use引入的若干路径有公共部分,可把它们写在一行里,不同的部分放大括号里以逗号分隔。
use
语句里的*
(glob运算符)代表一个路径下所有的公共项。
把模块分割到不同的文件
mod
语句后跟分号,而不是代码块,则Rust编译器会从与模块同名的文件中加载模块的内容。这类似于C语言里的#include
的作用。
8常见集合
集合是Rust标准库中提供的一些数据结构。集合可以包含多个值,集合里的数据是存储在堆上的。常用的集合有:
- vector - 相同类型元素的集合。
- 字符串 - 字符的集合。
- 哈希map - 使用哈希函数实现的键值映射的集合。
vector
可以调用Vec::new
创建一个新的vector,也可以使用vec!
宏用一些初值来创建vector。
可以使用vector自带的push
方法向vector里增加值。
vector在离开其作用域会被释放。
要想读取vector中的元素,可以使用&
和索引号返回那个元素的引用,也可以使用vector自带的get
方法来返回一个Option<&T>
。
如果vector中的某个元素被引用,则被引用的那个元素是不可修改的。这意味着对vector的操作如引起被引用元素的修改,编译会报错。
使用for
循环可以遍历vector中的元素。
若想在vector里存储不同类型的值,可以把这些类型都放到一个枚举类型里,以枚举类型作为vector里元素的类型。
字符串
str
类型是由核心语言提供的。
String
类型是由标准库提供的。它是大小可增长的、内容可变的、有所有权的、UTF-8编码的字符串。
可以使用String::new
创建一个新的String,也可以使用String::from
或它自带的to_string
方法从字符字面值创建新String。
String类型自带的push_str
方法是在当前字符串后面附加一个字符串,它是使用引用的方式附加新字符串的,即被附加的字符串所有权没有改变。它自带的push
方法附加的是单个字符。使用+
运算符可以把两个已知的字符串合并在一起,此时+
前面的字符串的所有权要移动,而+
后面的字符串的所有权要复制。使用format!
宏可以把多个字符串拼接到一起,此时发生的是所有权的复制而不是所有权的移动。
Rust的字符串不支持索引,因为不同类型的字符在UTF-8中的编码长度是不一样的。Rust把字符串看成一堆数字的集合,或者一堆Unicode字符的集合,或者一堆字形簇的集合。
如果确实需要索引字符串,比如用索引创建一个字符串slice,则必须得知道每个字符的大小并给出正确的索引范围,否则编译器将报错。
String类型的chars
方法把字符串分解成一堆字符,而bytes
方法则把字符串分解成一堆数字。标准库中并没有把字符串分解成字形簇的功能,因为这很复杂。
哈希map
哈希map不在核心库中,需要从标准库中引入。它的路径:std::collecttions::HashMap
。
哈希map里每个元素都应该是同质的,即所有键都是相同类型,所有值都是相同类型。
可以使用HashMap::new
创建一个新的哈希map,也可以用vector的collect
方法来创建哈希map。
可以使用哈希map自带的insert
方法插入新元素。
栈中的数据进入哈希map,发生的是值的复制,所有权不变;堆中的数据进入哈希map,发生的是值的移动,所有权改变。
可以使用哈希map自带的get
方法获取某个键所对应的值,注意它返回的是Option<V>
。可以使用for
循环获取哈希map里的所有元素。
插入一个键值对,如果键相等,则旧值被覆盖;使用哈希map自带的entry
方法可以选择性地插入一个键值对,即键没有对应的值才插入新值,有对应的值仅返回它的可变引用;使用or_insert
方法可以根据旧值来更新一个值,注意返回的值是一个可变引用,所以要用*
号解引用才能真正更新那个值。
9错误处理
Rust里的错误分为可恢复错误和不可恢复错误。
- 可恢复错误 -
Result<T, E>
,类似于Java里的异常,用户可编码处理。 - 不可恢复错误-
panic!
,通常是bug的同义词。
不可恢复错误
panic!
宏打印错误信息,展开并清理栈数据,然后退出。
将环境变量RUST_BACKTRACE
设置为不是0的值可获取backtrace。backtrace是一个函数的调用列表,指明了到出错位置的一个调用链条。
可恢复错误
可恢复错误会返回Result
,它是一个枚举类型,定义了两个成员:
enum Result<T,E> {
Ok(T),
Err(E),
}
可以使用match
运算符来处理Result
的结果。
可以嵌套match
运算符来匹配不同的错误。但还有其它的方法来避免大量嵌套match
的情况,从而让代码看起来更优雅。
Result的unwrap
方法会处理所得到的结果,如果是Ok
则返回其值,如果是Err
则调用panic!
宏。它的expect
方法类似于unwrap
,所不同的是如果是Err
则可以打印出我们指定的信息再panic!
。
对于在Result里出现Err
的情况,也可以选择不painic!
,而是用return
把它传播出去,交给用户来处理。
使用?
运算符可实现使用return
一样的传播Err
的功能。
选择错误的原则
示例、代码原型、测试适合panic。因为示例是要凸显某些功能,在原型设计的时候还没想好如何处理错误,测试失败可更直观的发现问题。
当我们能确保Result的值一定是Ok
的时候,可以选择panic。因为此时panic永远不会执行。
在有可能导致有害的情况下,应该使用panic。
当代码尝试操作无效数据时,应该panic。
当错误是可预期的时候,应该把它传递出去,而不是panic。
10泛型
泛型是对类型、函数、方法的抽象替代。
trait用来定义泛型的行为。
生命周期是一种特殊的泛型。
定义泛型
在函数中定义泛型,就要在函数名称和参数列表之间用<>
声明泛型的类型。
在结构体中定义泛型,就要在结构体名称的后面用<>
声明泛型的类型。
在枚举类型中定义泛型,就要在枚举类型名称的后面用<>
声明泛型的类型。
在方法中定义泛型,就要在impl
后面用<>
声明泛型的类型。
使用泛型的代码在运行时不会有额外的开销,编译器使用了一种叫单态化的手段保证了运行的效率。
trait
用trait
关键字来声明一个trait,在随后的语句块中声明方法签名。方法签名可以有多个,以分号分隔。
用impl <trait名> for <类型名> {<方法的具体实现>}
在类型上实现trait。可以像使用普通方法那样使用trait实现的方法。
只有trait的声明或类型的定义在本地作用域,才能实现具体的trait。如果trait的声明和类型的定义都是外部的,则不能实现具体的trait,这是为了保护代码的安全性。
可以在声明trait的时候给出方法的具体实现,这就给出了相应方法的默认行为,可以选择在使用时保留或重载这个方法。
trait里的默认实现允许调用同一trait里的其它方法,即使那些方法没有默认实现。
函数的参数可以有自己的trait,这样参数就有了自己的方法,在参数后面跟上impl <trait名>
即可。前提是参数的类型是trait支持的类型。
impl <trait名>
是Trait Bound的语法糖,即<T: <trait名>>(<参数>: T)
。
如果函数里的参数需要多个trait,用+
把它们连接起来。
还可以把trait名通过where
从句写到函数体里,从而简化Trait Bound,函数签名就显得不那么杂乱了。
impl <trait名>
也可以作为函数返回值使用。
Clone
和Copy
都是一种trait,关于所有权的trait。Copy
是栈上的复制,Clone
是堆上的复制。
可以为实现了特定trait的类型实现具体的方法,或有条件地实现trait。
生命周期
Rust编译器通过借用检查器来避免悬垂引用,借用检查器通过比较作用域来确保所有的借用都是有效的。
函数之间的参数传递难以确定生命周期,所以需要泛型生命周期来定义引用间关系,以便检查器可以进行分析。
生命周期注解是在&
后面加'<生命周期名>
。两个引用的参数有同一个生命周期名意味着它们和这个泛型生命周期存在的一样久。
在函数中声名生命周期,类似于声明普通的生命周期,放在函数名的后面<>
之间,在需要的参数和返回值后面加上生命周期注解。
生命周期实际上是把参数和返回值进行关联。一个有生命周期的返回值如果没有关联上一个有生命周期的参数,这个返回值必然来自于函数内部,属于悬垂引用。
如果一个结构体中包含引用,则需要对这个引用添加生命周期注解。
有些引用可以不用写出生命周期,这就是生命周期省略规则:
- 对于输入生命周期(函数或方法的参数的生命周期),一个参数若是引用的,则必须有它自己的生命周期。
- 对于输出生命周期(返回值的生命周期),若只有一个输入生命周期参数,则所有的输出生命周期参数都与它相同。
- 对于输出生命周期参数,如果输入生命周期参数里有
&self
或&mut self
,则所有的输出生命周期参数都与它相同。
方法定义中的生命周期注解类似于函数中那样。
'static
生命周期是静态生命周期,这个生命周期能存活于整个程序期间。字符串字面值的生命周期默认为'static
。不建议为了通过编译,而把某个引用的生命周期 置为'static
。