Rust 学习笔记(基础篇)

Rust 学习笔记

文章目录

  • Rust 学习笔记
    • 基础篇
      • 包管理器 cargo
        • 创建项目
        • crate type
      • 命名
      • 格式化输出
        • print! println! format!
        • eprint! eprintln!
        • {} {:?} {:#?}
          • Debug
          • Display
        • 位置参数
        • 具名参数
        • 格式化参数
          • 精度
          • 进制
          • 指针地址
          • 转义
      • 变量与可变性
      • 隐藏 Shadowing
      • 数据类型
        • 复合类型
      • 函数
      • 控制流
      • 所有权
        • 所有权规则
        • 内存与分配
        • 所有权与函数
        • 返回值与作用域
        • 引用(借用)
        • 悬空引用
      • 切片
      • 定义和实例化
        • 基本使用
        • 更新语法
        • Tuple struct
        • 空结构体
        • 打印 struct
        • 标准错误输出
        • struct 方法
        • 关联函数
      • 枚举与模式匹配
        • enum
        • Option
        • match
        • if let
      • Package、Crates、Modules
        • 模块定义
        • 公开模块与公开函数
        • 使用模块 use/pub use/as
        • 模块嵌套
        • 创建 crate
        • 路径
      • 将模块内容放到其他文件
      • Collection
        • 介绍
        • 时间复杂度
        • Vec
          • 创建
          • 操作
        • HashMap
          • 创建
          • 操作
        • HashSet
          • 创建
          • 操作
        • String
          • 新建 String
          • 更新 String
          • 索引 String
          • 字符串 slice
          • 遍历字符串
      • 错误处理
        • panic!()
          • 栈展开与终止
          • backtrace
        • Result
          • 使用方式
          • unwrap()与expect()
        • 传播错误
        • 简化传播错误
      • 泛型、Trait
        • 泛型基础
        • 泛型性能
        • Traits 特质
          • 概念
          • 为 struct 实现 trait
          • trait 作为参数
          • trait bound
          • 返回值为trait
          • 修复 largest 函数
          • 有条件地实现方法
        • 泛型函数
      • 生命周期
        • 基本概念
        • 泛型生命周期
        • 生命周期标注语法
        • 深入理解生命周期
      • IO
        • 标准读写
        • 命令行参数
      • 文件读写
        • 打开并读取文件(只读)
        • 创建文件(可写)
        • 写入文件
        • 追加内容到文件末尾
        • 删除文件
        • 复制文件
      • 迭代器
        • iter()
        • into_iter()
        • iter_mut()
        • 取元素
          • next()
          • take(k)
          • nth(k)
          • last()
        • 变换
          • rev()
          • skip(k)
          • step_by(k)
          • chain()
          • zip()
          • map()
        • 求值结算
          • max、min、count、sum
          • fold
      • 闭包
        • 定义
        • 调用
    • 参考

基础篇

包管理器 cargo

####常用命令

cargo -h	# 查看帮助信息
cargo --list	# 查看所有命令

cargo new <project_name> # 新建 cargo 项目
cargo build	# 编译当前项目,构建可执行文件,cargo build --release 是编译时会进行优化,代码运行的更快,但是编译时间更长
生成在 target/release 而不是在 target/debug 
两种配置,一个开发,一个正式发布
cargo run	# 构建和运行项目
cargo check	# 分析当前项目并报告项目中的错误,但不会编译任何项目文件,检查代码,确保能通过编译
cargo update	# 更新当前项目中的 Cargo.lock 文件列出的所有依赖
cargo clean # 移除当前项目下的 target 目录及目录中的所有子目录和文件

如果对某个命令不熟悉,例如 new,可以直接使用 cargo help new查看详细信息。

创建项目

# 创建一个可执行的二进制程序 (cargo new project_name 也是创建一个可执行工程)
cargo new project_name --bin

# 创建一个库
cargo new project_name --lib

它们的 toml 完全一样,区别仅在于 src 目录下,可执行工程是一个 main.rs,而库工程是一个 lib.rs。这是因为 main.rs 和 lib.rs 对于一个 crate 来讲,是两个特殊的文件名。rustc 内置了对这两个特殊文件名的处理(当然也可以通过 Cargo.toml 进行配置,不详谈),我们可以认为它们就是一个 crate 的入口。

生成的 cargo.toml 是 cargo 的配置格式,[package] 是一个区域标题,表示下面的都是用来配置包的,[dependency] 是另一个区域的开始,他会列出项目的依赖项

在rust里面,代码包/代码库称作 crate,可执行 crate 和库 crate 是两种不同的 crate。

crate type

参考 文章

crate 有多种类型,执行命令

$ rustc --help|grep crate-type

# 输出
$ rustc --help|grep crate-type
        --crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
  • bin:二进制可执行 crate,编译出的文件为二进制可执行文件,必须要有 main 函数作为入口。这种 crate 不需要再 toml 中具体指定
  • lib:统称,它其实并不是一种具体的库,它指代后面各种库 crate 中的一种,如果什么都不配置,默认指的是 rlib
  • rlib:Rust Library 特定静态中间库格式。如果只是纯 Rust 代码项目之间的依赖和调用,那么,用 rlib 就能完全满足使用需求。它是一个 ar 归档文件
  • dylib:动态库
  • cdylib:c规范动态库
  • staticlib:静态库
  • proc-macro:过程宏 crate,这种 crate 里面只能导出过程宏,被导出的过程宏可以被其它 crate 引用。

命名

函数命名 this_is_a_function()

但变量命名为驼峰命名CamalCase

格式化输出

这种要详细讲其实特别复杂,以下仅列举几个经常使用的

print! println! format!

  • print! 将格式化文本输出到标准输出,不带换行符
  • println! 同上,但是在行的末尾添加换行符
  • format! 将格式化文本输出到 String 字符串
fn main() {
    let s = "hello";
    println!("{}, world", s);
    let s1 = format!("{}, world", s);
    print!("{}", s1);
    print!("{}\n", "!");
}

eprint! eprintln!

仅应该被用于输出错误信息和进度信息

eprintln!("Error: Could not complete task")

{} {:?} {:#?}

rust不像其他语言有 %s、%d需要程序员指定,而是使用{},自动推导。{:?}也是占位符,区别是

  • {} 适用于实现了 std::fmt::Display 特征的类型,用来以更优雅、更友好的方式格式化文本,例如展示给用户
  • {:?} 适用于实现了 std::fmt::Debug 特征的类型,用于调试场景
  • {:#?}{:?} 几乎一样,唯一的区别在于它能更优美地输出内容:

说白了,调试代码选择 {:?},其他选择 {}

Debug

如何 debug?对于数值、字符串、数组,可以直接使用 {:?} 进行输出,但是对于结构体需要在struct上加上注解 #[derive(Debug)],表示派生自 Debug,例如

#[derive(Debug)]
struct Person {
    name: String,
    age: u8
}

fn main() {
    let i = 3.1415926;
    let s = String::from("hello");
    let v = vec![1, 2, 3];
    let p = Person{name: "sunface".to_string(), age: 18};
    println!("{:?}, {:?}, {:?}, {:?}", i, s, v, p);
}
Display

实现了 Display 特征的 Rust 类型并不多,需要我们自定义想要的格式化方式:

let i = 3.1415926;
let s = String::from("hello");
let v = vec![1, 2, 3];	// 报错 实现 Display 特征
let p = Person {	// 报错 实现 Display 特征
    name: "sunface".to_string(),
    age: 18,
};
println!("{}, {}, {}, {}", i, s, v, p);

v和p不能像派生 Debug 一样取派生 Display。有以下三种方式解决

  • 使用 {:?}{:#?}
  • 为自定义类型实现 Display trait
  • 使用 newtype 为外部类型实现 Display trait

自定义类型实现 Display

如果类型是定义在当前作用域中的,那么可以为其实现 Display trait,可用于格式化输出

struct Person {
    name: String,
    age: u8,
}

use std::fmt;
// trait 只要实现 Display 特征中的 fmt 方法即可
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "姓名{},年龄{}",
            self.name, self.age
        )
    }
}
fn main() {
    let p = Person {
        name: "alex".to_string(),
        age: 18,
    };
    println!("{}", p);
}

为外部类型实现 Display trait

无法直接为外部类型实现外部特征,但是可以使用 newtype 解决此问题。newtype就是使用元组结构体将已有的类型包裹起来,比如下面的例子。

struct Array(Vec<i32>);

use std::fmt;
impl fmt::Display for Array {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "数组是:{:?}", self.0)
    }
}
fn main() {
    let arr = Array(vec![1, 2, 3]);
    println!("{}", arr);
}

Array 就是 newtype,它将想要格式化输出的 Vec 包裹在内,最后只要为 Array 实现 Display 特征,即可进行格式化输出

位置参数

可以指定参数位置,索引从0开始

fn main() {
    println!("{}{}", 1, 2); // =>"12"
    println!("{1}{0}", 1, 2); // =>"21"
  	println!("{1}{}{0}{}", 1, 2); // => 2112
}

具名参数

可以为参数指定名称,需要注意的是,带名称的参数必须放在不带名称参数的后面

fn main() {
    println!("{argument}", argument = "test"); // => "test"
    println!("{name} {}", 1, name = 2); // => "2 1"
    println!("{a} {c} {b}", a = "a", b = 'b', c = 3); // => "a 3 b"
}
// 报错
println!("{abc} {1}", abc = "def", 2);

格式化参数

这边主要讲一下常用的精度、进制、指针地址,其余可参考圣经

精度
fn main() {
    let v = 3.1415926;
    // 保留小数点后两位 => 3.14
    println!("{:.2}", v);
  
    // 带符号保留小数点后两位 => +3.14
    println!("{:+.2}", v);
  
    // 不带小数 => 3
    println!("{:.0}", v);
  
    // 通过参数来设定精度 => 3.1416,相当于{:.4}
    println!("{:.1$}", v, 4);

    let s = "hi我是Sunface孙飞";
    // 保留字符串前三个字符 => hi我
    println!("{:.3}", s);
  
    // {:.*}接收两个参数,第一个是精度,第二个是被格式化的值 => Hello abc!
    println!("Hello {:.*}!", 3, "abcdefg");
}
进制

