/rust-helloworld

rust practice

Primary LanguageRust

rust helloworld

个人rust学习练习项目。

教程: https://course.rs/

笔记总结

所有权与借用

引用分可变引用与不可变引用,只有可变引用才可以修改对应的值。

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

可变引用同时只能存在一个(一个可变引用的作用域内,不能存在指向同一个变量的另一个可变引用)

可变引用与不可变引用不能同时存在

编译器会确保数据不会在其引用之前被释放,想释放数据,必须停止使用引用。避免了悬垂引用的问题(dangling reference)

  • 同一时刻要么拥有一个可变引用,要么拥有任意多个不可变引用
  • 引用必须是有效的

复合类型

只使用基本类型的局限性:无法从更高层次来简化代码。

字符串(String)以及字符串切片(&str)。字面量字符串本质是切片。字符串使用UTF-8编码。Rust不允许索引字符串。对字符串进行切片非常危险,无法保证索引的字节正好位于字符的边界上。

字面值str不可变,性能快速且高效。而String类型是程序运行中生成的,大小不可预知,需要请求内存存放string,并且使用完成后要归还。

你只能将 String 跟 &str 类型进行拼接,并且 String 的所有权在此过程中会被 move.

与 str 的很少使用相比,&str 和 String 类型却非常常用,因此也非常重要。

对于 Rust 而言,安全和性能是写到骨子里的核心特性,如果使用 GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust 的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存

元组

多种类型组合在一起的,长度固定,元组中的元素顺序也是固定的。

模式匹配解构元组:用同样的形式把一个复杂对象中的值匹配出来。

结构体

必须将结构体声明为可变的,才能修改其中的字段,rust不支持将某个结构体某个字段标记为可变。

初始化结构体时,必须初始化每个字段。

结构体可以简化创建,变量名和结构体名相同时,可以直接使用。

更新结构体的时候,可以将已有结构体传入新结构体内部来简化更新。结构体更新赋值的时候,可能会转移所有权。

元组结构体,字段没有名称,长得很像元组。

单元结构体类似单元类型。没有任何字段和属性。

在结构体中使用引用,必须加上生命周期标识符。

枚举

枚举类型是一个类型,会包含每一个可能的枚举成员,而枚举值是该类型中的某个成员的实例。

枚举成员还可以直接关联数据,不仅如此,同一个枚举类型下的不同成员还可以持有不同的数据类型(Java不允许这么做)。可以同一化类型。

Option枚举用来处理空值,rust抛弃了null值,而改为使用Option枚举变量来表述这种结果,Option有两个成员,一个是Some表示有值,一个是None表示没有值。

Some和None无需使用Option:: 就可以使用。通过Some包装后的值,不能直接与原值进行交互,因为属于不同的类型,这样的包装有利于解决隐藏的空指针问题。要进行运算必须转换类型为T(各种unwrap方法)

Match表达式可以使用匹配模式识别枚举的成员,根据不同的成员使用不同的逻辑。

数组

Array 速度很快但长度固定, Vector(动态数组)可以动态增长但是有性能损耗。

Array和Vector的关系与&str和String 的关系很像,前者是固定长度的字符串切片,后者是可动态增长的字符串。

数组必须满足 长度固定,类型相同,线性排列这三个条件,因此array是可以储存在栈内存上的。

数组切片,允许引用部分连续的片段,使用切片引用类型&[T]

流程控制

If 分支控制

if语句块是表达式,可以给变量赋值。

循环控制

for in 循环:

for item in &container {

}

使用for的时候,往往使用的是集合的引用形式,除非不想在后续代码中使用该集合(所有权转移后回收)。

while vs for: for不会使用索引去访问数组,因此更加安全和简洁,同时避免数组边界检查,性能更好。

loop循环:适用所有循环场景,就是一个简单的无限循环,使用时一定要注意死循环影响性能。

模式匹配

模式匹配经常出现在函数式编程中,用于为复杂的类型系统提供轻松的解构能力。

可以使用match来替代else if这种丑陋的多重分支使用方式。

match匹配enum类似于其他语言的switch,而默认情形_, 类似于default。

match本身也可以是一个表达式,用它进行赋值。

模式匹配的一个重要功能是从模式中取出绑定的值(比如取出enum中绑定的值)。

matches!宏,可以返回匹配结果为true或者false

