Featured image of post RUST 学习记录

RUST 学习记录

分享学习RUST的过程(不保证完全准确)

RUST 的数据类型

整数类型

无符号整数

类型 范围
u8 0~28-1
u16 0~216-1
u32 0~232-1
u64 0~264-1
u128 0~2128-1
usize 0~232-1 or 0-264-1

usize取决于系统位数,32为32,64为64.

有符号整数

类型 范围
i8 -27~27-1
i16 -215~215-1
i32 -231~231-1
i64 -263~263-1
i128 -2127~2127-1
isize -231~231-1 or -263~263-1

isize同上取决于系统位数,32为31,64为63.

浮点类型

类型 精度 范围
f32 IEEE 单精度(至少6位小数) 约-3.4×1038~3.4×1038
f64 IEEE 双精度(至少15位小数) 约-1.8×10308~1.8×10308

布尔类型

  • True & False

字符类型

  • Rust的字符类型以32位值的形式表示单个Unicode字符
    • 这使得它可以支持i18n甚至包括emoji

数组

  • 最简单的形式,将数据直接括起来:[1,1,2,3,5,8,13]
  • *不要把数组局限于数;如["hello", "rust", "love", "you"]
    也是一个合法的数组
  • 可以通过[V; N]来生成一个相同元素的数组;
    • 其中,V为每个元素的值[Value], N为数据个数[Number].

元组

  • 元组不同于数组,元组中的元素可以是任何类型
    例如("rust", 520)是一个元组,它的类型是(&str, i32)
  • 访问元组中元素的方法tup.0, tup.1, tup.2
    index从0开始
  • *零元组:()

指针类型

引用

&xx类型的值就是对一个xx值的引用

&T : 不可变引用;
&mut T : 可变引用.

Box

let tup_1 = (12, "eggs");
let box_1 = Box:new(tup_1); //在堆中分配一个元组
  • box分配的元组在堆内存中,即涉及到所有权

原始指针

  • *mut T
  • *const T

    星号[*]还可以表示解引用

向量

向量Vec<T>分配在堆内存上[可变]

let mut vec_1 = vec![2, 3, 5, 7];
  • 以上使用了vec!宏创建向量
vec_1.push(11);
vec_1.push(13);
  • 以上使用push()vec_1向量中添加元素

切片

以下先创建数组与向量

let vec_1: Vec<f64> = vec![0.0,  0.707,  1.0,  0.707];
let arr_1: [f64; 4] =     [0.0, -0.707, -1.0, -0.707];

首先来介绍普通引用

let sv: &[f64] = &vec_1;
let sa: &[f64] = &arr_1;

以上创建了不可变引用
sv和sa的指针指向源数据而不是创建了新的数组和向量
此时svsa输出结果和vec_1arr_1相同

若想获得特定的元素或者元素片段,则需要进行切片

切片

可以使用如下语句来将数组或者向量切片

print(&vec_1[0..2]); //打印vec_1的前两个元素
print(&arr_1[2..]); //从a[2]开始打印arr_1的元素
print(&sv[1..3]); //这是有趣的一部分,因为它对一个切片进行了切片!

字符串

字符串字面量

字符串字面量放置于双引号中;例如"hello rust!" 这种形式的数据存储于栈内存中,因此无需考虑所有权机制.

字符串

以下方式创建了字符串(String);

let str_1 = "hello rust".to_string();

需要注意的是,字符串(String)存储在堆内存中,因此在使用时需要考虑所有权机制!
下一章将介绍所有权机制!

所有权

rust通过所有权机制而不是垃圾回收机制来确保内存安全

转移

在rust中,给变量赋值的操作通常不会创建新值,而是将新的指针指向旧值(栈数据除外)

let v_1 = "Hello rust".to_string();
let v_2 = v_1;

上述代码创建了两个变量为v_1v_2并且他们的值都为Hello rust,在内存中,这两个变量指向的是同一个地址,同一个数据.

同理,为函数传递参数也是这种结果.

值得注意的是,存储在栈内存中的数据在传参等操作后,原值通常可用(这也称作copy,也就是会产生新的数据.),而存储在堆内存中的数据经过这样的操作后原值会变为未初始化的状态.