使用 # 号来控制数字的进制输出:

  • #b, 二进制
  • #o, 八进制
  • #x, 小写十六进制
  • #X, 大写十六进制
  • x, 不带前缀的小写十六进制
fn main() {
    // 二进制 => 0b11011
    println!("{:#b}", 27);
  
    // 八进制 => 0o33
    println!("{:#o}", 27);
  
    // 十进制 => 27
    println!("{}", 27);
  
    // 小写十六进制 => 0x1b
    println!("{:#x}", 27);
  
    // 大写十六进制 => 0x1B
    println!("{:#X}", 27);

    // 不带前缀的十六进制 => 1b
    println!("{:x}", 27);

    // 使用0填充二进制,宽度为10 => 0b00011011
    println!("{:#010b}!", 27);
}
指针地址
let v= vec![1, 2, 3];
println!("{:p}", v.as_ptr()) // => 0x600002324050
转义

有时需要输出 {},但这两个字符是特殊字符,所以 {使用{转义,}使用}转义。

fn main() {
    // {使用{转义,}使用} => Hello {}
    println!("Hello {{}}");
}

变量与可变性

  • 声明使用 let 关键字,变量都是不可变的,使用 mut 关键字使其可变
  • 常量,在绑定值以后不可变,与不可变量有区别
    • 不可以使用 mut
    • 使用 const 声明,类型必须被标注
    • 可在任何作用域声明
    • 命名所有字母大写,const MAX_VALUE:u32 = 100_000

隐藏 Shadowing

  • 可以使用相同的名字声明新的变量,新的变量就会 shadow(隐藏) 之前的变量
let x = 5
x = 5 + 1 ❌

let x = 5
let x = x + 1; ✅

数据类型

rust 是静态编译语言,在编译时需要知道所有变量类型

####标量类型

一个标量类型代表一个单个的值,主要有4种标量类型

  • 整数类型

    • 例如U32,无符号整数,占32位,0~2^31-1

    • 无符号 u 开头,有符号 i 开头
      
      8bit		i8 	u8
      16bit 	i16 u16
      32bit		i32 u32
      64bit 	i64 u64
      128bit 	i128 u128
      arch 		isize	usize
      
      isize、usize类型 位数由程序运行的计算机的架构决定,如果是 64 位计算机,那就是64位的,使用场景主要是对某种集合进行索引操作
      
    • 整数字面值
      
      十进制 	Decimal 98_222 	可以加下划线
      十六进制 Hex		0xff
      八进制		Octal		0o77
      二进制		Binary	0b1111_0000
      字节 		Byte(u8 only) b'A'
      
    • 除了 byte 类型以外,所有数值字面值都允许使用类型后缀,例如 57u8

    • rust 默认类型 i32

    • 整数溢出:u8范围0-255,如果把u8设置为 256,那么,调试模式下编译,如果发生溢出,程序运行时会panic,发布模式下(release),rust 不会检查可能导致 panic 的溢出,如果溢出,rust执行环绕操作,256变0,257变1,依此类推

  • 浮点类型

    • f32、f64,f64是默认类型,精度更高
  • 布尔类型

    • true、false,占一个字节
  • 字符类型

    • char,字符类型使用单引号,占4字节

复合类型

可以将多个值放在一个类型里,提供了两种基础复合类型 tuple、数组

  • tuple

    • 长度固定,一旦创建,无法更改,元素类型不必相同

    • fn main(){
      	let tup:(i32,f64,u8)  = (500,6.4,1);
      	
      	let (x,y,z) = tup;	// 解构 tup 的值
      	printli!("{},{},{}",tup.0, tup.1, tup.2)	// 访问 tup 元素
      }
      
  • 数组

    • 可以将多个值放在一个类型里,每个元素类型必须相同,长度固定,不过一般用 vector
      
      // 声明方式一
      fn main(){
      	let a = [1,2,3,4,5];
        
      }
      
    • 数组类型 :[类型;长度]
      
      // 声明方式二
      let a:[i32;5] = [1,2,3,4,5]
      
    • // 声明方式三
      如果数组中每个值都相同 [初始值; 长度]
      
      let a = [0;5];	// let a = [0,0,0,0,0]
      
    • 使用索引访问,和其他语言一样

函数

  • 在 rust 里,返回值就是函数体里最后一个表达式的值
  • 若想提前返回,需使用 return 关键字,并指定一个值,大多数都是默认使用最后一个表达式作为返回值
  • 最后的表达式不要加 ;,加了就是语句,不是表达式

控制流

  • if-else,和其他语言类似,但如果使用多个 else if,请使用 match 重构

  • if 是表达式,所以可以将它放在 let 语句中等号的右边

    • fn main{
      		let num = if condition {5} else {6};	// if -else 要求返回结果类型一致,在编译时就得确定类型
      }
      
  • 循环

    • loop、while、for

    • loop:反复执行,直到被 break

      • fn main{
        		let mut cnt = 0;
        		let result = loop{
        				cnt += 1;
        				
        				if cnt == 10{
        						break cnt*2;	// 如果 cnt 为10,break 后返回 cnt*2
        				}
        		};
        }
        
    • while:和其他语言一样

      • while num != 0{
        		num = num - 1;
        }
        
    • for

      • fn main{
        		let a = [1,2,3,4,5];
        		for elem in a.iter(){
        				printlin!(elem);
        		}
        }
        
    • Range(start..end)左闭右开,rev()可以反转 Range,
      
      fn main{
      		let a = [1,2,3,4,5];
      		for elem in (1..4).rev(){
      				printlin!(elem);	//3,2,1
      		}
      }
      

所有权

https://mp.weixin.qq.com/s/2kwlT9K5A4RQxQh9TJhu4w,这篇写的很好

无需gc即可保证内存安全,某些语言需要显式地分配和释放内存,rust采用所有权管理,包含一组编译器在编译时检查的规则,当程序运行时,所有权特性不会减慢程序的运行速度,因为 rust 把内存管理工作都提到了编译时去完成。

所有权解决的问题

  • 跟踪代码哪些部分正在使用heap的哪些数据
  • 最小化 heap上重复数据量
  • 清理 heap 上未使用的数据避免空间的不足
  • 一旦懂得所有权,不需要考虑堆栈,管理 heap 数据才是所有权存在的原因

所有权规则

  1. 每个值都有一个变量,变量为该值的所有者,叫做Owner;
  2. 一个值同时只能有一个owner;
  3. 当所有者离开了自己的作用域(Scope),那么值就会被丢掉。

内存与分配

对于某个值来说,当拥有它的变量走出作用范围,内存会立即自动的交还给操作系统(调用 drop())

变量和数据的交互方式: move

堆上的数据,深浅拷贝

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);	// ❌,编译时报错,Rust在s1赋值给s2时,会认为s1已经无用了,将其直接标识为无效。所以后面的释放就不用考虑s1了。

改为深拷贝

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

栈上的数据,复制

fn main(){
  	let x = 5;
  	let y = x;
  	println!("{},{}",x,y);	// x和y仍然有效,在编译时就确定了大小,且存储在 stack 中,对于这些值的赋值操作永远都是快速的,对于这些类型而言,深浅拷贝无区别
}

所有权与函数

语义上,把值传递给函数和把值赋给变量是类似的。将值传递给函数也会发生移动复制

函数调用传值时的 move

fn main() {
    let s = String::from("hello"); 

    takes_ownership(s);	// s 把值 move 给函数后,s就失效了,如果在此之后调用s就会编译报错
  
  	let x = 5;	// 基础类型,实现了 copy trait ,复制操作,传递的是副本
	  makes_copy(x);
}
fn takes_ownership(str: String) { 
    println!("{}", str);
} 
fn makes_copy(num: i32) { 
    println!("{}", num);
} 

因此,每一个在heap内存中保存的值,只能有一个“拥有者”(Owner),也就是保存了这个内存地址的变量,一旦换了其他变量来保存,也就是换了“拥有者”,原来的拥有者就失效了。

返回值与作用域

函数在返回值的过程中同样会发生所有权的转移

fn main(){
  	let s1 = gives_ownership();
}

fn gives_ownership() -> String {
  	let str = String::from("aaa");
	  str	// 所有权也 move 到调用它的函数中,即 main
}

一个变量的所有权总是遵循同样的模式

  • 把一个值赋给其他变量时就会发生移动
  • 当一个包含 heap 数据变量离开作用于时,它的值就会被 drop() 清理,除非数据移动到另一个变量上了

引用(借用)

前面的那个例子中,s一旦传给了函数,本身就失效了,因为换了Owner。如果我们后面的代码还想使用s,那就要换一种方式来给函数传值

引用:允许引用某些值而不取得其所有权,解引用用*,和cpp一样

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);	// 传递引用

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

把引用作为函数参数这个行为叫做借用,且不能修改借用的东西!

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);	// 传递引用

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
	  s.push_str("aaa");	// 编译出错,不可变借用
    s.len()
}

可变引用(借用)

fn main() {
    let mut s1 = String::from("hello");	// 变量可变

    let len = calculate_length(&mut s1);	// 传递可变引用

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &mut String) -> usize {	// 接收参数为可变引用
	  s.push_str("aaa");	
    s.len()
}

有一个重要限制,在特定作用域内,对某一块数据,只能有一个可变引用,即只能借一次可变引用!好处是在编译时防止数据竞争,数据竞争出现的情况

  • 两个或多个指针同时访问同一个数据
  • 至少一个指针用于写入数据
  • 没有使用任何机制来同步对数据的访问

rust 直接在编译时解决数据竞争。

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;	// 报错,借了两次 second mutable borrow occurs here 

println!("{}, {}", r1, r2);

可以创建新的作用域来允许非同时的创建多个可变引用

另一个限制:一个变量不可以同时拥有一个可变引用和一个不可变引用,但可以有多个不可变引用

let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // 报错,因为 s 已经借了个不可变引用了,所以不能再借出一个可变引用

println!("{}, {}, and {}", r1, r2, r3);

悬空引用

也称作悬垂引用,Dangling References/Pointer,一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了。

在 rust 里,编译器可以保证引用永远都不会进入悬空状态

fn main(){
  	let res = dangle();
}

fn dangle() -> &String {
  	let s = String::from("hello");
	  &s	// s被销毁而引用却被返回,按理来说引用将会指向一个新地址,而rust就会直接报错
}