匹配模式同名变量可以覆盖老的值,绑定新的值(使用匹配守卫语句可以避免老的值被覆盖)。

匹配模式可以用来解构Option,解决rust变量是否有值的问题。

@(at)绑定。允许在模式中将模式中的一个字段绑定另外一个变量。

方法

rust的方法定义使用的是struct和impl分开的形式(类似于go),可以给予使用者极高的灵活度。

self &self &mut self

&self 是 self: &Self 的简写,在impl块内,Self指代被实现方法的结构体类型,而self指代此类型的实例。

self 依然有所有权的概念:

  1. self表示所有权转移到该方法中,这种形式用的很少
  2. &self表示对类型的不可变借用
  3. &mut self表示可变借用

self的使用跟函数参数一样,要严格遵守所有权规则。

使用方法代替函数有以下好处:

  • 不用在函数签名中重复些类型
  • 代码的组织性和内聚性更强,方便代码维护阅读。

方法名允许与字段名相同,主要用于实现getter访问。

rust对于方法接受者使用隐式借用,因此不需要像c或c++一样使用两个不同的运算符来调用方法(. 和 ->)。当object.something()调用方法时,rust会自动为object添加&,&mut或者*以便是object与方法的签名匹配。

关联函数

定义在impl中但没有self的函数称为关联函数,因为没有self,所以不能使用f.read()的用法调用,因此它是函数而不是方法。(类似java中的静态函数?)

rust 有一个约定俗成的规则,使用new来作为构造器的名称。

使用:: 来调用关联方法,该方法位于结构体的命名空间中,:: 语法用于关联函数和模块创建的命名空间。

同一个struct可以有多个impl定义。

枚举类型也可以实现方法。

泛型和特征

用同一功能的函数处理处理不同类型的数据。在不支持泛型的语言中通常需要为每一种类型编写一个函数。

T 是泛型参数,泛型参数可以用任意字母。使用泛型参数前必须对其进行声明:

fn largest<T>(list: &[T]) -> T {}

结构体中也可以使用泛型,跟泛型函数类似,使用泛型之前必须声明泛型参数Point.

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

枚举中也可以使用泛型

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

方法中也可以使用泛型。

不仅可以定义基于T的方法,还可以针对特定的具体类型来进行方法定义。

Rust泛型是零成本抽象,在编译的时候对每个类型生成各自的代码,损失了编译速度并增大了最终的文件大小。但是好处就是没有性能上的损失。

const 泛型

针对值的泛型,在数组类型中,不同长度的数组其实代表不同的类型,为了解决不同长度数组的问题,引入了

特征 Trait

特征定义了一个可以被共享的行为,只要实现了特征,你就能使用该行为(类似于接口)。

定义特征只定义行为看起来是什么样的,而不定义行为具体是什么样的。每一个实现特征的类型都需要具体实现该特征相应的方法,编译器也会确保完全一致。

特征定义可以进行默认实现,也可以override这种实现,默认实现里面可以调用其他未被实现的方法

特征可以作为返回值,并返回一个满足此特征的对象。但是有一个很大的限制,就是只能有一种具体的类型。

通过derive派生特征,派生出来的是rust提供的默认的特征,例如#[derive(Debug)]

特征对象

当特征作为返回值时,只能有一种类型,为了解决这个问题引入特征对象(Java语言使用的是继承,而rust没有继承)。

可以通过 & 引用或者 Box 智能指针的方式来创建特征对象。

dyn 关键字只用在特征对象的类型声明上,在创建时无需使用 dyn, dynamic 的含义是特征对象的动态分发,与之相对应的就是泛型T,也就是静态分发。

动态分发Box与静态分发Box的区别在于动态分发除了一个指向实际对象的pointer外还有一个vptr指向一个虚拟表,虚表中保存所有可能的实现特征的方法,在真正调用时,先从虚表中找到对应的方法,再执行。

self和Self的区别,self指代当前实例对象,Self指代特征或者方法类型的别名。只有对象安全的特征才可以使用特征对象,特征安全的定义时:

  1. 所有的方法返回值都不能是Self
  2. 方法没有任何泛型参数

关联类型

在特征定义中,可以指定一个type类型,并在方法签名中使用该类型,在实现特征的时候需要指定此类型。虽然相同的功能可以用泛型来实现,但是使用关联类型使程序更可读。

特征定义中的特征约束

可以让某个特征A使用另一个特征B的功能,类似于interface中的继承,在实现的时候需要同时实现特征A和特征B。

