Rust 学习笔记

rust 学习梳理

数据类型

  • 基于已明确的类型,Rust会推断剩下大部分类型。基于类型推断Rust具备了与动态类型语言近似的易读性,并仍能在编译期捕获类型错误。
  • 函数可以是泛型的:单个函数ujiu可以处理不同类型的值。
  • Rust的类型根基是一组固定宽度的数值类型,匹配几乎所有现代处理器直接硬件实现的类型。固定宽度可能丢失精度(u8 u16 u32 u64 u128 usize[有符号同理],f32,f64)
  • usize 和 isize是与目标机器地址空间保持一致
  • bool类型在rust用整字节表示
  • 字符类型用32位来表示单个Unicode字符
  • 元组类型中,每个元素有不同的类型;元组中只能用常量索引其中元素(t.1)。常用于在函数中返回多个值。可以用作一个小巧的结构体类型。
  • 指针类型:Rust有多种可以表示地址的类型(引用,Box,裸指针)
  • Rust的引用永远不会为空,Rust会跟踪值的所有权和生命周期,在编译器排除了悬空指针、双重释放、指针失效等错误。
  • Rust的引用分为不可变和可变。
  • Box是分配在堆上的引用类型。
  • 裸指针是不安全的,Rust不会跟踪它指向的内容。只能在Unsafe块中对裸指针解引用。
  • Rust用三种类型表示值序列(数组,向量,切片)
  • 数组是相同类型,确定大小的序列,不能追加或缩小。向量(vector)是动态分配可增长的相同类型的序列,元素存在堆中。切片是引用了数组或向量的一部分。
  • 切片是任意长度,对切片的应用是一个胖指针,双字值,包括第一个元素的指针和切片中元素的数量。
  • 函数以切片引用为参数,可以给它传入向量或数组,很多向量和数组的方法都是在切片上定义的。&[T]。可以使用范围值获取数组或向量一段区域(&a[2..6])
  • 字符串原始表达:r"/root/ddd"。
  • 可以在字符串前加入原始记号#,可以根据需求增加多个#来表示原始字符串开始与结束(r###“this is a raw string ### "也可以是原始字符",结束了"###)
  • 字节串是用b表示:b"abcd",这是一个u8类型的数组。字节串不能包含unicode字符,只能用ascii和\xHH转义序列。
  • 在内存中,字符串都是unicode字符,由于是可变字节编码,所以或占用多个字节。ascii编码会存储在一个字节中。
  • &str很像&[T],而String类似与Vec

String

创建String的方法

  1. .to_string() 将&str转换为String。这会复制字符串。.to_owned()会做同样的事情,只是命名风格适用与另一些类型
  2. format!()会返回一个新的String.
  3. 字符串的数组,切片和向量都有的两个方法:.concat()和.join(sep)。会形成新的String

所有权和生命周期

在Rust中,所有权是语言本身的特性,通过编译器强制检查执行。每个值斗殴与决定其生命的唯一拥有者,当拥有者被释放时,它拥有的值也同时被释放,rust中叫做丢弃。
凡是超出作用域的类型,它所拥有的值将被抛弃。

拥有者与它拥有的那些值形成了一课树,每棵树的总根都是一个变量,当该变量超出作用域时,整棵树都会随之消失。

  • 可以将值从一个拥有者转移给另一个拥有。这允许构建,重新排列和拆除树形结构。
  • 整数、浮点数、字符、bool类型的简单类型,不受所有权规则约束,这些被称为Copy类型
  • 标准库提供了引用计数指针类型 Rc和Arc,允许值在某些限制下有多个拥有者。
  • 可对值进行借用,获得值的引用。这种引用是非拥有型指针,有受限生命周期

移动

变量的赋值,传递给函数,或者函数返回,这样的操作会带来所有权的移动,也就是由目标变量来获得值的控制权,获得新的生命周期,构建新的所有权树。
而源变量将会变为未初始化状态,无法被使用。

从一个Vec中获得某一个string,直接取值会报错,没有实现Copy trait。建议是访问其中的元素使用引用而不是移动它。

如果真的要从Vec取出一个字符串,但Rust是不会将Vec的某一个元素设置为未初始化状态。因此需要一些操作:

  1. 从vec弹出一个值:v.pop()
  2. 指定索引处与最后一个值互换,把前者移出:v.swap_remove(1)
  3. 取出一个值与另一个值互换:std::mem::replace(&mut v[2],"abcdefg".to_string())