总结,引用的规则

  • 任何给定时刻,变量只有一个可变的引用,或任意数量的不可变引用
  • 引用得一直有效

切片

字符串切片时指向字符串中一部分内容的引用,左闭右开。

let s = String::from("hello world");

let hello = &s[0..5];	// 0可省略
let world = &s[6..s.len()];	// 如果是最后一位,s.len()也可忽略,[6..]
let whole = &s[..]	// 指向整个字符串切片

字符串字面值就是切片,存储在二进制程序中,因此可以将字符串切片作为参数传递

fn main{
  	let s = "hello world";	// 此处的 s 类型 就是 &str,即字符串切片,且是不可变引用,因此字符串字面值不可变
}

在定义函数时,使用字符串切片来代替字符串引用会使api更通用。有经验的开发者采用 &str 作为参数类型,这样可以同时接收 String 和 &str 类型的参数,比如函数 fn test(s: &str)-> &str{}

  • 如果传入的是字符串切片,可以直接调用该函数
  • 如果传入的是 String,可以创建一个完整的 String 切片来调用该函数

例如

fn main(){
  	let my_str = String::from("hello");
  	let worldIndex = first_world(&mt_str[..])	// 创建字符串切片传入
  	let my_str_literal = "hello world";	// 字符串字面值就是字符串切片
  	let worldindex = first_world(my_str_literal);
 
}

fn first_world(s: &str)->&str{
    let bytes = s.as_bytes();
  	for(i,&item) in bytes.iter().enumerate(){
      	if item == b''{
        		return &s[..i];
    	  }
  	}
  	&s[..]
}

定义和实例化

基本使用

结构体的定义和赋值类似 go

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
	  // 无需按声明顺序指定,赋值顺序可改变
    let user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
}

一旦 strut 实例时可变的,那么实例的所有字段都是可变的。

struct 作为函数返回值,例如

fn build_user(email: String, username: String) -> User {
    User {
        email,	// 当字段名和对应变量相同时,就可以使用字段初始化简写方式
        username,
        active: true,
        sign_in_count: 1,
    }
}

更新语法

当想基于某个 struct 实例来创建一个新的实例的时候,可以使用更新语法,假设有一个user2,它的field中username、active、sign_in_count的值和user1一样,只有email不一样,那可以这样赋值

fn main() {
    // --snip--

    let user2 = User {
        email: String::from("[email protected]"),	// 仅修改 email 即可
        ..user1	// 更新语法,其余来自 user1,类似 es6
    };
}

Struct 所有权:struct 拥有所有数据,只要struct 实例有效,那么里面的字段也是有效的。不过需要注意,如果user1中有field发生“move”行为,那user1中那个field就失效了。就像上面这个例子,user1中的username,在赋值user2后,就失效了,后面不可以再访问。

struct 里可以存放引用,这里需要使用生命周期

Tuple struct

允许定义类似tuple的struct,整体有个名字,但里面的 field 没有名字,适用于你想给某个 tuple 起名,并让它不同于其他 tuple,而且又不需要给每个元素起名

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

空结构体

Unit-Like Struct,即没有任何字段的结构体,适用于需要在某个类型上实现某个 trait,但是在里面又没有想要存储的数据

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

打印 struct

struct默认情况下不能直接用println进行打印

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {}", rect1);	// 报错!!!!`Rectangle` doesn't implement `std::fmt::Display`

}

需要打开debug,才可以使用{:?}打印,或者{:#?}以更好看的形式打印:

#[derive(Debug)]		// derive 就是派生的意思,让 Rectangle 派生于 debug trait
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:#?}", rect1);
}

/*
rect1 is Rectangle {
    width: 30,
    height: 50,
}
*/

标准错误输出

dbg!是标准错误输出的宏,跟标准输出println对应

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };

    dbg!(&rect1);
}

struct 方法

方法和函数类似,但不同是

  • 方法是在 struct(或 enum、trait对象)的上下文中定义
  • 方法的第一个参数是 self,表示方法被调用的 struct 实例

例如

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

// 想要在 struct 中定义方法需要使用 impl 关键字
// 在里面所定义的所有方法,都从属于这个 struct。
// 可以有多个 impl 块
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

方法调用运算符,在c/c++中,有.->两种调用方式,分别是实例调用和指针调用,而 rust 会自行判断,rust没有->运算符,rust 会自动引用会解引用

p1.distance(&p2);
(&p1).distance(&p2);

以上效果相同

关联函数

可以在 impl 块中定义不把 self 作为第一参数的函数,这种函数叫做关联函数(不是方法),可以理解为 java 中的静态方法,比如 String::from()

关联函数通常用于构造器

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

// 关联函数的调用方法是使用::
let sq = Rectangle::square(3);

枚举与模式匹配

enum

TODO https://www.twle.cn/c/yufei/rust/rust-basic-enums.html

使用关键字 enum 定义

enum IpAddKind {
  	V4,
  	V6
}

创建枚举值

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

将数据附加到枚举中,且可以嵌入任意类型数据,字符串、数字、结构体等,例如

enum IpAddr {
  	V4(u8,u8,u8,u8),
  	V6(String),
}

为枚举定义方法,同 struct 一样,使用 impl 关键字

enum IpAddr {
  	V4(u8,u8,u8,u8),
  	V6(String),
}

impl IpAddr{
  	fn call(&self){
      	
  }
}

Option

定义在标准库中,包含在 Prelude(预导入模块)中,可以直接使用。使用场景:某个值可能存在(某种类型)或不存在的情况。rust 没有 null,提供了类似 Null 概念的枚举 Option

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

Option、Some(T)、None 可以直接使用

Option和T是不同类型,若想使用其中的T,需要先进行转换,避免 null值泛滥。

  1. 当Some(T)调用时,能将T类型的值取出,若T未实现Copy trait,则发生所有权转移。
  2. 当None调用时,若在编译阶段,会由编译器报错,无法通过编译;若在运行阶段,程序会Panic。
  3. 调用unwarp的Option不要求是mut的。
  4. 为了防止None调用unwarp(),除了进行None检查外,还可以选各站调用unwarp_or系列方法,让程序在检测到None时自动进行额外处理。

match

控制流运算符,允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码,模式可以是字面值、变量名、通配符等。需要注意的是,match 匹配必须穷举所有的可能,如果不想全写,可以使用_ 通配符来代替其余没列出的值。

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

匹配 Option类型枚举

#![allow(unused_variables)]
fn main() {
  let five = Some(5);
  let six = plus_one(five);
  let none = plus_one(None);
}

// 传入一个枚举,返回一个枚举
  fn plus_one(x: Option<i32>) -> Option<i32> {
      match x {
          None => None,
          Some(i) => Some(i + 1),
      }
  }

_通配符的使用,代替其余没列出的值,例如

let some_u8_value = 0u8;	// 类型后缀
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

if let

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

// 同上面效果一样,想要对 Some(3) 匹配进行操作但是不想处理任何其他 Some 值或 None 值
if let Some(3) = some_u8_value {
    println!("three");
}

也可以使用 if let - else 表达式

Package、Crates、Modules

module 模块是实现相同功能或共同实现一个功能的函数、结构体的集合。类似于 cpp 的命名空间。比模块更高级的是库 crate,可以将多个模块放到一个库,crate是 rust 的基本编译单元,rust中可执行二进制文件或一个库就是 crate。一个库包(library crate) 没有入口函数(main())。 rust 内置 cargo 作为包管理器,类似 pip,同时提供了 crates.io 用作所有第三方包的中央存储服务器。

模块定义

模块是在 crate 内将代码进行分组,module中可包含其他项如 struct、enum、常量、trait、函数等

mod module_name {
   fn function_name() {
      // 具体的函数逻辑
   }
   fn function_name() {
      // 具体的函数逻辑
   }
}

公开模块与公开函数

模块或模块内的函数需要导出为外部使用,则需要添加 pub 关键字。否则均默认是私有的。

  • 私有模块的所有函数都必须是私有的,而公开的模块,则即可以有公开的函数也可以有私有的函数。
  • 父模块无法访问子模块中私有条目,子模块可以使用祖先模块中的条目
  • 同一个文件内的根基,可以互相调用,不论公有私有

对于 struct 来说,当 struct 变为 pub,其 field 仍然是私有的;但对于 enum 来说,pub enum 的字段也是公共的(不然没啥用啊)

//公开的模块
pub mod public_module {
   pub fn public_function() {
      // 公开的方法
   }
   fn private_function() {
      // 私有的方法
   }
}
//私有的模块
mod private_module {

   // 私有的方法
   fn private_function() {
   }
}

例如

pub mod movies {
   pub fn play(name:String) {
      println!("Playing movie {}",name);
   }
}

fn main(){
   // 使用模块中的方法
   movies::play("Herold and Kumar".to_string());
}

使用模块 use/pub use/as

使用 use 关键字在文件头部预先引入需要用到的外部模块中的函数或结构体。

use public_module_name::function_name;

例如

pub mod movies {
   pub fn play(name:String) {
      println!("Playing movie {}",name);
   }
}

// 使用模块
use movies::play;

fn main(){
   play("Herold and Kumar ".to_string());
}

针对函数,建议引入到父模块,通过父模块来调用函数,这样就知道该函数是引入的还是本地内定义的;针对 struct、enum、其他类型,指定完整路径,比如使用HashMap等。

as 关键字可以为引入的路径指定本地的别名

use std::fmt::Result;
use std::io::Result as IoResult;

如何公开我们使用的模块?只需要在引用路径前加 pub 即可。

pub use std::io::Result as IoResult;

这样外部代码就可以访问 IoResult,直接导出了。

清理 use 语句

如果使用同一个包或模块下的多个条目,可以这样写

use std::{io,cmp::Ordering}

还有一种情况

use std::io;
use std::io::Write;

// 使用self代替本身,改写为
use std::io{self,Write}

可以使用 * 把路径中所有公共条目都引入到作用域,谨慎使用!

模块嵌套

允许一个模块中嵌套另一个模块,例如

pub mod movies {
   pub mod english {
      pub mod comedy {
         pub fn play(name:String) {
            println!("Playing comedy movie {}",name);
         }
      }
   }
}

使用方式同引入函数一样

use movies::english::comedy::play; 

或者可以使用全路径语法

movies::english::comedy::play("Airplane!".to_string());

创建 crate