let x = vec![10 20 30];
if c {
  f(x); //传入x
} else {
  g(x); //同上
}
h(x) //Error!,因为x此时是未初始化状态

引用

rust有一种不获取源数据所有权来调用源数据内容的方式叫做引用 可以使用&e来获得一个对e的引用,和rust的变量部分一样,引用通常也是不可变的,如果要获得可变引用,则需要&mut e

  • rust不允许同时存在可变引用和不可变引用,但是允许同时存在多个不可变引用.
  • rust也允许对引用进行引用,例如
  • struct Points {x: i32, y: i32}
    let point = Points {x: 1000, y: 2000};
    let r: &Points = &point;
    let rr: &&Points = &r;
    

    如上给定了数据类型,但是rust也可以自行推断使用了几层引用.

  • 不允许对生命周期已经结束的数据进行引用例如
  • fn main() {
      {
        let v_1 = 42;
      }
    
      println!(&v_1); //此处非法,因为已经离开了作用域,v_1已经被释放!
    }
    

表达式

表达式语言

在rust中,if和match可以产生值
在c语言中,流程控制工具大多数是语句,而在rust都是表达式
Rust 的 if 表达式可以用来初始化变量

let status =
    if cpu.temp <= MAX_TEMP {
    HttpStatus::Ok
} else {
    HttpStatus::Error
};

Rust 的 match 表达式可以作为参数传给函数或宏

println!("Inside the vat, you see {}.",
    match vat.contents {
        Some(brain) => brain.decs(),
        None => "nothing of interest"
    });

这就解释了Rust为什么不需要类似C语言的那种三元操作符

块与分号

代码块同时也是表达式,代码块的值就是最后一个表达式的值

let display_name = match post.author() {
    Some(author) => author.name(),
    None => {
        let network_info = post.get_network_netadata()?;
        let ip = network_info.client_address();
        ip.to_string()
    }
};

None =>之后的部分是一个块表达式,这里块的值就是最后一个表达式ip.to_string()的值

声明

通常用let来声明局部变量

let name: type = expr;

其中,类型和初始值是可选的(Rust会自动进行类型推断),而分号是必须的.

Rust 中,变量默认不可变,例如下面这段代码是会报错的

let value_1 = 5;
value_1 = 2;

如需声明可变变量,则需要使用let mut

let mut value_1 = 5;
value_1 = 2;

if 与 match

if很简单,如下

if condition1 {
    block1
} else if {
    block2
} else {
    block3
}

这里不再过多介绍,接下来是match
match有点像C语言的switch,但是更灵活;

match code {
    0 => println!("Ok"),
    1 => println!("Wires Tangled"),
    2 => println!("User Asleep"),
    _ => println!("Unrecognized error {}", code)
}

根据code的值,四个分支只有一个会执行,其中_表示任何值;

  • 值得注意的是,match的分支是从第一条匹配到最后一条,也就是说如果将第四条放在顶端那么这段代码任意code都会输出Error

if let 表达式

if let表达式只是对match表达式的简写,if let可以实现的功能完全可以由match实现

if let pattern = expr {
    block1
} else {
    block2
}

这个结构实现的效果是:expr要么匹配pattern,运行block1,要么不匹配pattern,运行block2

循环

Rust中有四种循环实现方式

while condition {
    block
}

while let pattern = expr {
    block
}

loop {
    block
}

for pattern in collection {
    block
}

循环在rust中也是表达式但是循环产生的值是()

  • while循环和C类似
  • while let循环和if let类似,要么匹配后运行后面的代码,要么不匹配后退出循环
  • loop是死循环,除非遇到retrun或者break或者程序挂了
  • for循环,很简单,遍历.
  • 一般的,进行有限次循环时通常采用for遍历数组的形式,而不是loop或者while
    for i in 0..20 {
        println!("{i}");
    }
    

return 表达式

return表达式退出当前函数,并且向调用者返回值. 通常使用return;来代表return ();

函数与方法调用

一般地,有以下规则