移出一个Option,将其与None互换: std::mem::replace(&mut v[2],None)。实际上专门提供了一个方法实现:v[2].take()。

对于Copy类型,移动就是复制值。
在Rust中,每次移动都是字节级别的一对一浅拷贝,并让源变为未初始化。只有那些可以通过简单复制位来复制其值的类型才能作为Copy类型,任何在丢弃时需要做特殊操作的类型都不能是Copy类型。

共享所有权

rc和arc(线程安全)。Rust的内存和线程安全保证:确保不会有任何值是既共享又可变。Rc和Arc是不可变共享引用。

引用

  • 引用是一种非拥有型指针,这种指针对生命周期毫无影响。而引用的生命周期绝对不可以超出引用的目标。为了强调这个概念,rust创造了借用(borrow):创建对某个值的引用。借出去的还是要还回来的。

引用可以让你在不影响所有权的情况下访问值,主要分为两种:

  1. 共享引用允许你读取但不能修改引用目标,可以根据需求同时拥有任意数量的共享引用。
  2. 可变引用允许你读取并且修改值,但是一个值只能有一个可变引用。
  • 通过将值的所有权转移给函数的方式可以将值传递给函数,就是所谓的按值传递。如果是将值的引用传递给函数,就是所谓的按引用传递。

  • Rust的引用永不为空,如果需要用一个值来表示对某个可能不存在的事物引用,可以使用类型Option<&T>。在机器码级别Rust会设置None为空指针,设置Some(r)为非零地址。

  • Rust包括两种胖指针。第一个是对切片的引用,携带了切片的地址和长度;第二种是对特型对象(trait),携带值的地址和适用于该值的特性实现的指针,以便调用特型方法。

  • 借用局部变量:你不能借用对局部变量后将它移除局部变量的作用域。

  • Rust的全局变量等价于静态变量,它在程序启动就会被创建,一直保持到程序结束时。它的生命周期是全局的,不代表其全局可见。

  • 静态变量必须初始化。本质上不是线程安全的。其生命周期用'static表示

假设我们有一个解析函数,接受一个字节切片并返回一个存有解析结果的结构

fn parse_record<'i>(input: &'i [u8]) -> Record<'i>{...}