见https://www.twle.cn/c/yufei/rust/rust-basic-modules.html

路径

  • 绝对路径:从 crate root 开始,使用 crate 名或字面值 crate
  • 相对路径:从当前模块开始,使用 self、super或当前模块的标识符

路径至少由一个标识符组成,标识符之间使用::

rust 中使用 super 关键字来表示上级目录中的内容,类似..

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();	// 从模块里进入模块外,调用serve_order
    }

    fn cook_order() {}
}

将模块内容放到其他文件

模块定义时,如果模块名后边是 ;而不是代码块,那么 rust 会找这个文件并从中加载内容,例如

mod test;

rust 就会从 src 下查找该文件,模块内的内容就写在该文件中

pub mod hosting{
  	pub fn add_to_waitlist(){
      
  }
}

注意:目录的层级结构要和模块的层级结构相匹配。如果我要创建 hosting 模块,必须在 test 文件夹下创建。

Collection

介绍

官方提供常用的数据结构实现

Rust’s collections can be grouped into four major categories:

  • Sequences: Vec, VecDeque, LinkedList
  • Maps: HashMap, BTreeMap
  • Sets: HashSet, BTreeSet
  • Misc: BinaryHeap
  • Vec:列表

  • vecDeque:双端队列

  • LinkedList:链表

  • HashMap:无序 map

  • BTreeMap:有序 map

  • HashSet:无序,等同于 HashMap,值为空元组的特定类型

  • BTreeSet:有序,等同于 BTreeMap,值为空元组的特定类型

  • BinaryHeap:优先队列,基于二叉最大堆实现

强调一下,官方文档中的介绍非常详细,且有示例,一定要好好阅读,非常有帮助,一些 map、set的互转操作见此博客

时间复杂度

对于所有的操作,集合的大小用 n 表示,如果操作中涉及到另一个集合,则包含 m个元素。有摊销成本的操作用 * 作为后缀。有预期成本的作业用 ~ 作为后缀。

摊销成本是指当容量用尽时可能需要重新调整大小。如果发生调整大小,需要O(n)时间。集合从来不会自动缩小,所以移除操作不会摊销。在足够大的一系列操作中,每次操作的平均成本将确定地等于给定成本。

只有HashMap有预期成本,这是由于散列的概率性。

get(i) insert(i) remove(i) append split_off(i)
Vec O(1) O(n-i)* O(n-i) O(m)* O(n-i)
VecDeque O(1) O(min(i, n-i))* O(min(i, n-i)) O(m)* O(min(i, n-i))
LinkedList O(min(i, n-i)) O(min(i, n-i)) O(min(i, n-i)) O(1) O(min(i, n-i))
get insert remove predecessor append
HashMap O(1)~ O(1)~* O(1)~ N/A N/A
BTreeMap O(log n) O(log n) O(log n) O(log n) O(n+m)

Vec

创建

要注意使用泛型

创建方式一,使用 new() 静态方法创建

let mut vec:Vec<i32> = Vec::new();

创建方式二,使用 vec!() 宏创建,向量的数据类型由第一个元素自动推断出来。

let vec = vec![val1,val2,val3]

创建时可指定类型

let mut vec: Vec<i32> = vec![20,30];
操作
方法 签名 说明
new() pub fn new()->Vec 创建一个空的向量的实例
push() pub fn push(&mut self, value: T) 将某个值 T 添加到向量的末尾
remove() pub fn remove(&mut self, index: usize) -> T 删除并返回指定的下标元素。
contains() pub fn contains(&self, x: &T) -> bool 判断向量是否包含某个值
len() pub fn len(&self) -> usize 返回向量中的元素个数

例如查看是否包含某个值

fn main() {
   let v = vec![10,20,30];
   if v.contains(&10) {	// 取引用?
      println!("found 10");
   }
   println!("{:?}",v);
}

遍历

for i in vec

但需要注意

fn main() {
   let mut v = Vec::new();
   v.push(20);
   v.push(30);
   v.push(40);
   v.push(500);

   for i in v {
      println!("{}",i);	// 此处,元素被 move 了,向量变不可用
   }

   println!("{:?}",v); // ❌,运行出错,因为向量已经不可用
}

修复方式就是使用引用(借用)

fn main() {
   let mut v = Vec::new();
   v.push(20);
   v.push(30);
   v.push(40);
   v.push(500);

   for i in &v {	// 借用
      println!("{}",i);
   }
   println!("{:?}",v);	// 正常运行
}

读取有两种方式,使用 &[] 返回一个引用;或者使用 get 方法以索引作为参数来返回一个 Option<&T>

let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];

match v.get(2) {
    Some(third) => println!("The third element is {}", third),
    None => println!("There is no third element."),
}

如果读取的元素不存在

let v = vec![1, 2, 3, 4, 5];

let does_not_exist = &v[100];	// 直接 panic
let does_not_exist = v.get(100);	// 返回 None

一个经常犯的错误

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];	// 不可变引用
v.push(6);	// 可变引用, 报错!不能在相同作用域中同时存在可变和不可变引用,在 vector 的结尾增加新元素时,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

HashMap

HashMap 结构体在 Rust 语言标准库中的 std::collections 模块中定义,使用前需要显式导入。首先 use 标准库中集合部分的 HashMapHashMap 是最不常用的,所以并没有被 prelude 自动引用。标准库中对 HashMap 的支持也相对较少,例如,并没有内建的构建宏。

创建
// 仅仅是创建空 map,但不能马上使用,因为还没指定数据类型。当给map添加了元素之后才能正常使用。
let mut map = HashMap::new();

// 一旦键值对被插入后就为哈希 map 所拥有
map.insert("a",1);

创建时指定类型

let mut map: HashMap<String,i32> = vec![20,30];

另一个构建方法是使用一个元组的 vector 的 collect 方法,其中每个元组包含一个键值对

use std::collections::HashMap

let teams = vec![String::from("Blue"), String::from("Yellow")];
let init_scores = vec![10,50];

// 这里 HashMap<_, _> 类型注解是必要的,因为可能 collect 很多不同的数据结构
let scores: HashMap<_, _>  = teams.iter().zip(init_scores.iter()).collect();

迭代器的一些方法在迭代器章节会详细分析。

操作

大多数例子见官网文档即可

方法 方法签名 说明
insert() pub fn insert(&mut self, k: K, v: V) -> Option 插入/更新一个键值对到哈希表中,如果数据已经存在则返回旧值,如果不存在则返回 None
len() pub fn len(&self) -> usize 返回哈希表中键值对的个数
get() pub fn get(&lself, k: &Q) -> Option<&V> 根据键从哈希表中获取相应的值,返回 Option,结果被封装
get_mut() pub fn get_mut(&mut self, k: &Q) -> Option<&mut V> 返回对与键对应的值的可变引用。
iter() pub fn iter(&self) -> Iter 返回哈希表键值对的无序迭代器,迭代器元素类型为 (&'a K, &'a V)
contains_key() pub fn contains_key(&self, k: &Q) -> bool 如果哈希表中存在指定的键则返回 true 否则返回 false
remove() pub fn remove_entry(&mut self, k: &Q) -> Option<(K, V)> 从哈希表中删除并返回指定的键值对,如果不存在则返回 None
remove_entry() pub fn remove_entry(&mut self, k: &Q) -> Option<(K, V)> 从映射中删除一个键,如果该键以前在映射中,则返回存储的键和值。

键可以是映射键类型的任何借用形式,但借用形式上的 Hash 和 Eq 必须与键类型匹配
values_mut() pub fn values_mut(&mut self) -> ValuesMut<'_, K, V> 一个迭代器以任意顺序可变地访问所有值。迭代器元素类型是 &'a mut V。
values() pub fn values(&self) -> Values<'_, K, V> 以任意顺序访问所有值的迭代器。迭代器元素类型是 &'a V。
keys() pub fn keys(&self) -> Keys<'_, K, V> 以任意顺序访问所有键的迭代器。迭代器元素类型是 &'a K。
try_reserve() pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError> 尝试为要在 HashMap 中插入的至少更多元素保留容量。集合可能会保留更多空间来推测性地避免频繁的重新分配。调用reserve后,如果返回Ok(()),容量会大于等于self.len()。如果容量已经足够,则什么也不做。
try_insert() pub fn try_insert( &mut self, key: K, value: V ) -> Result<&mut V, OccupiedError<'_, K, V>> 尝试将键值对插入映射中,并返回对条目中值的可变引用。如果映射已经存在此键,则不会更新任何内容,并返回包含占用条目和值的错误。
retain() pub fn retain(&mut self, f: F) 仅保留指定的元素。换句话说,删除所有 f(&k, &mut v) 返回 false 的对 (k, v)。元素以未排序(和未指定)的顺序访问。
let mut map: HashMap = (0…8).map(|x| (x, x*10)).collect(); map.retain(|&k, _| k % 2 == 0);
into_keys() pub fn into_keys(self) -> IntoKeys 把 keys 转为 vec
into_values() pub fn into_values(self) -> IntoValues 把 values 转为 vec

get 操作

use std::collections::HashMap;
fn main() {
   let mut map = HashMap::new();
   map.insert("name","aaa");
   println!("{:?}",stateCodes);

   match stateCodes.get(&"name") {
      Some(value)=> {
         println!("Value for key name is {}",value);
      }
      None => {
         println!("nothing found");
      }
   }
}

迭代操作,使用 iter(),迭代器元素的类型为 (&'a K, &'a V)

use std::collections::HashMap;
fn main() {
   let mut map = HashMap::new();
    map.insert("name","aaa");

   for (key, val) in map.iter() {
      println!("key: {} val: {}", key, val);
   }
}

contains_key

use std::collections::HashMap;
fn main() {
    let mut map = HashMap::new();
    map.insert("name","aaa");

    if map.contains_key(&"name") {
        println!("found key");
    }
}

insert 与 or_insert

insert() 直接插入,所有权也同步转移
or_insert() 只在键没有对应值时插入
entry() 获取我们想要检查的键作为参数,返回一个枚举 Entry,它代表了可能存在也可能不存在的值,如果有值,什么也不做,如果没值,插入

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);	// 插入
scores.entry(String::from("Blue")).or_insert(50);	// 什么也不做

println!("{:?}", scores);	// {"Blue": 10, "Yellow": 50}