let x = gcd(1302, 462); //函数调用

let room = player.location(); //方法调用

同时,还有对静态方法的调用

let mut numbers = Vec::new(); //静态方法调用
  • 静态方法与非静态方法的区别是静态方法通过类型调用,而非静态方法通过值调用
  • 方法也可以链式调用
    Iron::new(router).http("localhost:3000").unwarp();
    
  • 需要注意的是,通常用于函数调用或方法调用的语法不能用于泛型Vec 所以需要使用::<T>,而不是<T>:例如
    return Vec::<i32>::with_capacily(1000);
    

字段与元素

game.black_pawns //结构体字段
coords.1         //元组元素
pieces[1]        //数组元素

此处还涉及到切片,我们以上提到过这里不再赘述
需要注意的是,在使用a..b这种结构来表示切片范围时,Rust会将其理解为一个半闭半开区如

0..5 
0 <= i < 5 
[0, 5)

引用操作符

& &mut在之前介绍过,这里依然不再重复

一元操作符*用于访问引用指向的值(解引用)

算数、位、比较和逻辑操作符

加减乘除和其他语言是类似的,这里不再介绍 ####一元-操作符:将数值取反(不可用于无符号类型)

逻辑运算符

其他逻辑运算符和C语言类似,这里不再介绍

  • 按位非:!而不是~

赋值

  • 赋值操作符=用于把值赋给mut变量以及他们的字段和元素
    let x = 1;
    
  • rust支持复合赋值例如+=-=
  • rust没有像C语言那样的递增运算符++和递减--运算符

类型转换

let x = 1;
let index = x as usize;

闭包

Rust有闭包,即类似函数的轻量值

let is_even = |x| % 2 == 0;
  • 闭包将在之后详细介绍

优先级

表达式类型 示例 相关特型
数组字面量 [1, 2, 3]
重复的数组字面量 [0; 50]
元组 (6, “crullers”)
分组 (2+2)
{ f(), g() }





控制流表达式
if ok { f() }
if ok { 1 } else {0}
if let Some(x) = f() { x } else { 0 }
match x {None => 0, _ => 1}
for v in e { f(v); }
while ok { ok = f(); }
while let Some(x) = it.next() { f(x); }
loop { next_event(); }
break
continue
return 0





std::iter::IntoIterator
宏调用 println!(“ok”)
路径 std::f64::consts::PI
结构体字面量 Point {x: 0, y: 0}
元组字段存取 pair.0 Deref、DerefMut
结构体字段存取 point.x Deref、DerefMut
方法调用 point.translate(50, 50) Deref DerefMut

函数调用

stdin()
Fn(Arg0, …) -> T
FnMut(Arg0, …) -> T
FnOnce(Arg0) -> T
索引 arr[0] Index, IndexMut
Deref, DerefMut
错误检查 create_dir(“tmp”)?
逻辑/按位非 !ok Not
取反 -num Neg
解引用 &ptr Deref, DerefMut
借用 &val
类型转换 x as u32
n * 2 Mul
n / 2 Div
n % 2 Rem
n + 2 Add
n - 2 Sub
左移 n « 1 Shl
右移 n » 1 Shr
按位与 n & 1 BitAnd
按位异或 n ^ 1 BitXor
按位或 n | 1 BitOr
小于 n < 1 std::cmp::PartialOrd
小于等于 n <= 1 std::cmp::PartialOrd
大于 n > 1 std::cmp::PartialOrd
大于等于 n >= 1 std::cmp::PartialOrd
等于 n == 1 std::cmp::PartialEq
不等于 n != 1 std::cmp::PartialEq
逻辑与 x.ok && y.ok
逻辑或 x.ok || backup.ok
范围 start..stop
赋值 x = val





复合赋值
x *= 1
x /= 1
x %= 1
x += 1
x -= 1
x «= 1
x »= 1
x &= 1
x ^= 1
x |= 1
MulAssign
DivAssign
RenAssign
AddAssign
SubAssign
ShlAssign
ShrAssign
BitAndAssign
BitXorAssign
BitOrAssign
闭包 |x, y| x + y