集合类型

动态数组Vector

动态数组只能存储相同类型的元素,想存储不同类型的元素可以使用枚举类型或者特征对象。

Vec::new 可以创建一个动态数组。vec![] vec宏也可以创建vector。

更新vector,必须将其声明为mut对象,然后可以使用push方法添加元素到尾部。

Vector元素超出作用域范围后,会被自动删除,删除后,其内部存储的所有内容也会被随之删除。

从vector中读取元素,可以使用下标访问或者get方法,推荐使用get方法,返回值会用Option封装,比较安全。

数组元素的借用遵循借用权使用原则,要么同时存在一个可变借用,或者任意多不可变借用,也就是说如果作用域内vector push过元素,那么在此之前的不可变引用将不能被继续使用。原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。

一次访问数组元素,可以使用迭代器方式,比用下标的方式更加安全可靠,每次下标访问会触发数组边界检查。可以在迭代的过程中,修改元素的值。

KV存储HashMap

存储键值对,是rust标准库中提供的集合类型。

可以使用new方法来创建,HashMap::new. 使用hashmap需要用use 引入到当前作用域,因为没有被包含在prelude中,而vec和string不需要。

集合类型都是动态的,没有固定额内存大小,因此底层数据都是存储在内存堆上,然后通过栈中的引用类型来访问。K必须用同样的类型,V也是。

如果预先知道KV对的个数,可以使用with_capacity(capacity) 来创建指定大小的HashMap,这样可以避免频繁的内存分类和拷贝,可以提升性能。

使用迭代器配合collect方法可以创建HashMap.

所有权转移和其他的Rust类型没有区别:

  • 若类型实现Copy特征,会复制仅HashMap,无所谓所有权
  • 如果没有实现Copy特征,所有权转移给HashMap中

如果使用引用类型放入到HashMap中,需要确保该引用的生命周期和HashMap一样久

查询hashmap使用get方法,get方法会返回Option类型,另外取到的值是V的借用,如果不使用借用的话,会发生所有权转移。

类型转换

基本数值类型可以使用as转换,只能将小类型转换为大类型。

tryInto转换可以拥有完全控制,可以将大类型转换为小类型。try into会尝试进行一次转换,并返回一个Result。

返回值和错误处理

Rust错误主要分两类:

  • 可恢复错误,只会影响用户操作进程,不会影响全局稳定性,用Result<T, E>用户可恢复错误
  • 不可恢复错误,对系统是致命的。用panic! 用于不可恢复错误。

panic! 宏时,程序会打印一个错误信息,展开报错点往前的函数调用堆栈,然后退出程序。

如果时main线程panic,则程序会终止,子线程panic不会导致程序结束。当你确切知道程序是正确的,可以使用panic,panic 触发方式更简单。

Result<T, E> 是一个枚举类型,T代表成功时存入正确的值,存放方式时Ok(T),e代表存入的是错误值,存放方式是Err(E).

Result 的unwrap 方法如果有值就取出来,没有值直接panic。expect也会panic,但是会带上自定义的错误信息,相当于重载了函数。

错误传播,涉及十几层函数调用的,需要将错误层层上传给上层函数来处理,这时可以使用问号?。问号使用等价于match,match成功返回值,match失败返回错误。问号还可以链式调用。

问号操作还可以用户Option的返回,Option通过?返回None值(满足条件返回正常值并继续,不满足条件直接返回None)。注意,只有返回值同样是Option 并且具备正常和异常两种情形才能使用。

包和模块

和其他语言一样,rust提供相应的模块概念用于代码的组织管理:

  1. 项目Packages,一个cargo提供的feature,用于构建,测试和分享包
  2. 包Crate,由多个模块组成的树形结构,可以作为三方库进行分发,也可以生成可执行文件来运行
  3. 模块Module,可以一个文件多个模块,也可以一个文件一个模块,模块是真实项目的代码组织单元。

包crate和package

概念很容易搞混,crate是一个独立的可编译单元,编译之后生成一个可执行文件或者一个库。

package本身是一个项目,包含独立的Cargo.toml 文件,可以包含多个Crate包。只能包含一个库library类型的包。