根据旧值更新一个值

use std::collections::HashMap;


let text = "hello world wonderful world";

let mut map = HashMap::new();
// 计数一些文本中每一个单词分别出现了多少次
for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

println!("{:?}", map);

其余操作见官方文档即可,非常全

HashSet

同 HashMap 类似,需要显式导入使用

创建
// 创建一个空的集合。但这个空的集合是不能立即使用的,因为它还没指定数据类型。当我们给集合添加了元素之后才能正常使用。
let mut set = HashSet::new();
操作

同 HashMap 几乎一样

方法 方法原型 描述
insert() pub fn insert(&mut self, value: T) -> bool 插入一个值到集合中 如果集合已经存在值则插入失败
len() pub fn len(&self) -> usize 返回集合中的元素个数
get() pub fn get(&self, value: &Q) -> Option<&T> 根据指定的值获取集合中相应值的一个引用
iter() pub fn iter(&self) -> Iter 返回集合中所有元素组成的无序迭代器 迭代器元素的类型为 &'a T
contains_key pub fn contains(&self, value: &Q) -> bool 判断集合是否包含指定的值
remove() pub fn remove(&mut self, value: &Q) -> bool 从结合中删除指定的值

String

Rust 的核心语言中只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现,&str。第四章讲到了 字符串 slice:它们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。

String 的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型

通常,当 Rustacean 们谈到 Rust 的 “字符串”时,它们通常指的是 String 和字符串 slice &str 类型,而不仅仅是其中之一。String 和字符串 slice 都是 UTF-8 编码的。

Rust 标准库中还包含一系列其他字符串类型,比如 OsStringOsStrCStringCStr。这些由 String 或是 Str 结尾的名字,对应着它们提供的所有权和可借用的字符串变体,就如之前的 String 与 str

新建 String
// 1. new 一个空 String 
let mut s = String::new();

// 2. 如果希望一开始就有这个字符串,可以使用 to_string,它能用于任何实现了 Display trait 的类型,字符串字面值也实现了它
let data = "initial contents".to_string();	// 使用字符串字面值创建 String

// 3. 使用 String::from 等同于使用 to_string
let s = String::from("initial contents");
更新 String

几种方式

  1. 使用+ 运算符或 format! 宏来拼接 String 值,需要注意使用方式!+ 运算符内部使用了 add 函数,类似fn add(self, s: &str) -> String
  2. 使用 push_strpush 附加字符串
// 方式 1
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // 注意 s1 被移动了,不能继续使用;s2 传递 &String 并被强转为 &str,后续仍然可用

s2 使用了 &,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add 函数的 s 参数:只能将 &strString 相加,不能将两个 String 值相加。但 &s2 的类型是 &String 而不是 &str。那么为什么还能编译呢?

之所以能够在 add 调用中使用 &s2 是因为 &String 可以被 强转coerced)成 &str。当add函数被调用时,Rust 使用了一个被称为 解引用强制多态deref coercion)的技术,你可以将其理解为它把 &s2 变成了 &s2[..]。后面会更深入的讨论解引用强制多态。因为 add 没有获取参数的所有权,所以 s2 在这个操作后仍然是有效的 String

其次,可以发现签名中 add 获取了 self 的所有权,因为 self 没有 使用 &。这意味着示例中的 s1 的所有权将被移动到 add 调用中,之后就不再有效。所以虽然 let s3 = s1 + &s2; 看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取 s1 的所有权,附加上从 s2 中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝,不过实际上并没有:这个实现比拷贝要更高效。

连接多个字符串

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;

可以转为使用 format!,它返回一个带有结果内容的 String

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);

另一种方式是通过 push_str 方法来附加字符串 slice,从而使 String 变长

// 方式 2
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);	// s 将会包含 foobar。push_str 方法采用字符串 slice,因为我们并不需要获取参数的所有权,s2 仍然可用
println!("s2 is {}", s2);//成功

push 方法是将单独的字符附加到 String

let mut s = String::from("lo");
s.push('l');
索引 String

Rust 的字符串不支持索引。String 是一个 Vec 的封装,而一个字符串的字节值索引并不总对应一个有效的 Unicode,比如

let len = String::from("Здравствуйте").len();

这个len长度不为12,而是24,是因为每个 Unicode 需要两个字节存储。

因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice,因此 rust 不支持索引,杜绝这种情况的发生,此外索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,

字符串 slice

可以使用 [] 和一个 range 来创建含特定字节的字符串 slice:

let hello = "Здравствуйте";
let s = &hello[0..4];	// s 为 &str,包含字符串的头四个字节
let s2 = &hello[0..1];	// Rust 在运行时会 panic
遍历字符串

如果需要单独操作 Unicode 标量值,可以使用 chars() 方法,返回每个 char

for c in "नमस्ते".chars() {
    println!("{}", c);
}
// 输出
न
म
स
्
त
े

如果想获取每个原始字节,可以使用 bytes() 方法

for b in "नमस्ते".bytes() {
    println!("{}", b);
}

错误处理

Rust 把错误分为两大类:可恢复不可恢复,相当于其它语言的 异常(Exception)错误(Error)

Name 描述 范例
Recoverable 可恢复、可以被捕捉,相当于其它语言的异常 Exception Result 枚举
UnRecoverable 不可恢复、不可捕捉,会导致程序崩溃退出,如Error,数组越界 panic 宏

panic!()

panic!() 宏和不可恢复错误,panic!() 会导致程序立即退出,并在退出时向它的调用者反馈退出原因。

panic!( string_error_msg )

可以通过手动调用 panic!() 以达到让程序退出的目的,不要乱用,除非遇到不可挽救的错误。

栈展开与终止

出现 panic 时,程序默认会开始 展开,Rust 会回溯栈并清理它遇到的每一个函数的数据,另一种选择是 终止,不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml[profile] 部分增加 panic = 'abort',可以由展开切换为终止。例如,如果你想要在release模式中 panic 时直接终止:

[profile.release]
panic = 'abort'
backtrace

panic! 可能会出现在我们的代码所调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的 panic! 宏调用,而不是我们代码中最终导致 panic! 的那一行。我们可以使用 panic! 被调用的函数的 backtrace 来寻找代码中出问题的地方。

例如

fn main() {
    let v = vec![1, 2, 3];

    v[99];	// panic
}

这种情况下其他像 C 这样语言会尝试直接提供所要求的值,即便这可能不是你期望的:你会得到任何对应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为 缓冲区溢出buffer overread),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。而 rust 会直接停止

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', libcore/slice/mod.rs:2448:10
note: Run with `RUST_BACKTRACE=1` for a backtrace.

note: Run with RUST_BACKTRACE=1 for a backtrace.。我们可以设置 RUST_BACKTRACE 环境变量来得到一个 backtrace。backtrace 是一个执行到目前位置所有被调用的函数的列表。阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。

Result

使用方式

一些比较古老的语言,比如 C 通过设置全局变量 errno 来告诉程序发生了什么错误,而其它的语言,比如 JAVA 在返回类型的基础上还要通过指定可捕捉的异常来达到程序可恢复的目的,而比较现代的语言,比如 Go 则是通过将错误和正常值一起返回来达到可恢复的目的。

Rust 在可恢复错误( Recoverable )上更大胆。它使用Result 枚举来封装正常返回的值和错误信息。 带来的好处就是只要一个变量就能接收正常值和错误信息,又不会污染全局空间。

Result 枚举被设计用于处理可恢复错误,定义如下

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

包含 OK 和 Err 两个值,其中TE 则是两个范型参数:

  • T 用于当 Result 的值为 OK 时作为正常返回的值的数据类型。
  • E 用于当 Result 的值为 Err 时作为错误返回的错误的类型。

例如

use std::fs::File;
fn main() {
   let f = File::open("main.jpg"); //文件不存在,因此值为 Result.Err
   println!("{:?}",f);
}

Err(Error { repr: Os { code: 2, message: "No such file or directory" } })

例如

use std::fs::File;
fn main() {
   let f = File::open("main.jpg");   // main.jpg 文件不存在
   match f {
      Ok(f)=> {
         println!("file found {:?}",f);
      },
      Err(e)=> {
         println!("file not found \n{:?}",e);   // 处理错误
      }
   }
   println!("end of main");
}

例如

fn main(){
   let result = is_even(13);
   match result {
      Ok(d)=>{
         println!("no is even {}",d);
      },
      Err(msg)=>{
         println!("Error msg is {}",msg);
      }
   }
   println!("end of main");
}
fn is_even(no:i32)->Result<bool,String> {
   if no%2==0 {
      return Ok(true);
   } else {
      return Err("NOT_AN_EVEN".to_string());
   }
}

匹配不同错误

io::ErrorKind 是一个标准库提供的枚举,它的成员对应 io 操作可能导致的不同错误类型

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

unwrap()与expect()

上面的 Result ,用 match 语句处理起来蛮不错的样子,但写多了就会有 Go 语言那种漫天飞舞 if err != nil 的赶脚。

有的时候我们不想处理或者让程序自己处理 Err, 有时候我们只要 OK 的具体值就可以了。

因此标准库中有两个辅助函数数 unwrap()expect()

方法 原型 说明
unwrap unwrap(self):T 如果 selfOkSome 则返回包含的值。 否则调用宏 panic!() 并立即退出程序
expect expect(self,msg:&str):T 如果 selfOkSome 则返回包含的值。 否则调用宏panic!() 输出自定义的错误并退出

expect() 函数用于简化不希望事情失败的错误情况。而 unwrap() 函数则在返回 OK 成功的情况下,提取返回的实际结果。

unwrap()expect() 不仅能够处理 Result 枚举,还可以用于处理 Option 枚举。

示例 unwrap()

fn main(){
   let result = is_even(10).unwrap();
   println!("result is {}",result);
   println!("end of main");
}
fn is_even(no:i32)->Result<bool,String> {
   if no%2==0 {
      return Ok(true);
   } else {
      return Err("NOT_AN_EVEN".to_string());
   }
}

示例 expect()

use std::fs::File;
fn main(){
   let f = File::open("pqr.txt").expect("File not able to open"); // 文件不存在
   println!("end of main");
}

传播错误

当编写一个其实现会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播propagating)错误,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。