错误处理

诧异

程序本身存在bug,编译器会诧异.

  • 越界访问数组
  • 整数被0除
  • 在值为None的Option上调用.unwarp()
  • 断言失败(数据类型)

还有一个叫做panic!()的宏可以主动触发诧异.

结果

Rust没有异常,函数执行失败可以通过一个返回类型来表示:

fn get_weather(location: LatLng) -> result<WeatherReport, io::Error>

这个Result类型表示可能失败,如果函数执行失败时会返回一个错误的结果Err(error_value)

使用match表达式来捕获错误

match get_weather(hometown) {
  Ok(report) => {
    ···
  }
  Err(err) => {
    ···
  }
}

传播错误

Rust的?操作符可以传播错误,可以在产生任何result的表达式后面添加?。

let weather = get_weather(hometown)?;

?操作符的行为取决于这个函数是返回一个成功结果,还是一个错误结果。

  • 成功 => 打开result并取出其中的成功值
  • 错误 => 立刻从闭合函数中返回,将错误结果沿调用链向上传播。

包和模块

Rust程序由包组成,每个包都是一个rust项目

模块

模块既是Rust的命名空间,也是函数,类型,常量,等构成Rust程序或库的容器。

  • 模块与包的区别是,包主要解决项目间的代码共享问题,而模块主要解决项目内代码组织的问题 如下就是一个模块
mod sporse {
  use cells::Cell;
  pub struct Spore{
    ···
  }

  pub fn produce_spore(factory: &mut Sporangium) -> Spore {
    ···
  }

  fn  recombine(parent: &mut Cell) {
    ···
  }
}
  • 模块是特性项item的集合,比如上面的例子,关键字pub用于标记公开的特性项,如果没有标记,则此特性项为私有,在模块外部不可访问
let s = spores::produce_spore(&mut factory);  //可以
spores::recombine(&mut cell);  //Error, recombine 是私有的

模块也可以嵌套,这意味着模块可以包含模块

mod plant_structures {
    pub mod roots {
        ···
    }

    pub mod stems {
        ···
    }

    pub mod leaves {
        ···
    }
}
  • 这种写法显然过于繁琐不够优雅,我们也可以将模块写在单独的文件中

把模块写在单独的文件中

mod spores;
pub struct Spore {
    ···
}

pub fn prooduce_spore(factory: &mut Sporangium) -> Spore {
    ···
}

fn recombine(parent: &mut Cell) {
    ···
}
  • spores.rs只包含构成模块的特性项。所以包含模块的文件里不再需要任何代码来声明这是一个模块
  • 模块也可以有自己的目录,例如
fern_sim/  
┣━━━Cargo.toml  
┗━━━src/  
    ┣━━━main.rs  
    ┣━━━spores.rs  
    ┗━━━plant_structures/  
        ┣━━━mod.rs  
        ┣━━━leaves.rs  
        ┣━━━roots.rs  
        ┗━━━stems.rs  
  • 如果此时在main.rs中声明了plant_structures模块
  • pub mod plant_structures;
    
    这样会加载plant_structures/mod.rs, 而这个文件又会声明三个子模块roots, stems, leaves.

路径与导入

操作符::用于访问模块的特性,可以通过绝对路径来引用标准库特性

if s1 > s2 {
    ::std::mem::swap(&mut s1, &mut s2);
}

这样引用显然过于繁琐,我们也可以使用use关键字来直接导入模块

use std::mem;

if s1 > s2 {
    mem::swap(&mut s1, &mut s2);
}

这里的use声明会让mem在整个代码块或整个模块中成为::std::mem的局部别名

也可以使用use std::mem::swap来直接导入swap,但是通常不这样做,而是使用上面的导入方式

还可以一次性导入多个模块

use std::collections::{HashMap, HashSet}; //同时导入两个模块
use std::io::prelude::*; //导入所有模块

super & self关键字

关键字super在导入声明中有特殊含义,他是父模块的一个别名 类似super,关键字self就是当前模块的一个别名

//in proteins/mod.rs

//从一个子模块导入
use self::synthesis::synthesize;

