computer_knowledge_notes/Languages/Rust/rust_lang_basic.md

20 KiB
Raw Blame History

3_1变量

变量(使用let关键字定义)默认是不可改变的(immutable)。为什么要默认让它不可改变呢?因为不应该改变的变量有可能被其它部分的代码改变其值。

使变量可变的方法是使用mut关键字。

不可改变的变量与常量(使用const关键字)的区别:常量的值只能来自于常量表达式,变量的值还可来自于函数返回值、或运行时才能计算出的值。

可改变的变量与隐藏(使用let关键字)的区别:隐藏的本质是创建了一个新变量,而可改变的变量还是原来那个变量。

3_2数据类型

标量

整型:有符号数以i开头,无符号数以u开头。长度有8、16、32、64、128四种。特别地isizeusize这两种整数类型的长度取决于计算机架构如64架构上它们就是64位的。

浮点型:有两种类型f32f64

布尔型:只有一种类型bool。只有两个值truefalse

字符型:只有一种类型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标准库中的枚举类型其成员SomeNone可以不需要前缀而直接使用。这个枚举类型说的是一个值只能有存在和不存在两种状态。其它语言中的空值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.rssrc/lib.rs

路径

路径里用::分割各层模块。

绝对路径从crate根开始以crate名或crate开头。

相对路径从当前模块开始,以selfsuper、或当前模块名开头。

模块中的所有项都默认为私有的,无法使用路径来访问这些私有的内容。若想使某个项或模块公有,需要使用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名>也可以作为函数返回值使用。

CloneCopy都是一种trait关于所有权的trait。Copy是栈上的复制,Clone是堆上的复制。

可以为实现了特定trait的类型实现具体的方法或有条件地实现trait。

生命周期

Rust编译器通过借用检查器来避免悬垂引用,借用检查器通过比较作用域来确保所有的借用都是有效的。

函数之间的参数传递难以确定生命周期,所以需要泛型生命周期来定义引用间关系,以便检查器可以进行分析。

生命周期注解是在&后面加'<生命周期名>。两个引用的参数有同一个生命周期名意味着它们和这个泛型生命周期存在的一样久。

在函数中声名生命周期,类似于声明普通的生命周期,放在函数名的后面<>之间,在需要的参数和返回值后面加上生命周期注解。

生命周期实际上是把参数和返回值进行关联。一个有生命周期的返回值如果没有关联上一个有生命周期的参数,这个返回值必然来自于函数内部,属于悬垂引用。

如果一个结构体中包含引用,则需要对这个引用添加生命周期注解。

有些引用可以不用写出生命周期,这就是生命周期省略规则:

  1. 对于输入生命周期(函数或方法的参数的生命周期),一个参数若是引用的,则必须有它自己的生命周期。
  2. 对于输出生命周期(返回值的生命周期),若只有一个输入生命周期参数,则所有的输出生命周期参数都与它相同。
  3. 对于输出生命周期参数,如果输入生命周期参数里有&self&mut self,则所有的输出生命周期参数都与它相同。

方法定义中的生命周期注解类似于函数中那样。

'static生命周期是静态生命周期,这个生命周期能存活于整个程序期间。字符串字面值的生命周期默认为'static。不建议为了通过编译,而把某个引用的生命周期 置为'static