例如

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,	// 如果 File::open 成功了,我们将文件句柄储存在变量 f 中并继续
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),	// 如果 read_to_string 成功了,那么这个函数就成功了,被封装进 Ok 的 s 中返回
        Err(e) => Err(e),
    }
}

使用 match 将错误返回给代码调用者。函数的返回值:Result。这意味着函数返回一个 Result 类型的值,其中泛型参数 T 的具体类型是 String,而 E 的具体类型是 io::Error,成功返回,调用者收到 String,错误返回,调用者收到 Err。这里选择 io::Error 作为函数的返回值是因为它正好是函数体中那两个可能会失败的操作的错误返回值:File::open 函数和 read_to_string 方法。

简化传播错误

使用 ? 简化传播错误,使用 ? 运算符向调用者返回错误的函数

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("name.txt")?;	// 使用 ? 运算符向调用者返回错误的函数
    let mut s = String::new();
    f.read_to_string(&mut s)?;	// 使用 ? 运算符向调用者返回错误的函数
    Ok(s)
}

match 表达式与问号运算符所做的有一点不同:? 运算符所使用的错误值被传递给了 from 函数,它定义于标准库的 From trait 中,用来将错误从一种类型转换为另一种类型。当 ? 运算符调用 from 函数时,收到的错误类型被转换为定义为当前函数返回的错误类型。这在当一个函数返回一个错误类型来代表所有可能失败的方式时很有用,即使其可能会因很多种原因失败。只要每一个错误类型都实现了 from 函数来定义如将其转换为返回的错误类型,? 运算符会自动处理这些转换。

简而言之:?能调用from把错误类型转换成我们定义的错误类型。针对不同的错误原因,返回同一种错误类型(只要每个错误类型实现了转换为所返回的cuowuleixing的 from 函数)

可以在 ? 之后使用链式调进一步优化代码

use std::io;
use std::io::Read;
use std::fs::File;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("name.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

更简短的写法,将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数,它会打开文件、新建一个 String、读取文件的内容,并将内容放入 String,接着返回它。

use std::io;
use std::fs;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("name.txt")
}

? 也可被用于返回值类型为 Result 函数,工作方式与 match 一样,matchreturn Err(e) 部分要求返回值类型是 Result,所以函数的返回值必须是 Result 才能与这个 return 相兼容。

泛型、Trait

泛型基础

Rust 使用 语法来实现泛型的东西指定数据类型。 其中 T 可以是任意数据类型。

泛型集合

let mut vec: Vec<i32> = vec![20,30];

泛型结构体(结构体成员可以是泛型)

struct struct_name<T> {
   field:T,
}

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

例如

fn main() {
   // 泛型为 i32
   let t:Data<i32> = Data{value:350};
   println!("value is :{} ",t.value);
   // 泛型为 String
   let t2:Data<String> = Data{value:"Tom".to_string()};
   println!("value is :{} ",t2.value);
}

struct Data<T> {
   value:T,
}

方法中定义泛型

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

// 实现方法 x,返回 T 类型的字段 x 的引用
// 必须在 impl 后面声明泛型 T,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型
// 这样就可以在结构体 Point 上实现的方法中使用它
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

以下就是一个没有在 impl 之后(的尖括号)声明泛型的例子,这里使用了一个具体类型 f32

// 具体类型的方法
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

泛型性能

Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。

Rust 通过在编译时进行泛型代码的 单态化monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

Traits 特质

概念

rust 没有类的概念,顺带取消了接口,Rust 提供了 特质 Traits 这个概念,相当于接口,定义共享行为。还可以使用 trait bounds指定泛型是任何拥有特定行为的类型。

定义特质

trait some_trait {

   // 没有任何实现的虚方法,以分号结尾
   fn method1(&self);

   // 有具体实现的普通方法,提供默认行为
   fn method2(&self){
      //方法的具体代码
   }
}

traits 可以包含具体方法(带实现的方法)或抽象方法(没有具体实现的方法)

  • 如果想让 traits 中的某个方法被实现了特质的结构体所共享,那么推荐使用具体方法。
  • 如果想让由实现了特质的结构体自己定义方法,那么traits中推荐使用抽象方法。

结构体也可以对特质的具体方法进行重写

为 struct 实现 trait

使用 impl for

fn main(){
   //创建结构体 Book 的实例
   let b1 = Book {
      id:1001,
      name:"Rust in Action"
   };
   b1.print();
}

//声明结构体
struct Book {
   name:&'static str,
   id:u32
}

// 声明特质,如果希望该 trait 被公开使用,需要加上 pub 关键字
pub trait Printable {
   fn print(&self);
}

// 实现特质,impl trait_name for struct_name ,为 Book 实现特质 Printable。
impl Printable for Book {
   fn print(&self){
      println!("Printing book with id:{} and name {}",self.id,self.name)
   }
}

实现 trait 时需要注意的一个限制是,只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。例如,可以为 aggregator crate 的自定义类型 Tweet 实现如标准库中的 Display trait,这是因为 Tweet 类型位于 aggregator crate 本地的作用域中。类似地,也可以在 aggregator crate 中为 Vec 实现 Summary,这是因为 Summary trait 位于 aggregator crate 本地作用域中。

但是不能为外部类型实现外部 trait。例如,不能在 aggregator crate 中为 Vec 实现 Display trait。这是因为 DisplayVec 都定义于标准库中,它们并不位于 aggregator crate 本地作用域中。这个限制是被称为 相干性coherence) 的程序属性的一部分,或者更具体的说是 孤儿规则orphan rule),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

这块没咋看懂

trait 作为参数

如何使用 trait 来接受多种不同类型的参数?直接上代码

// 摘要 trait
pub trait Summary {
    fn summarize(&self) -> String;
}

// trait 实现
pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

// trait 作为参数
pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

对于 item 参数,我们指定了 impl 关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型,如 NewsArticle、NewsArticle

trait bound

impl Trait 语法适用于直观的例子,它不过是一个较长形式的语法糖。这被称为 trait bound,如

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

如果场景复杂,比如需要使用多个 trait 参数,就可以简写