//从一个枚举中导入名字
//这样就可以用Lys而不是AminoAcid::Lys来表示赖氨酸
use self::AminoAcid::*;

标准前置模块

标准库std会链接到每个项目,一些很常用的名字都包含在标准前奏里,会被自动导入
如果要使用标准库中的其他功能,可以使用use std::xxx

特性项

模块由特性项构成,特性项也分很多种

函数

  • 这里不再过多讨论

类型

用户定义类型通过struct, enum, trait 关键字定义。 如下是一个简单的结构体

pub struct Fern {
    pub roots: RootSet,
    pub stems: StemSet
}

类型别名

type关键字可以为现有类型声明一个新名字;

type Table = HashMap<String, Vec<String>>;

这里声明的类型Table就是这个特定的HashMap的简写。

fn show(table: &Table) {
    ···
}

impl块

方法通过impl块添加到类型上;

impl Cell {
    pub fn distance_from_origin(&self) -> f64 {
        f64::hypot(self.x, self.y)
    }
}

常量

const关键字定义常量,这个语法与let的区别在于它可以标记为pub,而且必须写明类型,并且命名使用全大写

pub const ROOM_TEMPERATURE: f64 = 20.0; //摄氏度
//static关键字定义静态特性项,和const差不多
pub static ROOM_TEMPERATURE: f64 = 68.0; //华氏度

模块

模块在之前已经讨论过

导入

useextern crate声明也是特性项,即使他们只是别名,也可以是公有的;

pub use self::leaves::Leaf;
pub use self::roots::Root;

这代表Leaf和Root为公有特性项,同时也为别名

标准前置模块就是写成了这样一系列pub导入来定义的。

extern 块

extern可以声明用其他语言编写的函数集合,以便Rust代码调用它们

我们之后会详细讨论extern


结构体

类似c中的struct于python中的类class Rust有三种结构体类型

  • 命名字段结构体
  • 类元组结构体
  • 类基元结构体

命名字段结构体

如下是一个命名字段结构体

struct GraycaleMap {
    pixels: Vec<u8>,
    size: (usize, usize)
}

这里声明的结构体类型为GrayscaleMap,它包含两个给定类型的字段:pixels&size Rust对所有类型的命名有一个约定大驼峰;字段和方法名要使用蛇形命名法

可以使用结构体表达式创建结构体的值

let width = 1024;
let height = 576;
let image = GaryscaleMap {
    pixels: vec![0; width * height],
    size: (width, height)
};

结构体表达式以类型名开头,后跟一对花括号,其中列出了每个字段的名字和值

与Rust中的其他构成项一样,结构体默认是私有的,即使用了pub,结构体中的字段也是默认私有的.

类元组结构体

第二种结构体类型叫类元组结构体,因为它类似元组;

struct Bounds(usize, usize);

创建这种结构体就和创建元组一样,只不过需要加上结构体的名称

let image_bounds = Bounds(1024, 768);

类元组结构体的值称为元素(element),跟元组的值一样.访问这些值同样和访问元组中的值一样:

assert_eq!(image_bounds.0 * image_bounds.1, 786432);

类元组结构体中的个别元素可以是公有的,也可以不是:

pub struct Bounds(pub usize, pub usize)

表达式Bounds(1024, 768)看起来像个函数调用,而实际上也是.定义这种结构体会隐式定义一个函数:

fn Bounds(elem0: usize, elem1: usize) -> Bounds { ··· }
  • 命名字段结构体适合很多时候要使用,操作符取得值的组件
  • 类元组结构体适合经常需要使用模式匹配来查询新元素

类基元结构体

类基元结构体比较难以理解,因为它完全没有元素

struct Onesuch;

这种类型的值不占内存,非常像基元类型()Rust不会把类基元结构体的值保存到内存中,也不会生成操作它们的代码
因为通过其特殊的类型就可以知道它们的值;也就是说,类基元结构体只有一个值

let o = Onesuch;

类基元结构体在使用特型的时候会有用,这点以后会介绍到

结构体布局