不用看Record的类型定义就可以知道,如果从parse_record接收到Record,那么它包含的任何引用必然指向我们传入的输入缓冲区,而不是其他地方('static除外)。
Rust要求包含引用的类型都要接受显式的生命周期参数就是为了明示这种内部行为。

  • 在Rust中,我们可以借用一个向量的可变引用,也可以借用向量元素的不可变引用。但是这两种引用的生命周期不能重叠。也就是说,一个值拥有了可变引用,就不能同时再拥有不可变引用

共享访问是只读访问。共享引用借用的值是只读的,在共享引用的生命周期中,无论它的引用目标,还是从该引用目标间接访问的值,都不能被任何代码改变,是只读状态,值被冻结。
可变访问是独占访问。可变引用借用的值只能通过该引用访问,在整个生命周期中,都没有任何其他路径访问该值。

表达式

  • Rust是所谓的表达式语言。表达式有值,而语句没有值。
  • Rust中的if和match是可以产生值的。
  • 所有可以链式书写的运算符都是左结合的。
  • 比较运算符、赋值运算符和范围运算符(..右开区间和..=右闭区间)是无法链式书写的
  • 块是一种最通用的表达式,一个块生成一个值,在任何需要值的地方使用。
  • 当块的最后一行不带分号的时候,就以最后这个表达式的值作为块的值,而不是通常的()。
  • Rust禁止执行未覆盖所有情况的match表达式
  • if let 是match的一种简写形式,是一种模式匹配,不是条件判断。同理while let也是一种模式匹配。

Rust的循环有四种:while、 while let 、loop、for

  • 各种循环都是rust的表达式,但是while和for的值总是()。因此它们的值通常没有什么用处。而loop表达式,根据指定的值来生成一个值。
  • loop是一种无限循环,除非遇到break,return或者线程崩溃。
  • break与continue可以使用标签来跳出或跳转进指定的循环,标签类似生命周期的符号'some_word
  • Rust在分析程序控制流时,会尝试检查每条路径返回预期类型的值。但一些情况下会希望返回错误
fn wait_for_process(.....) -> i32{
  while true{
    if process.wait(){
      return process.exit_code();
    }
  }
}

上述代码会产生错误,因为Rust会认为return返回的值与期望的i32不同。但实际上我们希望程序退出,因为有异常情况。不能正常结束的表达式属于一个特殊类型:!
它不受“类型必须匹配”的约束。例如在std::process::exit()中,exit()返回类型是!,意味着exit()永远不会返回,它是一个发散函数。
在这种情况下,使用loop就可以顺利返回一个它接收的值,而不需要匹配类型。

类型转换

在Rust中,采用as关键字进行显示的类型转换。Rust允许的几种类型转换:

  • 数值可从任意内置数值类型转换为其他内置数值类型。从大范围转换到小范围会丢失精度
  • Bool类型,char类型,enum类型的值可以转换为任意整数类型,但不允许反向转换。
  • 不涉及安全的指针类型转换也是允许的

自动转换就是隐式转换,不需要声明就可以使用,默认转换,例如可变引用转换为不可变引用。
几个重要的自动转换:

  • &String类型自动转换&str类型
  • &Vec类型自动转换为&[32]
  • &Box类型自动转换为&T

这些都是隐式解引用,实现了内置的Deref trait。

错误处理

错误可以向上传播,Rust的?符号可以执行此操作,在任意生成Result的表达式上加?,?的行为去决定于函数返回了成功结果还是错误结果。
如果是成功结果,它会解包Result以获得其成功的值。
如果是错误结果,它会立刻从当前函数返回,将错误上传。
?的作用与Option类型一样,在返回Option的函数中,也可以使用?解包,遇到None时返回。

处理多种类型
使用?时,如果同时能向上传播多种错误类型会出现错误类型转换问题。

解决这个问题有几种方法:

  • 可以自己定义错误类型,实现几个错误类型的转换,可以试试thiserror crate.
  • 所有标准库的类型都可以转换为Box<dyn std::error:Error + Send + Sync + 'static>,可以将其设置为返回类型
  • 通用的错误类型会不方便了解错误类型,可以用error.downcast_ref::()进行引用,如果正是你希望的错误类型,会成功借用。

使用unwarp()可直接获得值,忽略错误,可以处理不可能发生的错误。但是如果发生了错误,unwrap()会报panic.
可以用let _ = some_func()忽略函数返回的错误。
main函数是不接受?返回错误的,处理main函数的错误,最简单的方法是调用expect(string),当发生错误时,它会panic,并打印string字符串。
干净一些的做法,是使用print来打印err信息,利用std::process::exit(1)来退出程序。

Result的设计要点:

  • Rust要求程序员在每个可能发生错误的地方做出决策,并记录在代码中。
  • 常见的决策是让错误继续传播,使用?字符实现。相比C和Go的错误处理更加直观简洁
  • 是否可能出错时每个函数返回类型的一部分,因此哪些函数会失败一目了然。如果将一个修改为能够出错,就要同时修改它的返回类型。
  • Rust会检查Result是否被用过了,这样就不会让错误悄悄溜走
  • 由于Result是一种与其他数据类型没有本质区别的数据类型,很容易将成功结果与错误结果存储在一个集合中,也容易对“部分成功”的情况进行模拟

代价时需要考虑更多的错误处理情况,但这些时值得的。

结构体

结构体会将多个不同类型的值组合成一个单一的值,便于当做一个单元来处理。Rust有三种结构体类型:具名字段型结构体、元组型结构体和单元型结构体。
三种结构体在引用方式上有所不同:

  • 具名字段型会为每个组件命名
  • 元组型结构体按组件顺序标识它们
  • 单元型结构体没有组件

Rust约定是,所有类型的名称将每个单词的第一个字母大写,是大驼峰格式。字段和方法是小写,单词用下划线连结,称为蛇形格式

元组型结构体保存的值成为元素,就像元组的值一样。元组型结构体适用于创造新的类型,创建一个只包含单组件的结构体,以获得更严格的类型检查。

单元型结构体声明了一个没有元素的结构体类型:struct Onesuch 。这种类型不占用内存,很像单元类型()。Rust不会在内存中实际存储它的值,也不会生成代码操作它。
从类型就可以知道关于值的所有信息。逻辑上讲,空结构体是一种可以像其他任何类型一样有值的类型,准确说空结构体是一种只有一个值的类型。
let o = Onesuch;
单元型结构体在处理特型很有用。

泛型结构体可以带生命周期参数,用以声明此结构体包含的引用的生命周期。
泛型结构体可以带常量参数
多项式结构体

struct Polynomial<const N:usize>{
cofficients:[f64:N]
}

根据定义,Polynomial<3>是一个二次多项式,因为N表示的是数组容量,数组索引的最大值是2。

可以为结构体自动实现某些公共特型,前提是结构体的每个字段都实现了该特型。

内部可变性

有时候我们需要一个不可变值的内部发生一点变化,这称为内部可变性。Rust提供了多种方案。

std::Cell的Cell和RefCell。Cell的特殊地方在于即使你对Cell本身没有mut权限,也可以获得和设置私有值字段。
cell.get()获取
cell.set()设置
Cell不允许在共享值上调用mut方法。get方法会返回Cell的值的副本,因此它仅在T实现了Copy特型才有效。
而RefCell可以用借用来取得T的值
ref_cell.borrow()
ref_cell.borrow_mut()等。。

Cell很容易使用,虽然破坏了规则但是可以在这些一些场景还是有用的。Cell以及包含它的类型都不是线程安全的。

枚举与模式

  • Rust的枚举不单单可以定义类型,还可以包含数据,甚至是不同数据类型。
  • 枚举的各个值会保存为整数,默认是从0开始分配,可以自定义数值。
  • 枚举可以有方法,与结构体类似。
  • 枚举的内存分布,枚举值会保存为一字节,如果枚举类型绑定了数据,数据按照其内存占用在内存中顺序分布排列,按照四字节对齐。
  • Rust没有对枚举的内存布局做出承诺,可能有不同顺序进行内存优化布局

模式

  • 表达式会生成值,而模式会消耗值。模式行为与表达式行为是相反的。
  • 按照字面量、变量和通配符进行模式匹配(0=> "abc"=> 、other=> 、_=> )
  • 按照元组型模式匹配( (abc,0)=> 、(_,"abc") => )
  • 按照结构体模式匹配( Point {x : 0,y: line} => 、 Point {x:x,y:y}=>)
  • 按照数据模式或切片模式匹配([1,2,3] = > 、[,,255]=>、[a,...,b]=>)

按照引用模式匹配:

匹配不可复制的值会移动该值,就会出现匹配后无法再次使用问题。
我们需要借用此值,而非转移其所有权。

match account {
  Account {ref name ,ref language,..}=> ..........
}

ref是借用已匹配值的一部分,ref mut 是借入可变引用的。

与ref相对的是&模式,以&开头的会匹配到引用。

match sphere.center(){
  &Pointer(x,y,z) => .....

center会返回对sphere中私有字段的引用,该模式会匹配到这些引用。

在表达式中,&会创建一个引用。而在模式匹配中,&会匹配到一个引用。
在表达式中,元组会将两个值合并为一个元组,而在模式匹配中,会将一个元组拆成两个值。

匹配守卫

模式可以创建新的局部变量

match something(hex) {
  None => Err("ooooo"),
  Some(word) => println!("you  can say : {}。",word)
}

可以使用这个临时变量,并且可以在match中使用if对变量尽心判断。
匹配守卫方式:

match something(hex){
  None => Err("00000"),
  Some(word) if word == you_wanted => println!("you wanted say :{}。",word)
  Some(word) => Ok(word)
}

如果匹配到你想说的词,就会执行,否则会继续匹配下一行。

可以用|来匹配多个可能性,如果能匹配到任何一个子模式,则匹配成功

模式绑定

x @ pattern 的方式是模式绑定,匹配成功时,它会创建变量x,将整个匹配的值移动到x里。

当你的pattern需要对一个数值解包时,例如元组和结构体,而匹配后又需要对整个值进行操作,可以采用这种方式。
先保存在变量中,然后再匹配后的操作里使用这个变量。

还可以用于范围模式

match chars.next(){
  Some(digit @ '0'..='9') => read_number(digit,chars),
}

模式匹配用途

  • 将结构体解包成多个变量
  • 解包某个元组
  • 迭代hashmap的键和值
  • 自动对闭包解引用

可反驳模式是一种可能不会匹配的模式。
if let 和while let 也允许使用可反驳的模式,可以用于:

  • 处理只有一个枚举值的特例
  • 当查表成功时才会运行
  • 重复尝试某些事情,直到成功
  • 在某个迭代器上手动循环

特型

  • 为类型添加特型不需要额外的内存
  • 内置特型其实时Rust为运算符重载和其他特型提供的语言级钩子
  • 泛型函数会在界限中使用特型来阐明它能针对哪些类型的参数进行调用

特型对象

在Rust中使用特型编写多态代码有两种方式:特型对象和泛型。

对于特型的引用称为特型对象,特型对象指向某个值,它具有生命周期,可以是可变和共享的。
特型对象无法在编译期间确定引用的类型,因此特型对象需要包含一些引用目标类型的额外信息。
在内存中,特性对象指向值的指针和表示该值类型的虚表的指针。
在rust中,虚表指挥在编译器生成一次,并由同一类型的所有对象共享。
Rust在需要时会将普通引用转换为特性对象,这种转换也是创建特型对象的唯一方法。
在发生转换的地方,Rust知道引用目标的真实类型,因此它只要加上适当的虚表地址,把常规指针变为一个胖指针就可以了。

在使用泛型函数时,Rust会从参数类型推断出泛型的具体类型,进行单态化,为每个类型生成一份单独的机器码。
如果你调用的泛型函数没有任何能提供对于类型推断有帮助的线索,需要明确写出类型。
生命周期不会对机器码产生影响,对一个泛型函数两次调用使用了相同的类型,但不同的生命周期,会调用同一个编译结果函数,
只有不同类型才会让rust编译出泛型函数的多个版本。

如何选择特型对象还是泛型

  • 当需要一些混合类型值的集合,特型对象时正确的选择。例如某一类食物,某一种动物
  • 特型对象还有一个好处是减少编译后的代码体积。相比较使用泛型函数而言

使用泛型函数,Rust会根据每种类型来编译代码,会导致二进制文件过大。不过泛型函数通常具有更多的优点。

  1. 泛型函数根据类型编译代码,运行速度会很快。不需要动态确认类型,可以让编译器内联函数到特定部分。
  2. 不是所有特型都能支持特性对象,特型中的关联函数等特性只能在泛型中使用。
  3. 泛型可以很容易指定具有多个特型的泛型参数边界。特性对象无法做到。

在实现特型时,特型或者类型,二者必须至少一个是在当前crate中新建的,这叫做孤儿规则。它帮助rust确保特型的实现是唯一的。

可以声明一个特型是另一个特型的扩展,类似其他语言的子接口,但rust中,子特型不会继承关联项。

实际上子特型只是针对self的类型界限简写。

trait Creature where Self:Visible{
}

定义类型之间关系的特型

一些特型可以描述类型之间的关系

  • std::iter::Iterator 为每个迭代器类型与其生成值的类型建立联系
  • std::ops::Mul 特型可以相乘的类型有关

关联类型

迭代器特型中第一个类型是type Item,就是关联类型,实现了Iterator的每种类型都必须指定
它生成的条目的类型。

迭代器的第二个就是next方法,它返回值使用了关联类型:OptionSelf::Item

impl trait

impl trait 可以让我们擦除类型,仅指代它的一个或多个特型。而无须进行动态分配。
impl trait 提高了代码可读性,意味着你将来可以更改返回的实际类型。使用impl trait方式来编写函数签名
,可以不需要更改就可以用于多种类型,或者多种迭代器的场景。
impl trait是一种静态派发的形式,编译器需要在编译时就获得具体类型,从内存上分配正确的空间。
Rust不允许特型方法使用impl trait作为返回值。只有自由函数和关联具体类型的函数才能使用impl trait方式

关联常量

关联常量在特型中可以声明,但不能为其定义值。而特型的实现者可以定义这些值。
关联常量不能与特性对象一起使用,在编译器选择正确的值,编译器会依赖相关实现的类型信息。

posted on 2024-01-08 08:13  Tmacy  阅读(23)  评论(0编辑  收藏  举报

导航