pub fn notify(item1: impl Summary, item2: impl Summary) {
  
// 简写,泛型 T 被指定为 item1 和 item2 的参数限制
pub fn notify<T: Summary>(item1: T, item2: T) {

使用 + 指定多个 trait bound

如果 item 需要同时实现两个不同 trait,就可以这么写

pub fn notify(item: impl Summary + Display) {

// 等同于
pub fn notify<T: Summary + Display>(item: T) {

使用 where 简写 trait bound

使用过多的 trait bound 也有缺点。每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读,为此,Rust 有另一个在函数签名之后的 where 从句中指定 trait bound 的语法。比如

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {

// 等价于
fn some_function<T, U>(t: T, u: U) -> i32
  where T: Display + Clone,
  			U: Clone + Debug
{
返回值为trait

可以在返回值中使用 impl Trait 语法,来返回实现了某个 trait 的类型

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        retweet: false,
    }
}

但是注意,只能返回单一的具体类型,返回可能的不同类型会报错,比如下面的 if-else,就会报错,因为有两种类型去选择

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from("Penguins win the Stanley Cup Championship!"),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from("The Pittsburgh Penguins once again are the best
            hockey team in the NHL."),
        }
    } else {	// 报错
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from("of course, as you probably already know, people"),
            reply: false,
            retweet: false,
        }
    }
}
修复 largest 函数

对于下面这样的一个例子,是会报错的,

第一个问题:大于号> 运算符实际对应的是std::cmp::PartialOrd 默认的方法,只有 T 实现了这个 trait,才可以使用大于号来进行比较。

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {	// 报错
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

第二个问题:无法从 list 中移出(move)函数,因为没有实现 Copy trait,或者考虑借用 &list[0]

我们的集合,要么是整数,要么是字符, 这两种数据类型他们都存储在 stack 上,实现了 Copy trait,但是在泛型函数中,T却没有加上 Copy trait的约束,那么加上约束即可。

error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       help: consider using a reference instead: `&list[0]`

error[E0507]: cannot move out of borrowed content
 --> src/main.rs:4:9
  |
4 |     for &item in list.iter() {
  |         ^----
  |         ||
  |         |hint: to prevent move, use `ref item` or `ref mut item`
  |         cannot move out of borrowed content

修改为下面的即可

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {	// 报错
            largest = item;
        }
    }

    largest
}

但如果我们的集合存储 String 类型,存储在堆上,没有实现 Copy trait,但是它实现了 Clone trait,只要将 Copy 改为 Clone 即可,同时有些地方需要做出相应改变

fn largest<T:PartialOrd+Clone>(list:&[T])->&T{
    let mut largest = list[0];
    for item in list.iter(){
        if item > &largest{
            largest = item;
        }
    }
    largest
}
有条件地实现方法

可以有条件为实现了特定 trait 的类型来实现方法,例如

use std::fmt::Display;

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

impl<T> Pair<T> {
  	// 实现 new 函数
    fn new(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

// T 加了约束,只有实现了 Display 和 PartialOrd 才拥有 cmp_display 方法
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

此外,可以为实现了其他 trait 的任意类型有条件地实现某个 trait,叫做覆盖实现。例如 string.rs

// 这句的意思是 对所有满足 Display trait 约束的类型,都实现了 ToString 这个 trait,这就是覆盖实现
// 即可以为任何实现了 Display trait 的类型调用 ToString trait 中的方法
impl<T: fmt::Display +?Sized> ToString for T{
  
}

比如
let s = 3.to_string();因为所有的整数类型都实现了 Display trait,因此 也能使用 ToString 中的 to_string()方法

泛型函数

主要是指函数的参数是泛型,但并不是要求所有的参数都是泛型。语法定义如下:

fn function_name<T[:trait_name]>(param1:T, [other_params]) {

   // 函数实现代码
  
}


// 例如定义一个可以打印输出任意类型的泛型函数 `print_pro`
use std::fmt::Display;

fn main(){
   print_pro(10 as u8);
   print_pro(20 as u16);
   print_pro("Hello TutorialsPoint");
}

fn print_pro<T:Display>(t:T){
   println!("Inside print_pro generic function:");
   println!("{}",t);
}

// 类型参数声明位于函数名称与参数列表中见间的 <> 中,函数 largest 有泛型类型 T。它有一个参数 list,它的类型是一个 T 值的 slice。largest 函数将会返回一个与 T 相同类型的值。
fn largest<T>(list: &[T]) -> T {}

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {	// 报错 std::cmp::PartialOrd might be missing for `T`,因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

生命周期

基本概念

生命周期:即引用保持有效的作用域

  • 每个引用有自己的生命周期
  • 大多数情况,生命周期是隐式、可被推断的
  • 引用的生命周期以不同方式相互关联时,可手动标注

生命周期的主要目标是避免悬垂引用

例如

{
    let r;{
        let x = 5;
        r = &x;	// x 在括号外就失效了,离开了作用域,r的作用域更大
    }
    println!("r: {}", r);
}

rust 使用 借用检查器 来比较作用域所有借用是否合法

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

被引用对象x的生命周期比 r 的生命周期要短,要解决这个问题很简单,只要让 b 不小于 a即可

fn main(){
  		let x = 5;
  		let r = &x;
}

泛型生命周期

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

// 报错,在返回类型这块缺少命名的生命周期参数
// 返回类型包含了一个借用的值,但没有说明这个借用的值来自 x 还是 y
// 考虑引入生命周期参数
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

需要添加泛型生命周期参数

// 'a 表示有 a 这样一个生命周期
// 按照如下写法就表明,返回值的生命周期与参数的生命周期是 “一样的”
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

生命周期标注语法

  • 当指定泛型生命周期参数,函数可以接受带有任何生命周期的引用
  • 生命周期标注 ,描述了多个引用的生命周期之间的关系,不会影响生命周期

生命周期参数名

  • '开头
  • 通常全小写且非常短 比如'a

生命周期标注位置

  • 引用符号 &
  • 用空格将 &'a 和参数类型分隔开,如&'a mut i32

函数签名中的生命周期参数

  • 泛型生命周期参数声明在<>
// 这个签名告诉 rust,这两个参数的存活时间必须大于等于 'a,返回的也要大于等于 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

这个泛型的生命周期 'a 是哪一块呢?是传入的参数 x 和y 生命周期较小的那个(重叠的部分),比如下面的就会报错

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

深入理解生命周期

  1. 如果返回值的生命周期只和x有关,y就不需要添加生命周期,如下写法

    fn longest<'a>(x: &'a str, y: &str) -> &'a str {
        x
    }
    
  2. 当函数返回引用时,返回类型生命周期参数需要与其中一个参数的生命周期匹配,如果返回的引用没有指向任何参数,那么只能引用函数内创建的值,这就是悬垂引用,因为这个值结束时就走出了作用域,比如,下面这个就报错

    fn longest<'a>(x: &str, y: &str) -> &'a str {
        let result = String::from("really long string");
        result.as_str()	
    }// result 被清理
    

    如果我非要用这个 result 怎么办?不要返回引用,直接返回值即可,把所有权移交给函数调用者

    fn longest<'a>(x: &str, y: &str) -> String {
        let result = String::from("really long string");
        result
    }
    
  3. struct 定义时的生命周期标注

    struct包括 自持有类型、引用,如果是引用,每个引用都需要添加生命周期标注,例如

    struct ImportantExcerpt<'a> {
        part: &'a str,	// 要求这个引用比实例存活时间长 
    }
    
    fn main() {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let p = novel.split('.')
            .next()
            .expect("Could not find a '.'");
        let i = ImportantExcerpt { part: p };	// 成功编译,因为 p的生命周期在7-10行,而i这个实例生命周期在第10行,前者生命周期更长
    }
    
  4. 生命周期省略

    lifetime elision。每个引用都有生命周期,且需要为使用引用的函数或结构体指定生命周期,但如下例子却(参数与返回值都是引用)能不指定生命周期而编译成功

    fn first_word(s: &str) -> &str {
        let bytes = s.as_bytes();
    
        for (i, &item) in bytes.iter().enumerate() {
            if item == b' ' {
                return &s[0..i];
            }
        }
    
        &s[..]
    }
    

    Rust团队发现,特定情况下,程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。后来把这些模式编码进了编译器,就不需要再显示添加注解了,这些模式被称为生命周期省略规则

    • 开发者无需遵守,编译器自动推断
    • 如果代码符合情况,就无需显式添加生命周期

    函数与方法的参数的生命周期称之为 输入生命周期,返回值的生命周期称之为 输出生命周期

    省略规则

    • 规则1:每个引用类型的参数都有自己的生命周期,‘a‘b
    • 规则2:如果只有1个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
    • 规则3:如果有多个输入生命周期参数,但其中一个是 &self(方法中)&mut self(方法中),那么 self 的生命周期会被赋给所有的输出生命周期参数。

    注意

    • 规则1,应用于输入生命周期
    • 规则2、3,应用于输出生命周期
    • 不符合3个规则,报错
    • 这些规则适用于 fn(函数或方法)定义和 impl 块

    示例1

    // 尚未关联任何生命周期
    fn first_word(s: &str) -> &str {
      
    // 规则1,每个引用参数都有其自己的生命周期,因此给引用参数加上 'a
    fn first_word<'a>(s: &'a str) -> &str {
      
    // 规则2,恰好只有一个参数,输入生命周期赋给输出生命周期
    fn first_word<'a>(s: &'a str) -> &'a str {
      
    // 因此编译器可以分析代码而无须程序员手动标注
    

    示例2

    // 尚未关联任何生命周期
    fn longest(x: &str, y: &str) -> &str {
      
    // 规则1,每个参数有自己的生命周期 'a、'b
    fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
    
    // 规则2不适用,规则3也不适用(它是函数,不是方法)
    
  5. 方法定义时的生命周期注解

    第三条规则只适用于方法,在结构体上使用生命周期实现方法,语法和泛型参数语法一样。

    struct 字段的生命周期名:

    • impl 后声明
    • struct 名后使用
    • 这些生命周期时 struct 的一部分

    impl 块内的方法签名中

    • 引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以。、
    • 生命周期省略规则经常使得方法中国的生命周期标注不是必须的
    struct ImportantExcerpt<'a>{
      	part: &'a str,
    }
    
    // 第一个 'a 是结构体生命周期,第二个'a 是应用于该结构体名后面
    impl<'a> ImportantExcerpt<'a> {
      	// 根据规则 1,唯一的参数是 self 的引用,而且返回值只是一个 i32 并不引用任何值
        fn level(&self) -> i32 {
            3
        }
      
      	// 根据规则1,会为这两个参数添加生命周期,根据规则3,返回值被赋予 self 的生命周期,因此所有的生命周期被计算出来了,可以编译通过
      	fn announce_and_return_part(&self, announcement: &str) -> &str {
            println!("Attention please: {}", announcement);
            self.part
        }
    }
    
  6. 静态生命周期

    'static 是一个特殊的生命周期,为整个程序的持续时间。比如所有的字符串字面值都有'static

    let s: &'static str = "I have a static lifetime.";
    

    试用前请仔细考虑

  7. 结合泛型类型参数、trait bounds、生命周期

    方法多了个泛型 T,where 语句指定了 T 可以是任何实现了 Display trait 的类型,因为生命周期也是泛型的一种,所以'aT都放到了<>

    use std::fmt::Display;
    
    
    fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
        where T: Display
    {
        println!("Announcement! {}", ann);
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    

IO

标准读写

建议查看官方文档

  • stdin:https://doc.rust-lang.org/std/io/struct.Stdin.html
  • stdout:https://doc.rust-lang.org/std/io/struct.Stdout.html
  • read:https://doc.rust-lang.org/std/io/trait.Read.html
  • write:https://doc.rust-lang.org/std/io/trait.Write.html

命令行参数

Rust 语言在标准库中内置了 std::env::args() 函数返回所有的命令行参数,std::env::args() 返回的结果包含了程序名。例如

./main.exe 2022 "test"

对应

["./main.exe","2022","test"]

再例如,输出所有命令行参数

fn main(){
   let cmd_line = std::env::args();
   println!("总共有 {} 个命令行参数",cmd_line.len()); // 传递的参数个数

   let mut sum = 0;
   let mut has_read_first_arg = false;

   //迭代所有参数并计算它们的总和

   for arg in cmd_line {
      if has_read_first_arg { // 跳过第一个参数,因为它的值是程序名
         sum += arg.parse::<i32>().unwrap();
      }
      has_read_first_arg = true; // 设置跳过第一个参数,这样接下来的参数都可以用于计算
   }
   println!("和值为:{}",sum);
}

文件读写

使用结构体 File 来描述一个文件,所有对 File 的操作都会返回一个 Result 枚举。

常用文件读写方法

模块 方法 方法签名 说明
std::fs::File open() pub fn open(path: P) -> Result 静态方法,以 只读 模式打开文件
std::fs::File create() pub fn create(path: P) -> Result 静态方法,以 可写 模式打开文件。 如果文件存在则清空旧内容 如果文件不存在则新建
std::fs::remove_file remove_file() pub fn remove_file(path: P) -> Result<()> 从文件系统中删除某个文件
std::fs::OpenOptions append() pub fn append(&mut self, append: bool) -> &mut OpenOptions 设置文件模式为 追加
std::io::Writes write_all() fn write_all(&mut self, buf: &[u8]) -> Result<()> 将 buf 中的所有内容写入输出流
std::io::Read read_to_string() fn read_to_string(&mut self, buf: &mut String) -> Result 读取所有内容转换为字符串后追加到 buf 中

打开并读取文件(只读)

  1. 使用 open() 函数打开一个文件
  2. 然后使用 read_to_string() 函数从文件中读取所有内容并转换为字符串。
use std::io::Read;

fn main() {
  	// 以只读方式打开
    let file = std::fs::File::open("data.txt").unwrap();
    println!("文件打开成功:{:?}",file);
  
  	let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    print!("{}", contents);
}

// 输出
文件打开成功:File { fd: 3, path: "/Users/alex/Downloads/guess-game-app/src/data.txt", read: true, write: false }

之前在传播错误一节中写过的更简短的写法

use std::io;
use std::fs;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("name.txt")
}

创建文件(可写)

pub fn create(path: P) -> Result
fn main() {
   let file = std::fs::File::create("data.txt").expect("create failed");
   println!("文件创建成功:{:?}",file);
}

写入文件

标准库 std::io::Writes 提供了函数 write_all() 用于向输出流写入内容。

fn write_all(&mut self, buf: &[u8]) -> Result<()>