用cargo new创建出来的是一个Package,而其中的包名正好和package名称相同,所有容易搞混。真实项目中典型的package会包含多个二进制包,放在src/bin下,每个文件都是独立的二进制包,同时存在一个库包src/lib.rs

  • 唯一库包:src/lib.rs
  • 默认二进制包:src/main.rs,编译后生成的可执行文件与 Package 同名
  • 其余二进制包:src/bin/main1.rs 和 src/bin/main2.rs,它们会分别生成一个文件同名的二进制可执行文件
  • 集成测试文件:tests 目录下
  • 基准性能测试 benchmark 文件:benches 目录下
  • 项目示例:examples 目录下

模块Module

模块是代码的构成单元(类比java的一个包)。

  • 使用mod关键字来创建新模块,后面紧跟模块名称
  • 模块可以嵌套
  • 模块中可以定义各种Rust类型,例如函数,结构体枚举,特征等
  • 所有模块均定义在同一个文件中。

使用模块可以将我们的相关功能组织在一起,通过一个名称来说明为什么被组织在一起,方便他人使用。

模块路径每一层用 :: 分割,绝对路径指包根(main.rs lib.rs)crate root 开始,路径名以包名或者crate开头 相对路径,以当前模块开始,以self super或者当前模块标志作为开头。

代码可见性:模块不仅对组织代码很有用,还能定义代码的私有化边界。rust出于安全考虑,默认情况下所有的类型都是私有化的,包括函数,方法,结构体,枚举,变量,连模块本身也是私有化的。

父模块无法访问子模块的私有项,子模块可以访问父模块,以及父父模块的私有项。

pub关键字,类似于public或者go语言中的首字母大写,用来控制模块和模块中指定项对外的可见性。

super 引用模块相当于文件系统的上一层../

pub结构体和枚举的默认可见性,结构体pub但是所有字段依旧私有,枚举pub字段对外可见。

模块与文件分离,实现是在与模块同名的文件中,而定义则是在lib.rs 中。

use关键字

绝对路径引入模块,使用use和绝对路径的方式将模块引入当前作用域。

避免同名引用,可以使用模块::函数的方法来具体指定,也可以使用as 进行别名引用(go有类似功能)。

引入项再导出,可以将引入进来的模块重新再公开(在use前加入pub)

使用第三方包的时候,可以修改Cargo.toml文件的dependencies区域,然后就是在代码中use即可

搜索地方放包可以在crates.io 或者lib.rs 中检索使用,查找包更推荐lib.rs

受限可见性语法:

  • pub 意味着可见性无任何限制
  • pub(crate) 表示在当前包可见
  • pub(self) 在当前模块可见
  • pub(super) 在父模块可见
  • pub(in ) 表示在某个路径代表的模块中可见,其中 path 必须是父模块或者祖先模块

注释和文档

Rust中的注释分为三类:

  • 代码注释,说明某一块代码的功能。
  • 文档注释,支持markdown,对项目描述,公共api等功能进行介绍
  • 包和模块注释(属于文档注释的一种),主要说明模块和包的功能,方便用户迅速了解项目。

行注释和块注释与其他语言一致。

文档行注释 /// 文档块注释/** ... */

  • 文档注释需要位于lib类型的包中,例如lib.rs
  • 文档注释使用markdown语法
  • 被注释的对象使用pub对外可见

使用cargo doc可以直接生成html文件,放入到target/doc目录下,使用cargo doc --open 命令,可以生成文档之后自动在浏览器打开。

常用的文档标题

  • '#Example' 表示例子
  • "#Panics" 函数可能出现的异常情况
  • "#Errors" 函数可能出现的错误

除了函数,结构体等注释,还可以给包和模块添加注释,需要注意的是注释要添加在包和模块的最上方

包级别的注释也分两种 行注释//! 块注释/*! ... */

文档测试(Doc Test),Rust允许我们在文档注释中写入单元测试的用例。

格式化输出

print! println! format! 都可以用来进行格式化输出。

{}和{:?}是格式化占位符, 无需为特定类型指定特定占位符,而是统一使用{} 来代替

区别:

  • {} 适用于实现了Display特征的类型,用来输出格式化文本
  • {:?}适用于实现了Debug特征的类型,用于调试场景。

位置参数: 使用{1}等格式,可以使用指定位置的参数代替占位符

// println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");

具名参数: 使用{argument}的格式,需要注意,具名参数必须放到不带名称的参数后面.

格式化参数,可以指定宽度{:5},字符串填充{:05},对齐{:<},精度{:.2},进制{:#x}, 指数{:2e},指针{:p}