在内存中,命名字段结构体与类元组结构体一样都是值的集合,类型可以不同,并以特定方式存储
比如之前定义的GrayscaleMap

struct GrayscaleMap {
    pixels: Vec<u8>,
    size: (usize, usize)
}

与别的语言不同,Rust直接把pixels和size放倒GrayscaleMap值的内存里
只有pixels向量拥有自己分配在上的内存块

通过impl定义方法

可以给任意结构体定义方法,在Rust中定义方法要使用单独的impl块:

///一个后进先出的字符队列
pub struct Queue {
    older: Vec<char>, //旧元素,最老的在最后
    younger: vec<char>  //新元素,最新的在最后
}

impl Queue {
    ///把一个字符推到队列后端
    pub fn push(&mut self, c: char) {
        self.younger.push(c);
    }

    ///从队列前端取出一个字符,如果可以,取出字符返回Some(c),
    ///否则如果队列是空的,返回None
    pub fn pop(&mut self) -> Option<char> {
        if self.older.is_empty() {
            if self.younger.is_empty() {
                return None;
            }
            
            ///把younger中的元素转移到older中,
            ///并保持对外承诺的顺序
            use std::mem::swap
            swap(&mut self.older, &mut self.younger);
            self.older.reverse();
        }

        // 到这里older肯定有元素,Vec的pop方法已经返回Option,这就可以了
        self.older.pop()
    }
} 

impl块只是fn定义的集合,这些函数都会成为块顶部提到名字的那个结构体类型的方法。
这里先定义了一个公有结构体Quene,接着又给他定义了两个公有方法push&pop
方法也成为关联函数,因为它们是与特定类型关联的。与关联函数相对的叫自由函数
即不是作为impl块中的构成项定义的函数

泛型结构体

Rust结构体可以是泛型的,也就是说,这种结构体是一个模板,可以在其中插入任何类型

pub struct Queue<T> {
    older: Vec<T>,
    younger: Vec<T>
}

可以把Queue中的读作“对于任何元素T······”。因此,上面的定义就可以翻译成
“对于任意类型T,Queue<T>包含两个Vec<T>类型的字段。”比如,对于Queue<String>,
T是String,因此older和younger的类型都是Vec。事实上,Vec本身也是一个泛型结构体

在泛型结构体的定义中,位于尖括号<>中的类型名叫做类型参数。与泛型结构体对应的impl块类似如下:

impl<T>  Queue<T> {
    pub fn new() -> Queue<T> {
        Queue { older: Vec::new(), younger: Vec::new() }
    }

    pub fn push(&mut self, t: T) {
        self.younger.push(t);
    }

    pub fn is_empty(&self) -> bool {
        self.older.is_empty() && self.younger.is_empty()
    }

    ···
}

第一行中的impl<T> Queue<T>可以理解为“对于任意类型T,这里给Queue定义几个方法”
之后就可以在方法定义中使用类型参数T作为一种类型了

为了使代码简单,impl块还定义了一个特殊类型参数Self,表示要把方法添加到其中的任意类型
因此可以让Queue::new的定义再简短一些

pub fn new() -> Self {
    Queue { older: Vec::new(), younger: Vec::new }
}

带生命周期参数的结构体

如果结构体类型包含引用,则必须指定这些引用的生命周期,比如

struct Extrema<'elt> {
    greatest: &'elt i32,
    least: &'elt i32
}

类比之前的,可以将以上代码理解为给定任意生命周期'elt,都可以创建一个包含具有该生命周期引用的Extrema<'elt>

为结构体类型派生共有特型

结构体可以很简单

struct Point {
    x: f64,
    y: f64
}

然而,根据定义,这个结构体不可以被复制、克隆、打印、还不支持== 和 !=

上述每个特性在Rust中都有一个名字:Copy,Clone,Debug,PartialEq
它们被称为特型(trail)

需要额外自定义行为时,Rust也可以自动实现,只需要加上#[derive]属性

#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
    x: f64,
    y: f64
}

未完待续

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-10-18 21:50 CST
发表了2篇文章 · 总计8.68k字
使用 Hugo 构建 · 主题 Stack 由 Jimmy 设计