向当前流写入 buf 中的内容。如果写入成功则返回写入的字节数,如果写入失败则抛出错误

use std::io::Write;
fn main() {
   let mut file = std::fs::File::create("data.txt").expect("create failed");
   file.write_all("aaa".as_bytes()).expect("write failed");
   file.write_all("\nbbb".as_bytes()).expect("write failed");
   println!("data written to file" );
}

write_all() 方法并不会在写入结束后自动写入换行符 \n

追加内容到文件末尾

函数 append() 用于将文件的打开模式设置为 追加。在模块 std::fs::OpenOptions 中定义,它的函数原型为

pub fn append(&mut self, append: bool) -> &mut OpenOptions

例如

use std::fs::OpenOptions;
use std::io::Write;

fn main() {
   let mut file = OpenOptions::new().append(true).open("data.txt").expect(
      "cannot open file");
   file.write_all("aaa".as_bytes()).expect("write failed");
   file.write_all("\nbbb".as_bytes()).expect("write failed");
   file.write_all("\nccc".as_bytes()).expect("write failed");
   println!("数据追加成功");
}

删除文件

标准库 std::fs 提供了函数 remove_file() 用于删除文件。

use std::fs;
fn main() {
   fs::remove_file("data.txt").expect("could not remove file");
   println!("file is removed");
}

复制文件

标准库没有提供任何函数用于复制一个文件为另一个新文件。但可以使用上面提到的函数和方法来实现文件的复制功能。

例如实现 copy 命令

copy old_file_name  new_file_name
use std::io::Read;
use std::io::Write;

fn main() {
   let mut command_line: std::env::Args = std::env::args();
   command_line.next().unwrap();

   // 跳过程序名
   // 原文件
   let source = command_line.next().unwrap();

   // 新文件
   let destination = command_line.next().unwrap();
   let mut file_in = std::fs::File::open(source).unwrap();
   let mut file_out = std::fs::File::create(destination).unwrap();
   let mut buffer = [0u8; 4096];
   loop {
      let nbytes = file_in.read(&mut buffer).unwrap();
      file_out.write(&buffer[..nbytes]).unwrap();
      if nbytes < buffer.len() { break; }
   }
}

迭代器

rust 所有集合都实现了迭代器,遍历语法如下

for iterator_item in iterator {
   // 使用迭代项的具体逻辑
}

// 例如
fn main() {
   let a = [10,20,30];
   let iter = a.iter();
   for data in iter{
      print!("{}\t",data);
   }
}

迭代器所带来的问题

  1. 迭代器遍历集合后,集合能否再使用?分为两种
    • 只读遍历,但可以重新遍历,对应 iter()
    • 只读遍历,但不可重新遍历,对应into_iter()
  2. 迭代器遍历时,能否修改集合中的元素?,分为两种
    • 可修改遍历,但不可重新遍历,对应 iter_mut()
    • 可修改遍历,但不可重入遍历(没多大用)

重入概念

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反, 不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥 (或者使用信号量,或者在代码的关键部分禁用中断)。

方法 描述
iter() 返回一个只读可重入迭代器,迭代器元素的类型为 &T,集合可重用
into_iter() 返回一个只读不可重入迭代器,迭代器元素的类型为 T,集合不可重用
iter_mut() 返回一个可修改可重入迭代器,迭代器元素的类型为 &mut T,集合可重用

iter()

遍历结束后集合可重用。返回值为集合元素的引用,因为是引用,所以集合保持不变,并且集合在迭代器遍历之后还可以继续使用

fn main() {
   let names = vec!["a", "b", "c"];
   for name in names.iter() {
      match name {
         &"b" => println!("b!"),
         _ => println!("Hello {}", name),
      }
   }
   println!("{:?}",names); // 迭代之后可以重用集合
}

into_iter()

**遍历结束后集合不可重用。**返回自动拆箱迭代,into_iter() 运用了 所有权 ownership 的概念。把所有迭代的值从集合中移动到一个迭代器对象中。我们的迭代变量就是一个普通对象而不是对集合元素的引用。在 match 匹配时就不需要引用 &

fn main(){
   let names = vec!["a", "b", "c"];
   for name in names.into_iter() {
      match name {
         "b" => println!("b!"),
         _ => println!("Hello {}", name),
      }
   }
   // 迭代器之后集合不可再重复使用,因为元素都被拷贝走了
   //println!("{:?}",names); 
   //Error:Cannot access after ownership move
}

iter_mut()

遍历结束后集合是可以重复使用的。iter() 方法返回的是一个只读迭代,不能通过迭代器来修改集合。iter_mut() 方法返回 &mut T 智能指针。我们可以通过对迭代变量 解引用 的方式来重新赋值。

fn main() {
   let mut names = vec!["a", "b", "c"];
   for name in names.iter_mut() {
      match name {
         &mut "b" => { *name = "d";println!("b=>d!")},
         _ => println!("Hello {}", name),
      }
   }

   // 集合还可以重复使用
   println!("{:?}",names);
}

https://course.rs/advance/functional-programing/iterator.html

以下参考此博客

取元素

next()

一次取出一个值,直至返回None,可多次调用

et mut it = 1..3;
assert_eq!(Some(1), it.next());
assert_eq!(Some(2), it.next());
assert_eq!(None, it.next());
take(k)

取前面k个元素,只可调用一次,因为调用后,迭代器的所有权会被转移到take方法内部

assert_eq!(vec![1,2,3], (1..10).take(3).collect::<Vec<_>>());
nth(k)

取得迭代器剩余元素中第 k 个位置的元素,位置从 0 开始;之后,迭代器跳转到下一个位置。

let mut it = [1, 2, 3].iter();

assert_eq!(Some(&1), it.nth(0));
assert_eq!(Some(&2), it.nth(0));
assert_eq!(Some(&3), it.nth(0));
assert_eq!(None, it.nth(0));
assert_eq!(Some(3), (0..4).nth(3));
last()

只取最后一个元素,只能调用一次。

assert_eq!((1..4).last(), Some(3));

变换

rev()
println!("{:?}", "-".repeat(10));

// 输出:4, 3, 2, 1, 0,
vec![0, 1, 2, 3, 4].iter().rev().for_each(|x|print!("{x},"));

// 输出:9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
for i in (0..10).rev() {
    print!("{:?},", i);
}

println!("\n{:?}", "-".repeat(10));
skip(k)

跳过 k 个元素

assert_eq!(vec![2,3], (0..4).skip(2).collect::<Vec<_>>());
step_by(k)

从第一个元素开始,每k个取一个出来

assert_eq!(vec![0,2,4,6], (0..7).step_by(2).collect::<Vec<_>>());
chain()

合并,对迭代器进行顺序拼接合并

let it = (0..5).chain(15..20);
//[0, 1, 2, 3, 4, 15, 16, 17, 18, 19]
println!("{:?}", it.collect::<Vec<_>>());
zip()

将2个迭代器合并为一对一元组迭代器,比如就可以用两个vec生成 map

let it = [1,3,5].iter().zip([2,4,6].iter());
assert_eq!(vec![(&1,&2),(&3,&4),(&5,&6)], it.collect::<Vec<(_,_)>>());
assert_eq!(vec![(0,'f'),(1,'o'),(2,'o')], (0..).zip("foo".chars()).collect::<Vec<_>>());
let teams = vec![String::from("Blue"), String::from("Yellow")];
let init_scores = vec![10,50];

// 这里 HashMap<_, _> 类型注解是必要的,因为可能 collect 很多不同的数据结构
let scores: HashMap<_, _>  = teams.iter().zip(init_scores.iter()).collect();
map()

类似java stream 的map,对迭代器中每一个元素进行映射,返回一个新的迭代器

assert_eq!(vec![0,1,4,9,16], (0..5).map(|x|x*x).collect::<Vec<_>>());

求值结算

max、min、count、sum
//最大值
assert_eq!([1,2,3].iter().max(), Some(&3));

//最小值
assert_eq!([1,2,3].iter().min(), Some(&1));

// count()计算迭代器中元素的个数
assert_eq!([1,2,3].iter().count(), 3);

// 求和
assert_eq!([1,2,3].iter().sum::<i32>(), 6);
fold

通过传入一个初始值和一个闭包累加器,对迭代器中的每一个元素依次进行处理并“累加”,最后返回“累加”结果。这里用“累加”来指代函数操作,并不仅仅是能做加法。

assert_eq!(3, (1..3).fold(0, |acc, x|acc+x));// 1+2
assert_eq!(6, (1..3).fold(0, |acc, x|acc+2*x));// 2*1 + 2*2

闭包

定义

  • 闭包就是在一个函数内创建立即调用的另一个函数。
  • 闭包是一个匿名函数。
  • 闭包虽然没有函数名,但可以把整个闭包赋值一个变量,通过调用该变量来完成闭包的调用。从某些方面说,这个变量就是函数名的作用。
  • 闭包不用声明返回值,但它却可以有返回值。并且使用最后一条语句的执行结果作为返回值。闭包的返回值可以赋值给变量。
  • 闭包有时候有些地方又称之为 内联函数。这种特性使得闭包可以访问外层函数里的变量。

从上面的描述中可以看出,闭包就是函数内部的一个没有函数名的内联函数。对于那些只使用一次的函数,使用闭包是最佳的代替方案。

定义

// 去掉 fn 关键字,去掉函数名,去掉返回值声明,并把一对小括号改成一对 竖线 ||。
|parameter| {
   // ...
}

// 没有参数的闭包
||{
   // ...
}

// 将闭包赋值给一个变量,然后就可以通过调用这个变量来完成闭包的调用。
let closure_function = |parameter| {
   // ...
}

调用

具有函数的特质,使用小括号 () 来调用闭包

closure_function(parameter); 

例如

fn main(){
   let is_even = |x| {
      x%2==0
   };
   let no = 13;
   println!("{} is even ? {}",no,is_even(no));
}

闭包可以访问他所在的外部函数可以访问的所有变量

fn main(){
   let val = 10; 
   // 访问外层作用域变量 val
   let closure2 = |x| {
      x + val // 内联函数访问外层作用域变量
   };
   println!("{}",closure2(2));
}

参考

  1. Rust 基础教程
  2. Rust程序设计语言
  3. Rust 语言圣经
  4. Rust 学习笔记

你可能感兴趣的:(Rust,rust)