个人文档
  • AI编程Cursor
  • GPT使用笔记
  • npm常用库合集
  • 同步用
  • 小Demo们
  • 工具网站教程集合
  • HTML、CSS 工具方法集合
    • HTML 全局属性
    • css常用功能
    • font-face 字体|子集相关
    • iframe父子页面传值
    • input输入优化
    • loading状态
    • nodejs使用谷歌邮箱发邮件
    • 为 Dom 自定义事件监听
    • 初始html的head标签配置
    • 拼音输入中文汉字的事件监听
    • 文字颜色效果
    • 文档片段范围 Range
    • 移动端开发-rem
    • 等宽字体推荐
    • 网站SEO优化注意点
    • 邮件html模板
  • JS 工具方法集合
    • Axios 简单使用
    • Axios 简单封装
    • Gitbook的安装和使用
    • Github 登录开发
    • HTML转为纯文本
    • JS 中强大的操作符
    • cookie 操作
    • js 动态加载js资源
    • js 常用功能语句
    • js取代trycatch的方法封装
    • js接口下载二进制
    • script 标签的异步属性
    • 判断当前是移动端还是pc端
    • 刷新token队列管理
    • 前端多线程 Web Worker
    • 加密-AES对称加密
    • 加密-node进行rsa加密解密
    • 地区省市区三级联动的地址数据 + 功能
    • 复制插件
    • 开发时环境变量
    • 得到随机图片
    • 数字格式整理集合
    • 数学计算插件
    • 时间格式整理
    • 获取ip地址
    • 获取url传参
    • 进制转换和位运算符
    • 页面隐藏|激活|关闭的监听
  • JS 知识点研究
    • Babel 历史和原理
    • Babel 配置和使用
    • Function 的 apply、call、bind
    • HTTP浏览器缓存粗解
    • Source map 文件还原为源码
    • TS常用技巧
    • js 的加载和模块化
    • js 的新数据类型 Symbol
    • js的代理对象 proxy 和 defineProperty
    • js的原型链 prototype
    • vite 打包体积优化
    • webpack 可视化打包文件大小插件
    • webpack 基础使用配置
    • webpack 版本5的报错
    • yeoman 开发脚手架的工具
    • 同步异步和微任务宏任务
    • 移动端调试---谷歌工具+eruda+vconsole
    • 转换-Blob URL
    • 转换-FileReader
    • 转换-Js文件类型和转换
    • 转换-前端开发的URL的编码和解码
    • 转换-字符串和Base 64的转换
  • Node 和 Npm 相关
    • Node 开发环境配置
    • express + jwt 校验
    • node 常用方法
    • node后台服务器-PM2
    • node基本使用
    • npm 中依赖的版本问题
    • npm 功能使用
    • npm指令说明和其他对比
    • nvm版本管理+自动切换node版本
  • React 学习
    • React Hook
    • React 项目基础开发
    • React.memo 和 React.PureComponent
    • React懒加载进阶
    • useContext Hook
    • useEffect Hook
    • useMemo 和 useCallback - Hook
    • useRef Hook
    • useState Hook
    • 同步修改变量功能封装 useVal for react
    • 轻便的传值组件
  • Rust 语言相关
    • Rust 基本
    • Rust 基础学习
    • Rust 调用 Object-C 的API
    • Tauri 基本使用
    • Tauri 是什么
  • VUE 学习
    • Vue3 使用
    • Vue3使用hook
    • Vue开发小技术点
    • vue路由切换时的动画效果
    • 花式引入组件和资源-打包时拆包减少js体积
  • Web3相关
    • Web3.0开发上-准备和概念理解
    • Web3.0开发下-功能代码示例
    • 以太坊区块链和Web3.0
    • 开发智能合约
  • python
    • pyenv版本管理工具
    • python初始化
    • python基本概念
    • venv虚拟环境
  • 个人其他
    • Steam Deck的基本设置和插件
  • 其他编程相关
    • Git教程和常用命令
    • Java开发-JDK和Maven的安装和卸载
    • Jenkins安装和基本使用
    • Linux系统指令
    • Mac 使用2K屏幕开启缩放
    • Mac 使用VS code打开项目
    • Mac 安装 Homebrew
    • Mac 的终端 shell 与 zsh
    • Mac 软件和插件
    • MacBook使用建议
    • Mac升降级到指定版本的系统
    • Mac安装Zsh
    • Mac安装软件各种提示
    • Mac系统脚本语言 AppleScript 的使用
    • Mac终端代理工具
    • Markdown(md)文档开发-Typora
    • Mysql 的安装和使用
    • Nginx 安装和基础使用
    • Nginx 稍微高深的配置
    • Slate - Api 的文档开发工具
    • Sublime配置
    • Ubuntu的 apt-get 使用
    • VScode配置
    • Windows 软件和插件
    • curl 工具使用
    • github 网站访问优化
    • host 文件
    • inquirer 终端中和用户交互
    • uTools的插件开发教程
    • vim 文本编辑功能
    • 使用 Github Pages 免费部署网站
    • 压缩指令 zip 和 unzip
    • 油猴的安装和开发(Tampermonkey)
    • 阿里云简略使用
  • 微信开发
    • 微信小程序开发
    • 微信开发必读
    • 微信开发提前购买域名
    • 微信手机打开的页面中授权登录
    • 微信扫码登录
    • 微信服务号登录+推送服务提醒
    • 自定义分享卡片-node.js实现
  • 数据结构与算法
    • KMP算法
    • Wildcard字符串分析算法
    • 二叉树
    • 字典树
    • 时间复杂度浅析
    • 算法神器——动态规划
Powered by GitBook
On this page
  • Rust 基础学习
  • 数据类型
  • 函数
  • 控制流
  • 所有权
  • 结构体 struct
  • 枚举
  • 模块系统
  • 常见集合
  • 错误处理
  • 泛型、Trait 和生命周期

Was this helpful?

  1. Rust 语言相关

Rust 基础学习

title: Rust 基础学习 id: 7f0ee34faf7b216624eba060dfe4e62c tags: [] date: 2000/01/01 00:00:00 updated: 2023/03/04 19:29:12 isPublic: true --#|[分隔]|#--

Rust 基础学习

包含 rust 最最基础的基本。

数据类型

每一个值都属于某一个 数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。

Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。

标量类型(scalar)

标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。

整型

整数是一个没有小数部分的数字,根据是否有负数(有符号)、取值范围,共12个种。

长度
有符号
无符号

8-bit

i8

u8

16-bit

i16

u16

32-bit

i32

u32

64-bit

i64

u64

128-bit

i128

u128

arch

isize

usize

有符号的变体可以储存包含从 -2^(n - 1) 到 2^(n - 1) - 1 在内的数字,所以 i8 可以储存从 -2^7 到 2^7 - 1 在内的数字,也就是从 -128 到 127。

无符号的变体可以储存从 0 到 2^n - 1 的数字,所以 u8 可以储存从 0 到 2^8 - 1 的数字,也就是从 0 到 255。

isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的,32 位架构上它们是 32 位的。

浮点型

Rust 也有两个原生的 浮点数(floating-point numbers)类型,它们是带小数点的数字。

Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位,默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。

所有的浮点型都是有符号的。

浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。

布尔型

true 和 false。

字符类型

Rust 的 char 类型是语言中最原生的字母类型。下面是一些声明 char 值的例子:

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

复合类型(compound)。

复合类型(Compound types)可以将多个值组合成一个类型。

Rust 有两个原生的复合类型:**元组(tuple)**和 数组(array)。

元组类型

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。

元组长度固定:一旦声明,其长度不会增大或缩小。

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    // 直接取值
    let first = tup.0
    let second = tup.1
    let third = tup.2

    // 可以使用模式匹配(pattern matching)来解构(destructure)元组值
    let (x, y, z) = tup;
}

不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。

这种值以及对应的类型都写作 (),表示空值或空的返回类型。

如果表达式不返回任何其他值,则会隐式返回单元值。

数组类型

与元组不同,数组中的每个元素的类型必须相同。

Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。

无法解构取值。

fn main() {
    // 可以如此声明类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量
    let a: [i32; 5] = [1, 2, 3, 4, 5];
    // 可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组
    let a = [3; 5];
    // 和上面一行等效
    let a = [3, 3, 3, 3, 3]
  
    // 访问数组数据
    let first = a[0];
    let second = a[1];
}

函数

语句和表达式

语句(Statements)是执行一些操作但不返回值的指令。

表达式(Expressions)计算并产生一个值。

控制流

if 表达式

loop 循环

loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。

fn main() {
    let mut num = 1;
    loop {
      num += 1;
      // 循环中的 continue 关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代
      if num % 2 == 0 {continue};
      // break 关键字来告诉程序何时停止循环
      if num >= 10 {break};
      println!("again! {}", num);
    }
}

从循环返回值

break 时,可能会需要将操作的结果传递给其它的代码。

如果将返回值加入你用来停止循环的 break 表达式,它会被停止的循环返回:

fn main() {
    let mut num = 1;
    let num = loop {
      num += 1;
      if num % 2 == 0 {continue};
      if num >= 10 {break num};
      println!("again! {}", num);
    };
    println!("最终的num:{}", num);
}

循环标签,再嵌套循环间指定要break或continue的是哪一个

如果存在嵌套循环,break 和 continue 应用于此时最内层的循环。

但可以选择在一个循环上指定一个循环标签(loop label),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环,而不再是最内层的循环了。

pub fn run() {
  let mut num = 1;
  let num = 'abc: loop {
    num += 1;
    if num % 2 == 0 {
      loop {
        if num * 10 > 100 {
          // 有标记,打破的是 'abc 这个循环,并返回 num
          break 'abc num;
        }
        // 没有标记,打破的是正常的最近的这个内部循环
        break;
      }
    } else if num >= 10 {
      // 有标记,打破的是 'abc 这个循环,并返回 num
      break 'abc num;
    };
  };
  println!("最终的num:{}", num);
}

while 条件循环

先判断条件再循环,同样可以使用 break 和 continue。

pub fn run() {
  let mut num = 1;
  while num < 14 {
    num += 1;
    if num > 8 {break;}
    if num > 5 { continue; }
    println!("num {} <= 5", num);
  }
  println!("最终的num:{}", num);
}

for 遍历集合

可以使用 while 结构来遍历集合中的元素,比如数组。

但可能会出现越界,就是索引值超出了集合的数量,会报错。

所以 for 循环是更简洁的替代方案。

pub fn run() {
  let arr: [u32; 4] = [1, 2, 3, 4];
  for value in arr {
    println!("值:{:?}", value)
  }
}

所有权

所有程序都必须管理其运行时使用计算机内存的方式。

一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存,也就是自动回收内存(CG模式);

在另一些语言中,程序员必须亲自分配和释放内存。

Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。

栈(Stack)与堆(Heap)

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。

栈:

以放入值的顺序存储值并以相反顺序取出值,这也被称作 后进先出(last in, first out)。

增加数据叫做 进栈(pushing onto the stack),而移出数据叫做 出栈(popping off the stack)

栈中的所有数据都必须占用已知且固定的大小,在编译时大小未知或大小可能变化的数据,要改为存储在堆上。

堆:

堆是缺乏组织的,当向堆放入数据时,你要请求一定大小的空间。

内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。

这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。

总结

使用栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间,其位置总是在栈顶。

访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。

rust 中的每个数据(变量、集合等等)都有作用域,离开作用域后,所使用的内存会被回收。

所有权规则

Rust 中的每一个值都有一个 所有者(owner)。

值在任一时刻(可以理解为一行执行完时),有且只有一个所有者。

当所有者(变量)离开作用域,这个值将被丢弃。

引用和借用

很多时候,我们调用某个方法传入参数时,这个方法只需要使用这个参数变量的使用权就行,不需要获得变量的所有权。

因为一旦获得了所有权,方法执行完后除非把所有权还回去,否则这个变量就被销毁了,原方法内的后续就不能再使用了。

这里可以创造一个变量的 引用,创造引用的动作,被称为借用。

如下的 name_string 就是 name 的一个引用:

pub fn run() {
  let name = String::from("Hello world world");
  // name_string 就是 name 的引用,下面这一行的行为被称作借用
  let name_string = &name;
  let len = get_len(name_string);
  println!("{}", len);
}

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

因为引用依托于本体,所以某变量的引用还没有离开作用域时,变量的本体也必须没有超出作用域。

比如下面的代码会报错:

pub fn main() {
  let s_str = get();
}

fn get() -> &String {
  let s = String::from("aaa");
  // 当 get 这个方法结束时,s 这个变量被销毁了
  // 但它的引用 &s 却被方法返回给其他方法了
  // 这就会报错
  return &s;
}

可变引用

使用引用也可以修改原数据,但需要进行调整,见代码中注释:

pub fn run() {
  // name 本身先改为可变变量
  let mut name = String::from("Hello world");
  // 引用也是一个可变引用
  let name_string = &mut name;

  // 把可变引入传入,让这个方法去修改 name
  add_name(name_string);
  println!("{}", name);
}

fn add_name(s: &mut String) -> () {
  let new_name = String::from(" newAdd");
  s.push_str(&new_name);
}

引用的规则

引用也是有使用规则的,不能无限制的给某个变量创建引用。

  • 在任意给定时间,要么只能有一个可变引用,要么 只能有n个不可变引用。

  • 引用必须总是有效的。

Slice 切片类型

slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

slice 是一类引用,所以它没有所有权。

slice 引用是固定的不可变引用。

fn run() {
  let s = String::from("hello");
  // s_str 的为 e 这一个字符的索引
  let s_str = &s[1..2]

  // s_str 的为 he 字符的索引
  let s_str = &s[..2]

  // s_str 的为 llo 字符的索引
  let s_str = &s[2..]

  // s_str 的为 hello 字符的索引
  let s_str = &s[..]
}

上面 s_str 的数据类型是 &str。

结构体 struct

struct,或者 structure,是一个自定义数据类型,允许你包装和命名多个相关的值,从而形成一个有意义的组合。

感觉就类似 ts 和 java 的接口,先定义,然后去实现,实现后看上去类似一个 js 的对象。

和元组一样,结构体的每一部分可以是不同类型。

但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。

由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。

结构体的定义和实例化

需要使用 struct 关键字并为整个结构体提供一个名字,结构体的名字需要描述它所组合的数据的意义。

接着在大括号中,定义每一部分数据的名字和类型,我们称为 字段(field)。

pub fn run() {
  // 定义结构体
  struct Man {
    name: String,
    is_woman: bool,
    age: u32,
  }

  // 实例化结构体
  let bob_1 = Man {
    name: String::from("bob"),
    is_woman: false,
    age: 30,
  };


  // 当前作用域有同名变量的字段时,可以简写
  let name = String::from("bob_2");
  let age = 30;
  let bob_2 = Man {
    name,
    is_woman: false,
    age,
  };

  // 可以借助现有结构体来实例化新结构体,..box_2 需要放在后面
  // 这里有一个注意点,见后面的笔记说明!!!!!!!
  // 此外这是一个可变结构体
  let mut bob_3 = Man {
    name: String::from("bob_3"),
    ..bob_2
  };
  
  // 可变结构体中任意一个字段都可以更改
  bob_3.age = 33;

  // 读取结构体
  println!("姓名:{},年龄:{},是女人:{}", bob_1.name, bob_1.age, bob_1.is_woman);
  println!("姓名:{},年龄:{},是女人:{}", bob_2.name, bob_2.age, bob_2.is_woman);
  println!("姓名:{},年龄:{},是女人:{}", bob_3.name, bob_3.age, bob_3.is_woman);
}

上面是例子,注意例子中有一个 注意点。

就是借助现有的 box_2 中的数据,实例化了 box_3,这里其实会执行 = 的赋值操作。

因为 box_2 中除了 name 字段,其他字段都是存在栈中的标量,所以会直接复制给 box_3。

因为 box_3 中自定义了 name 这个 String 类型的字段,否则,box_2.name 将不能再被使用,因为这个字段的所有权就会被转移给 box_3。

元祖结构体

可以定义一个值如元祖的结构体,不关注字段,只为定义一种固定类型的元祖。

比如颜色色值、点位置坐标的元祖。

pub fn run() {
  // 虽然变量名是 yellow,但类型只是普通元祖
  let yellow = (255, 255, 127);

  #[derive(Debug)]
  // 定义一个类型为 Color 的元祖结构体
  struct Color(u32, u32, u32);
  // 实例化元祖结构体,此时 white 的类型名为 Color
  let white = Color(255, 255, 255);

  println!("{:?}", yellow);
  println!("{:?}", white);
}

类单元结构体

一个没有任何字段的结构体,它们被称为 类单元结构体(unit-like structs)。

类单元结构体,常常在你想要在某个类型上实现 trait ,但又不需要在这个类型中存储数据的时候发挥作用。

下面是一个声明和实例化一个名为 AlwaysEqual 的 unit 结构的例子。

注意 subject 将不会又任何数据,但我们可以给它方便的添加方法,这个得看后面的 trait 的教程了。

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

示例:计算面积

下面是一个使用结构体定义矩形,并使用一个方法计算矩形面积的例子:

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

pub fn run() {
  // 一个矩形
  let rect1 = Rectangle {
    width: 10,
    height: 20,
  };

  // 防止所有权被转移,这里入参为结构体的引用
  let the_area = area(&rect1);
  println!("{}", the_area);

  // 打印结构体
  println!("{:#?}", rect1);
}

// 计算面积的方法
fn area(rect: &Rectangle) -> u32 {
  rect.width * rect.height
}

结构体的方法

但上面的例子中,计算面积是 Rectangle 这个结构体特有的方法,所以我们可以把他们两者关联起来:

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

// 定义方法
impl Rectangle {
  // 实现一个计算面积的方法
  // 第一个参数总是这个结构体本身
  fn area(&self) -> u32 {
    self.width * self.height
  }
}

pub fn run() {
  // 一个矩形
  let rect1 = Rectangle {
    width: 10,
    height: 20,
  };

  let the_area = rect1.area();
  println!("{}", the_area);

  // 打印结构体
  println!("{:#?}", rect1);
}

上面的代码中,使用 impl 给结构体 Rectangle 这个结构体添加了一个 function。

这个 function 的第一个参数固定是 self 或者 self 的引用,那么 function 就是这个结构体的 方法。

并且这个方法,只有在结构体被实例化后才能使用。

当使用结构体方法时,传入的参数,将从结构体方法的第二个入参才开始接收,因为第一个参数是 self。

结构体的关联函数

使用 impl 给结构体 Rectangle 这个结构体添加了一个 function 时,第一个参数也可以不是 self。

这种 function 称作关联函数。

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

impl Rectangle {
  fn area(&self) -> u32 {
    self.width * self.height
  }
  // 定义关联函数
  fn new(size: u32) -> Self {
    Self {
      width: size,
      height: size,
    }
  }
}

pub fn run() {
  // 使用关联函数创建一个矩形
  let rect1 = Rectangle::new(33);

  let the_area = rect1.area();
  println!("{}", the_area);
}

上面的实例中设置添加了 new 这个关联函数,这个 function 的第一个参数不是 self 或 &self。

他接收一个 u32,返回一个 Self,且函数内部也使用了 Self,这里的两个 Self 指的是这个结构体本身的定义,也就是 impl 后面的那个结构体,这里是 Rectangle。

使用关联函数,是使用 :: 来使用,其实我们使用的 String::from,from 就是 String 这个 struct 的关联函数。

可以为同一个 struct 写多个 impl 来添加 function,他们并不冲突。

枚举

枚举(enumerations),也被称作 enums。

枚举允许你通过列举可能的 成员(variants)来定义一个类型。

就好像结构体给予将字段和数据聚合在一起的方法,而枚举给予将一个值成为一个集合之一的方法。

枚举中的所有值都是这个枚举的成员,定义完枚举,需要用 :: 来使用其中的值。

定义和使用枚举

下面是最简单的一个枚举的示例:

pub fn run() {
  // 定义 ip 地址的类型,只有 V4 和 V6 两种
  enum IpAddrKind {
    V4,
    V6,
  }

  // 下面 four 和 six 的数据类型都是 IpAddrKind
  let four = IpAddrKind::V4;
  let six = IpAddrKind::V6;
}

具体的使用场景,比如就是要生命两个地址的结构体,要包含地址类型和具体地址:

pub fn run() {
  // 定义 ip 地址的类型,只有 IPv4 和 IPv6 两种
  #[derive(Debug)]
  enum IpAddrKind {
    V4,
    V6,
  }

  // 定义结构体,其中 kind 的数据类型是 IpAddrKind
  // 也就是说,kind 的值是 IpAddrKind 中的某一项
  #[derive(Debug)]
  struct IpAddr {
    kind: IpAddrKind,
    address: String,
  }

  // 家的IP地址信息
  let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
  };
  // 某个回路的地址信息
  let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::0"),
  };

  println!("{:?}", home);
  println!("{:?}", loopback);
}

更进一步,会发现上面使用定义 ip 地址信息时可以更明确,可以把 ip 地址和 ip 类型合二为一,枚举可以在定义成员时,给成员关联数据类型。

再进一步,IPv4 是由4个255以内的数字组成的,IPv6是单个字符串,所以可以写成下面这样。

实际关联的类型可以更灵活,比如可以自己声明一个结构体,作为关联的类型。

pub fn run() {
  // 定义 ip 地址和类型,只有 IPv4 和 IPv6 两种
  #[derive(Debug)]
  enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
  }

  // 家的IP地址信息
  let home = IpAddr::V4(127, 0, 0, 1);
  // 某个回路的地址信息
  let loopback = IpAddr::V6(String::from("::0"));

  println!("{:?}", home);
  println!("{:?}", loopback);
}

下面是一个更复杂的枚举示例:

Quit 没有关联任何类型。

Move 关联的是一个类似结构体的类型。

Write 关联的是一个字符串。

ChangeColor 关联的是三个 i32。

pub fn run() {
  // 更复杂的枚举
  #[derive(Debug)]
  enum Message {
    Quit,
    Move { x: i32, y: i32},
    Write(String),
    ChangeColor(i32, i32, i32),
  }
}

枚举的 impl

结构体和枚举还有另一个相似点:就像可以使用 impl 来为结构体定义方法那样,也可以在枚举上定义方法。

这是一个定义于我们 Message 枚举上的叫做 call 的方法:

pub fn run() {
  // 更复杂的枚举
  #[derive(Debug)]
  enum Message {
    Quit,
    Move { x: i32, y: i32},
    Write(String),
    ChangeColor(i32, i32, i32),
  }

  impl Message {
    fn call(&self) {
      // 在这里定义方法体
    }
  }

  let m = Message::Write(String::from("hello"));
  m.call();
}

Option 枚举

Option 是标准库定义的另一个枚举,Option 类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。

Option 使用场景,就是请求一个列表第一项时,返回的数据包装在 Option 枚举中,如果有值,就赋值为有值的那个成员(Some(T)),否则就赋值为没有值的那个成员(None)。

例如,如果请求一个非空列表的第一项,会得到一个值,如果请求一个空的列表,就什么也不会得到。

从类型系统的角度来表达这个概念就意味着编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他编程语言中非常常见的 bug。

Rust 并没有很多其他语言中有的空值(null)功能。

Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。

这个枚举是 Option,而且它定义于标准库中,如下:

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

Option 枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。

它的成员也是如此,可以不需要 Option:: 前缀来直接使用 Some 和 None。

当有一个 Some 值时,我们就知道存在一个值,而这个值保存在 Some 中。

pub fn run() {
  let had_1 = Some(5);
  let had_2 = Some(String::from("abc"));

  // 对于 None,需要手动指定类型,因为编译器不能推断 None 的数据类型。
  let none_1: Option<i32> = None;
  let none_2: Option<bool> = None;
}

但注意,下面的程序无法编译通过:

pub fn run() {
  let had_1 = Some(5);
  let had_2 = Some(6);

  let num = had_1 + had_2;
}

因为 had_1 和 had_2 都是 Option 类型,无法相加。

换句话说,在对 Option 进行运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。

那么当有一个 Option<T> 的值时,如何从 Some 成员中取出 T 的值来使用它呢?Option<T> 枚举拥有大量用于各种情况的方法:你可以查看它的文档。

match 控制流

Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。

模式可由字面值、变量、通配符和许多其他内容构成。

匹配普通值

下面就是一个示例:

fn get(num: u32) -> String {
  match num {
    1 => 1.to_string(),
    2 => 2.to_string(),
    3 => {
      3.to_string()
    },
    // 代指其他所有的可能,且 other 这个参数必须要用到
    other => other.to_string(),
  }
}

通配模式和_占位符

注意上面使用了一个变量名为 other,这是通配模式,这个变量代指的是其他所有未匹配到上面的值时要进行到操作,算是缺省处理。

但上面使用了 other 这个变量,那我们就必须使用,否则会报错。

担当我们不打算使用匹配到的值时,也就不需要 other 这个变量,那我们可以是用 _ 这个占位符:

fn get(num: u32) -> String {
  match num {
    1 => 1.to_string(),
    2 => 2.to_string(),
    3 => {
      3.to_string()
    },
    // 代指其他所有的可能,且 _ 这个通配符不会使用
    _ => String::from("a"),
  }
}

使用枚举

当我们使用枚举时,必须要穷尽枚举的所有可能,否则会报错,但也同样可以使用通配模式:

enum Temp { A, B, C, D }
pub fn run() {
  let string = get(Temp::D);
  println!("{}", string);
}

fn get(coin: Temp) -> String {
  match coin {
    Temp::A => String::from("a"),
    Temp::B => String::from("b"),
    Temp::C => {
      let string = String::from("c");
      string
    },
    _ => String::from("d"),
  }
}

绑定值模式

上一个例子只是直接值,而当存在绑定值时,同样可以使用。

enum Temp {
  A(String),
  B(u32, i32, bool),
  C,
  D,
}

pub fn run() {
  let temp_1 = Temp::A(String::from("哈"));
  let temp_2 = Temp::B(2, 4, true);

  println!("{}", get(temp_1));
  println!("{}", get(temp_2));
}

fn get(temp: Temp) -> u32 {
  match temp {
    Temp::A(_) => 1,
    Temp::B(u1, _, _) => u1,
    Temp::C => 3,
    _ => 4,
  }
}

if let 简洁控制流

当我们只想处理一种匹配,其他可能的全部直接忽略时,如果用 match 需要像下面这么写:

enum Temp {
  A(String),
  B(u32, i32, bool),
  C,
  D,
}

fn get(coin: Temp) {
  // 只处理一种情况,其他什么也不做
  match coin {
    Temp::A(s) => {
      println!("{}", s);
    },
    _ => (),
  }
}

还要使用这种结构并使用通配模式,代码会比较多余,这时可以使用 if let 这种更短的方式编写:

enum Temp {
  A(String),
  B(u32, i32, bool),
  C,
  D,
}

fn get_1(coin: Temp) {
  if let Temp::A(s) = coin {
    println!("{}", s);
  }
}

可以认为 if let 是 match 的一个语法糖,它当值匹配某一模式时执行代码而忽略所有其他值。

可以在 if let 中包含一个 else,else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else。

fn get_1(coin: Temp) {
  if let Temp::A(s) = coin {
    println!("{}", s);
  } else {
    println!("其他");
  }
}

模块系统

模块引入的功能,也就是 js 的 export/import 的功能。

下面几个概念需要知道。

  • 包(Packages):Cargo 的一个功能,它允许你构建、测试和分享 crate。

  • Crates :一个模块的树形结构,它形成了库或二进制项目。

  • 模块(Modules)和 use:允许你控制作用域和路径的私有性。

  • 路径(path):一个命名例如结构体、函数或模块等项的方式

Crate 和包

Crate

crate 是 Rust 在编译时最小的代码单位。

crate 可以包含模块,模块可以定义在其他文件,然后和 crate 一起编译。

crate 有两种形式:二进制项(binary crate)和库(library crate)。

  • 二进制项(binary crate)可以被编译为可执行程序,比如一个命令行程序或者一个服务器,它们必须有一个 main 函数来定义当程序被执行的时候所需要做的事情,目前我们所创建的 crate 都是二进制项。

  • 库(library crate)并没有 main 函数,它们也不会编译为可执行程序,它们提供一些诸如函数之类的东西,使其他项目也能使用这些东西,比如第二章的引入的 rand crate 包,就提供了生成随机数的东西。

crate root 是一个源文件,Rust 编译器以它为起始点,并构成你的 crate 的根模块。

包

包(package)是提供一系列功能的一个或者多个 crate,一个包会包含一个 Cargo.toml 文件,阐述如何去构建这些 crate。

Cargo 就是一个包含构建你代码的二进制项的包,Cargo 也包含这些二进制项所依赖的库.

其他项目也能用 Cargo 库来实现与 Cargo 命令行程序一样的逻辑。

包中可以包含至多一个库 crate (library crate)、任意多个二进制 crate (binary crate),但是必须至少包含一个 crate(无论是库的还是二进制的)。

src/main.rs 就是一个与包同名的二进制 crate 的根。

src/lib.rs 就是一个与包同名的库 crate 的 根。

如果一个项目名称为 **my-project **,当有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project 的二进制 crate。

如果这个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个二进制的和一个库的,且名字都与包相同。

通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。

定义模块

这里我们提供一个简单的参考,用来解释模块、路径、use关键词和pub关键词如何在编译器中工作,以及大部分开发者如何组织他们的代码。

  • 从 crate 根节点开始: 当编译一个 crate, 编译器首先在 crate 根文件(通常,对于一个库 crate 而言是src/lib.rs,对于一个二进制 crate 而言是src/main.rs)中寻找需要被编译的代码。

  • 声明模块: 在 crate 根文件中,你可以声明一个新模块;比如,你用mod garden声明了一个叫做garden的模块。编译器会在下列路径中寻找模块代码:

    • 内联,在大括号中,当mod garden后方不是一个分号而是一个大括号

    • 在文件 src/garden.rs

    • 在文件 src/garden/mod.rs

  • 声明子模块: 在除了 crate 根节点以外的其他文件中,你可以定义子模块。比如,你可能在src/garden.rs中定义了mod vegetables;。编译器会在以父模块命名的目录中寻找子模块代码:

    • 内联,在大括号中,当mod vegetables后方不是一个分号而是一个大括号

    • 在文件 src/garden/vegetables.rs

    • 在文件 src/garden/vegetables/mod.rs

  • 模块中的代码路径: 一旦一个模块是你 crate 的一部分,你可以在隐私规则允许的前提下,从同一个 crate 内的任意地方,通过代码路径引用该模块的代码。举例而言,一个 garden vegetables 模块下的Asparagus类型可以在crate::garden::vegetables::Asparagus被找到。

  • 私有 vs 公用: 一个模块里的代码默认对其父模块私有。为了使一个模块公用,应当在声明时使用pub mod替代mod。为了使一个公用模块内部的成员公用,应当在声明前使用pub。

  • use 关键字: 在一个作用域内,use关键字创建了一个成员的快捷方式,用来减少长路径的重复。在任何可以引用crate::garden::vegetables::Asparagus的作用域,你可以通过 use crate::garden::vegetables::Asparagus;创建一个快捷方式,然后你就可以在作用域中只写Asparagus来使用该类型。

引用模块项目的路径

来看一下 Rust 如何在模块树中找到一个项的位置,我们使用路径的方式,就像在文件系统使用路径一样。为了调用一个函数,我们需要知道它的路径。

路径有两种形式:

  • 绝对路径(absolute path)是以 crate 根(root)开头的全路径;对于外部 crate 的代码,是以 crate 名开头的绝对路径,对于对于当前 crate 的代码,则以字面值 crate 开头。

  • 相对路径(relative path)从当前模块开始,以 self(自身的层级)、super(父级的层级) 或当前模块的标识符开头。

绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。

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

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

使用 use 引入模块的其他写法

正常写法:

use std::collections::HashMap;

引入时使用 as 给模块重命名:

use std::io::Result as IoResult;

嵌套路径来消除大量的 use 行:

use std::cmp::Ordering;
use std::io;

// 改写为下面
use std::{cmp::Ordering, io};

使用 glob * 运算符导入所有:

use std::collections::*;

常见集合

Rust 标准库中包含一系列被称为 集合(collections)的非常有用的数据结构。

不同于内建的数组和元组类型,这些集合指向的数据是储存在堆上的,这意味着数据的数量不必在编译时就已知,并且还可以随着程序的运行增长或缩小。

Vector

第一个类型是 Vec<T>,也被称为 vector。

vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。

vector 只能储存相同类型的值。

vector 基本用法

因为没有向这个 vector 中插入任何值,所以需要添加一个类型注解,因为程序无法推断出里面将要放的是何种类型的数据。

vector 是用泛型实现的, Vec<T> 是一个由标准库提供的类型,它可以存放任何类型,而当 Vec 存放某个特定类型时,那个类型位于尖括号中。

pub fn run() {
  // 正儿八经的形式,但很少用
  // let mut v: Vec<i32> = Vec::new();

  // 简写形式
  let mut v: Vec<i32> = vec![];

  // 添加值
  v.push(1);
  v.push(2);

  // 读取值的两种形式
  // 第一种
  let first = v[0];
  // 第二种
  let second_option = v.get(1);
  let second: i32 = match second_option {
    Some(v) => *v,
    None => 666,
  };

  println!("{:#?}", v);
  println!("{:#?}", first);
  println!("{:#?}", second);
}

上面代码中读取值的形式有两种。

第一种的弊端,就是如果 v[索引值] 中填写的索引值超过了 v 的最大索引值,会直接程序崩溃。

而第二种本身是返回一个 Option,所以就算 v.get(索引值) 的索引值不存在,也不会崩溃。

vector 遍历

使用 for in 来遍历,可以直接得到每一项的索引。

也可以直接得到值,并操作修改值。

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

  // 遍历得到每一项的索引,并打印
  for val in &v {
    println!("{val}");
  }

  // 遍历得到每一项的可变索引,使用解引用运算符来解引用后再修改,再打印
  for val in &mut v {
    *val += 1;
    println!("{val}");
  }

  // 遍历得到每一项的值,并打印
  for val in v {
    println!("{val}");
  }
}

使用枚举来储存多种类型

vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。

幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举:

#[derive(Debug)]
struct People {
  name: String,
  age: u32,
}

#[derive(Debug)]
enum ZooList {
  北京动物园,
  南京动物园,
  东京动物园,
  西京动物园,
}

#[derive(Debug)]
struct Animal {
  varieties: String,
  zoo_name: ZooList,
}

#[derive(Debug)]
enum Info {
  People(People),
  Animal(Animal),
  ZooList(ZooList),
}

pub fn run() {
  // 定义三个 Info 类型的数据
  let 小明 = Info::People(People {
    name: String::from("小明"),
    age: 12,
  });
  let 小猪 = Info::Animal(Animal {
    varieties: String::from("小猪"),
    zoo_name: ZooList::北京动物园,
  });
  let 南京动物园 = Info::ZooList(ZooList::南京动物园);

  // 最后全放进 vector 中
  let list = vec![小明, 小猪, 南京动物园];

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

String

rust 的 String 使用 utf8 编码。

我们之前用过字符串字面值、字符串切片 slices:

pub fn run() {
  let 字符串字面值 = "字符串字面值";
  let 字符串切片 = &String::from("字符串切片")[..];
}

其实字符串字面值,也是字符串切片slices。

操作字符串

pub fn run() {
  // 创建字符串
  let mut str_1 = String::from("第一种方式");
  let mut str_2 = "第二种方式".to_string();

  // 更新字符串
  // push 只能添加一个字符,需要用单引号的字符类型
  str_1.push('新');
  // push 能添加字符串切片
  str_2.push_str(" 添加字符串");
  // String + &String,注意 str_1 所有权被交出去了,被 + 使用掉了
  let str_3 = str_1 + &str_2;

  // println!("{}", str_1); // 注意这里不能打印
  println!("{}", str_2);
  println!("{}", str_3);
}

多项相加

但如果比较复杂的操作,比如用 - 连接 A、B、C、D,操作就比较麻烦:

pub fn run() {
  let str_A = String::from("A");
  let str_B = String::from("B");
  let str_C = String::from("C");
  let str_D = String::from("D");

  // 多项复杂相加
  let all = str_A + "-" + &str_B + "-" + &str_C + "-" + &str_D;
}

索引字符串的错误

在 rust 索引字符串是很麻烦的。

他无法像其他语言那样使用 "天地玄黄"[0] 类似的写法获取指定索引位置的字符。

比如:

pub fn run() {
  let str = String::from("天地玄黄");
  let a = str[0];
}

// 执行会报错
// `String` cannot be indexed by `{integer}`

错误和提示说明了全部问题:Rust 的字符串不支持索引。

String 是一个 Vec<u8> 的封装,下面通过获取字符串的 .len() 来说明。

  let hello = String::from("Hola");
  println!("{}", hello.len());
  // 打印
  // 4

  let hello = String::from("天地玄黄");
  println!("{}", hello.len());
  // 打印
  // 12

上面的 天地玄黄 四个字的 .len() 是 12,这是使用 UTF-8 编码 天地玄黄 所需要的字节数。

所以,当使用 "天地玄黄"[0] 时,得到是这 12 个字节的第一个字节对应的位置。

这个位置对应并不是一个完整的字,比如 "天" 这个字当使用 UTF-8 编码时,由 101、102、103 这三个字节组成,那 "天地玄黄"[0] 得到的是 102,但用户通常不会想要这样一个字节值被返回。

即便 &"hello"[0] 是返回字节值的有效代码,它也应当返回 104 而不是 h。

为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。

字节、标量值和字形簇

从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。

比如这个用梵文书写的印度语单词 “नमस्ते”。

最终它储存在 vector 中的 u8 值看起来像这样,下面展示的是字节,也就是计算机最终会储存的数据:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

如果从 Unicode 标量值的角度理解它们,也就像 Rust 的 char 类型那样,这些字节看起来像这样:

['न', 'म', 'स', '्', 'त', 'े']

这里有六个 char,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。

最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:

["न", "म", "स्", "ते"]

Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。

最后一个 Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。

字符串 slice

索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串 slice。

因此,如果你真的希望使用索引创建字符串 slice 时,Rust 会要求你更明确一些。

为了更明确索引并表明你需要一个字符串 slice,相比使用 [] 和单个值的索引,可以使用 [] 和一个 range 来创建含特定字节的字符串 slice:

let hello = "天地玄黄";

let s = &hello[0..3];

这里,s 会是一个 &str,它包含字符串的头三个字节。

因为 天地玄黄 四个字总的 .len() 是12,天 一个字实际是占用了 3 个字符,所以上面如果打印 s,那么实际 s 就是 天 这一个字。

如果我们就只取一个字符呢?实际编译时就直接报错了,因为只取一个,不是字符簇的边界,不能稳稳取到一整个字。

遍历字符串的方法

操作字符串每一部分的最好的方法是明确表示需要字符还是字节。

对于单独的 Unicode 标量值使用 chars 方法。

对 “Зд” 调用 chars 方法会将其分开并返回两个 char 类型的值,接着就可以遍历其结果来访问每一个元素了:

for c in "Зд".chars() {
    println!("{c}");
}
// 这些代码会打印出如下内容:
// З
// д

另外 bytes 方法返回每一个原始字节,这可能会适合你的使用场景:

for b in "Зд".bytes() {
    println!("{b}");
}
// 这些代码会打印出组成 String 的 4 个字节:
// 208
// 151
// 208
// 180

不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。

从字符串中获取如同天城文这样的字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。

Hash Map

常用集合类型是 哈希 map(hash map),HashMap<K, V> 类型储存了一个键类型 K 对应一个值类型 V 的映射。

它通过一个 哈希函数(hashing function)来实现映射,决定如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。

哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。

新建

可以使用 new 创建一个空的 HashMap,并使用 insert 增加元素。

必须首先 use 标准库中集合部分的 HashMap。

像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 HashMap 的键类型是 String 而值类型是 i32。

类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

比如用一个 HashMap 记录蓝队、黄队的分数:

use std::collections::HashMap;

pub fn run() {
  let mut scores = HashMap::new();
  scores.insert(String::from("Blue"), 10);
  scores.insert(String::from("Yellow"), 50);

  println!("{:#?}", scores)
}

访问哈希 map 中的值

可以通过 get 方法并提供对应的键来从哈希 map 中获取值:

use std::collections::HashMap;

pub fn run() {
  let mut scores = HashMap::new();
  scores.insert(String::from("Blue"), 10);
  scores.insert(String::from("Yellow"), 50);

  // 得到 Option
  let blue_some = scores.get("Blue");
  println!("{:?}", blue_some.copied().unwrap_or(0));
}

blue_some 是一个 Option,get 方法返回 Option<&V>,如果某个键在哈希 map 中没有对应的值,get 会返回 None。

程序中通过调用 copied 方法来获取一个 Option<i32> 而不是 Option<&i32>,接着调用 unwrap_or,如果 score 没有对应键的项将 score 设置为零。

可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 for 循环:

use std::collections::HashMap;

pub fn run() {
  let mut scores = HashMap::new();
  scores.insert(String::from("Blue"), 10);
  scores.insert(String::from("Yellow"), 50);

  for (key, val) in &scores {
    println!("key: {}, val: {}", key, val);
  }
}

哈希 map 和所有权

对于像 i32 这样的实现了 Copy trait 的类型,其值可以拷贝进哈希 map。

对于像 String 这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者。

更新哈希 map

尽管键值对的数量是可以增长的,每个唯一的键只能同时关联一个值。

当我们想要改变哈希 map 中的数据时,必须决定如何处理一个键已经有值了的情况:

  • 覆盖一个值:完全无视旧值并用新值代替旧值。

  • 只在键没有对应值时插入键值对:选择保留旧值而忽略新值,并只在键没有对应值时增加新值。

  • 可以结合新旧两值。

覆盖一个值

如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。

只在键没有对应值时插入键值对

我们经常会检查某个特定的键是否已经存在于哈希 map 中并进行如下操作:如果哈希 map 中键已经存在则不做任何操作,如果不存在则连同值一块插入。

为此哈希 map 有一个特有的 API,叫做 entry,它获取我们想要检查的键作为参数。

entry 函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。

比如说我们想要检查红队的键是否关联了一个值,如果没有,就插入值 50:

use std::collections::HashMap;

pub fn run() {
  let mut scores = HashMap::new();
  scores.entry(String::from("Red")).or_insert(50);
}

根据旧值更新一个值

另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。

例如,计数一些文本中每一个单词分别出现了多少次。

我们使用哈希 map 以单词作为键并递增其值来记录我们遇到过几次这个单词,如果是第一次看到某个单词,就插入值 0:

use std::collections::HashMap;

pub fn run() {
  let text = "hello ha , my name is ha ha , you name is hello .";
  let mut map = HashMap::new();

  for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
  }

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

错误处理

Rust 要求你承认错误的可能性,并在你的代码编译前采取一些行动。

这一要求使你的程序更加健壮,因为它可以确保你在将代码部署到生产环境之前就能发现错误并进行适当的处理。

Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。

对于一个可恢复的错误,比如文件未找到的错误,我们很可能只想向用户报告问题并重试操作。

不可恢复的错误总是 bug 出现的征兆,比如试图访问一个超过数组末端的位置,因此我们要立即停止程序。

大多数语言并不区分这两种错误,并采用类似异常这样方式统一处理他们。

Rust 没有异常,相反,它有 Result<T, E> 类型,用于处理可恢复的错误,还有 panic! 宏,在程序遇到不可恢复的错误时停止执行。

本章首先介绍 panic! 调用,接着会讲到如何返回 Result<T, E>。此外,我们将探讨在决定是尝试从错误中恢复还是停止执行时的注意事项。

panic! 处理不可恢复的错误

程序崩溃报错,一般有两种情况会导致:

  • 代码出问题,panic 崩溃。

  • 代码调用 panic! 宏,手动 panic。

通常情况下这些 panic 会打印出一个错误信息,展开并清理栈数据,然后退出。

通过一个环境变量,你也可以让 Rust 在 panic 发生时打印调用堆栈(call stack)以便于定位 panic 的原因。

当出现 panic 时,程序默认会开始 展开(unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。

另一种选择是直接 终止(abort),这会不清理数据就退出程序,那么程序所使用的内存需要由操作系统来清理。

如果你需要项目的最终二进制文件越小越好,panic 时通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展开切换为终止:

[profile.release]
panic = 'abort'

错误实例

程序出现错误

执行一下程序:

pub fn run() {
  second = 12;
}

会报错:

   Compiling rust-study v0.1.0 (/Users/majun/myCode/rust-study)
error[E0425]: cannot find value `second` in this scope
 --> src/a_08/a_08_01_01.rs:2:3
  |
2 |   second = 12;
  |   ^^^^^^
  |
help: you might have meant to introduce a new binding
  |
2 |   let second = 12;
  |   +++

For more information about this error, try `rustc --explain E0425`.
error: could not compile `rust-study` due to previous error

手动调用错误

下面使用 panic! 手动调用 panic。

pub fn run() {
  panic!("这是一个错误!");
}

会出发报错:

   Compiling rust-study v0.1.0 (/Users/majun/myCode/rust-study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `target/debug/rust-study`
thread 'main' panicked at '这是一个错误!', src/a_08/a_08_01_01.rs:2:3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

最后两行包含 panic! 调用造成的错误信息。

第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置。

被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 panic! 宏的调用。

在其他情况下,panic! 可能会出现在我们的代码所调用的代码中。

错误信息报告的文件名和行号可能指向别人代码中的 panic! 宏调用,而不是我们代码中最终导致 panic! 的那一行。

我们可以使用 panic! 被调用的函数的 backtrace 来寻找代码中出问题的地方。

下面我们会详细介绍 backtrace 是什么。

使用 panic! 的 backtrace

注意上面的报错:

   Compiling rust-study v0.1.0 (/Users/majun/myCode/rust-study)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running `target/debug/rust-study`
thread 'main' panicked at '这是一个错误!', src/a_08/a_08_01_01.rs:2:3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

下面的说明(note)行提醒我们可以设置 RUST_BACKTRACE 环境变量来得到一个 backtrace。backtrace 是一个执行到目前位置所有被调用的函数的列表。

Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件,这就是问题的发源地,这一行往上是你的代码所调用的代码;往下则是调用你的代码的代码。

这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。

让我们将 RUST_BACKTRACE 环境变量设置为任何不是 0 的值来获取 backtrace 看看。

$RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/rust-study`
thread 'main' panicked at '这是一个错误!', src/a_08/a_08_01_01.rs:2:3
stack backtrace:
   0: rust_begin_unwind
             at /rustc/90743e7298aca107ddaa0c202a4d3604e29bfeb6/library/std/src/panicking.rs:575:5
   1: core::panicking::panic_fmt
             at /rustc/90743e7298aca107ddaa0c202a4d3604e29bfeb6/library/core/src/panicking.rs:65:14
   2: rust_study::a_08::a_08_01_01::run
             at ./src/a_08/a_08_01_01.rs:2:3
   3: rust_study::main
             at ./src/main.rs:44:5
   4: core::ops::function::FnOnce::call_once
             at /rustc/90743e7298aca107ddaa0c202a4d3604e29bfeb6/library/core/src/ops/function.rs:251:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

这里有大量的输出!你实际看到的输出可能因不同的操作系统和 Rust 版本而有所不同。为了获取带有这些信息的 backtrace,必须启用 debug 标识。

当不使用 --release 参数运行 cargo build 或 cargo run 时 debug 标识会默认启用,就像这里一样。

Result 处理可恢复的错误

大部分错误并没有严重到需要程序完全停止执行。

Result 枚举,它定义有如下两个成员,Ok 和 Err:

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

T 和 E 是泛型类型参数。

现在你需要知道的就是 T 代表成功时返回的 Ok 成员中的数据的类型,而 E 代表失败时返回的 Err 成员中的错误的类型。

因为 Result 有这些泛型类型参数,我们可以将 Result 类型和标准库中为其定义的函数用于很多不同的场景,这些情况中需要返回的成功值和失败值可能会各不相同。

让我们调用一个返回 Result 的函数,因为它可能会失败:

use std::fs::File;

pub fn run() {
  let _file_result = File::open("./src/a_08/hello.txt");
}

File::open 的返回值是 Result<T, E>。

如果读取文件成功,则泛型参数 T 会被放入 std::fs::File。

如果读取文件失败,则泛型参数 E 会被放入 std::io::Error。

File::open 函数需要一个方法在告诉我们成功与否的同时返回文件句柄或者错误信息,这些信息正好是 Result 枚举所代表的。

所以我们需要用 match 处理 Result 枚举。

注意与 Option 枚举一样,Result 枚举和其成员也被导入到了 prelude 中,所以就不需要在 match 分支中的 Ok 和 Err 之前指定 Result::。

use std::fs::File;

pub fn run() {
  let _file_result = File::open("./src/a_08/hello.txt");

  let file = match _file_result {
    Ok(file) => file,
    Err(error) => panic!("打开文件失败: {:?}", error),
  };

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

这里我们告诉 Rust 当结果是 Ok 时,返回 Ok 成员中的 file 值,然后将这个文件句柄赋值给变量 greeting_file,match 之后,我们可以利用这个文件句柄来进行读写。

match 的另一个分支处理从 File::open 得到 Err 值的情况,在这种情况下,我们选择调用 panic! 宏。

匹配不同的错误

上面的代码不管 File::open 是因为什么原因失败都会 panic!。

我们希望对不同的错误原因采取不同的行为:

  • 如果 File::open 因为文件不存在而失败,我们希望创建这个文件并返回新文件的句柄。

  • 如果 File::open 因为任何其他原因失败,例如没有打开文件的权限,我们仍然希望像上面那样 panic!。

所以下面的代码使用 match 创建了出现错误时的不同分支,来处理当文件不存在时的情况。

use std::{fs::File, io::ErrorKind};

pub fn run() {
  let _file_result = File::open("./src/a_08/hello.txt");

  let file = match _file_result {
    Ok(file) => file,
    Err(error) => {
      match error.kind() {
        ErrorKind::NotFound => {
          match File::create("./src/a_08/hello.txt") {
            Ok(fc) => fc,
            Err(e) => panic!("创建文件失败: {}", e),
          }
        },
        other_error => {
          panic!("读取文件失败: {}", other_error);
        }
      }
    },
  };

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

unwrap 和 expect

match 能够胜任它的工作,不过它可能有点冗长并且不总是能很好的表明其意图。

Result<T, E> 类型定义了很多辅助方法来处理各种情况,其中之一叫做 unwrap。

如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!。

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

还有另一个类似于 unwrap 的方法它还允许我们选择 panic! 的错误信息:expect。

使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").expect("文件打开失败");
}

expect 与 unwrap 的使用方式一样:返回文件句柄或调用 panic! 宏。

expect 在调用 panic! 时使用的错误信息将是我们传递给 expect 的参数,而不像 unwrap 那样使用默认的 panic! 信息。

传播错误

很多时候某个方法中出现错误,但我们不想让这个方法处理错误,而是让它把错误传播给上级,也就是这个方法的调用者。

这被称为 传播(propagating)错误。

下面的 get_file 方法就是把错误进行了传播,其实就是返回一个 Result,让上级去处理。

use std::{fs::File, io::{Read, self}};

pub fn run() {

  let result = get_file("./src/a_08/hello.txt");

  let username = match result {
    Ok(username) => username,
    Err(e) => panic!("出现了错误:{}", e),
  };

  println!("{}", username);
}

fn get_file(path: &str) -> Result<String, io::Error> {

  let _file_result = File::open(path);

  let mut file = match _file_result {
    Ok(file) => file,
    Err(e) => return Err(e),
  };

  let mut username = String::new();
  
  match file.read_to_string(&mut username) {
    Ok(_) => Ok(username),
    Err(e) => Err(e),
  }
}

传播错误的简写 ? 运算符

下面使用 ? 运算符重写了 get_file 方法:

use std::{fs::File, io::{Read, self}};

pub fn run() {

  let result = get_file("./src/a_08/hello.txt");

  let username = match result {
    Ok(username) => username,
    Err(e) => panic!("出现了错误:{}", e),
  };

  println!("{}", username);
}

fn get_file(path: &str) -> Result<String, io::Error> {

  let mut _file_result = File::open(path)?;
  let mut username = String::new();
  _file_result.read_to_string(&mut username)?;
  
  Ok(username)
}

? 运算符会在遇到错误时,直接返回 Result::Err。

使用 ? 运算符,可以进一步简写,写成类似 js 的链式调用:

fn get_file(path: &str) -> Result<String, io::Error> {
  let mut username = String::new();
  File::open(path)?.read_to_string(&mut username)?;
  Ok(username)
}

将文件读取到一个字符串是相当常见的操作,所以 Rust 提供了名为 fs::read_to_string 的函数:

use std::{fs, io};

pub fn run() {

  let result = get_file("./src/a_08/hello.txt");

  let username = match result {
    Ok(username) => username,
    Err(e) => panic!("出现了错误:{}", e),
  };

  println!("{}", username);
}

fn get_file(path: &str) -> Result<String, io::Error> {
  fs::read_to_string(path)
}

哪里可以使用 ? 运算符

? 运算符只能被用于返回值与 ? 作用的值相兼容的函数,因为 ? 运算符被定义为从函数中提早返回一个值。

? 可以用于 Result 和 Option。

pub fn run() {
  let result = last_char_of_first_line("sdf");
  println!("{:?}", result);
}

fn last_char_of_first_line(text: &str) -> Option<char> {
  text.lines().next()?.chars().last()
}

Box<dyn Error> 类型是一个 trait 对象(trait object),目前可以将 Box<dyn Error> 理解为 “任何类型的错误”:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

要不要 panic!

如果代码 panic,就没有恢复的可能。

你可以选择对任何错误场景都调用 panic!,不管是否有可能恢复,不过这样就是你代替调用者决定了这是不可恢复的。

选择返回 Result 值的话,就将选择权交给了调用者,而不是代替他们做出决定,这是最好的选择。

示例、代码原型和测试都非常适合 panic

当你编写一个示例来展示一些概念时,在拥有健壮的错误处理代码的同时也会使得例子不那么明确。

在我们准备好决定如何处理错误之前,unwrap和expect方法在原型设计时非常方便,当我们准备好让程序更加健壮时,它们会在代码中留下清晰的标记。

当我们比编译器知道更多的情况

当你有一些其他的逻辑来确保 Result 会是 Ok 值时,调用 unwrap 或者 expect 也是合适的,虽然编译器无法理解这种逻辑。

比如下面的例子,parse() 方法必然是成功的不会 panic,所以我们可以直接使用 expect 来处理。

    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");

泛型、Trait 和生命周期

每一个编程语言都有高效处理重复概念的工具(重复代码、重复方法等等)。

在 Rust 中其工具之一就是 泛型(generics)。泛型是具体类型或其他属性的抽象替代。

我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。

比如一个用于加法计算的函数,我们可以设置入参是两个 u32,但实际上 i32 的参数也同样需要这个方法,所以我们可以用 T 来代替。

了解泛型后,我们需要了解 trait,这是一个定义泛型行为的方法,trait 可以与泛型结合来将泛型限制为只接受拥有特定行为的类型,而不是任意类型。

最后是 生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型,Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。

泛型数据类型

当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。

采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。

比如下面的 largest 方法,就是无论入参数组中的是什么类型,都可以调用这个函数。

注意 largest 方法的 T 的限制:std::cmp::PartialOrd,只有添加了这一行,编译器才能保证调用 largest 方法时,传入的数组中的数据类型都是可以使用 > 这个算数符的。

pub fn run() {
  let a_list = [1, 4, 6, 3, 8, 2, 4, 5];
  let largest = largest(&a_list);
  println!("{}", largest);
}

fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  let mut largest = &list[0];
  for item in list {
    if item > largest {
      largest = item;
    }
  }
  largest
}

结构体定义中的泛型

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。

正常定义普通的结构体:

struct Point {
  x: u32,
  y: u32,
}

使用了泛型的结构体:

// 使用了泛型的定义
struct Point<T> {
  x: T,
  y: T,
}

pub fn run() {
  // Point<i32> 类型的点
  let integer = Point {
    x: 12,
    y: 13,
  };
  // Point<f64> 类型的点
  let float = Point {
    x: 12.2,
    y: 13.2,
  };
}

其语法类似于函数定义中使用泛型。

首先,必须在结构体名称后面的尖括号中声明泛型参数的名称,接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

注意上面的实例,因为 x 和 y 的数据类型都是 T,说明两个是相同类型的。

如果想要是不同类型的,需要像下面这样定义:

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

pub fn run() {
  // Point<i32> 类型的点
  let integer = Point {
    x: 12,
    y: 13.2,
    z: 10,
  };
  // Point<f64> 类型的点
  let float = Point {
    x: 12.2,
    y: 13,
    z: 12.4,
  };
}

上面的示例中,表明 x 和 z 是同一类型,y 是另一种类型,所以在实例化时的数据,满足此要求才能声明成功。

枚举定义中的泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。

最近有使用的两种枚举都是使用泛型:

// 结果 Result 的枚举
enum Result<T, E> {
  Ok(T),
  Err(E),
}

// 取值结果的 Option 的枚举
enum Option<T> {
  Some(T),
  None,
}

同理,我们也可以自定义其他类似用法的枚举。

结构体方法中的泛型

在为结构体和枚举实现方法时,一样也可以用泛型。

并且,可以根据泛型的类型的不同,转为某些类型添加方法,而其他不符合类型条件的则没有。

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

// 无论 T 为何种类型,都会有下面的方法 x
impl<T> Point<T> {
  fn x(&self) -> &T {
    &self.x
  }
}
// 只有 T 为 u32 时,才会有下面的方法add
impl Point<u32> {
  fn add(&self) -> u32 {
    self.x + self.y
  }
}

pub fn run() {
  let point = Point {
    x: 12,
    y: 14,
  };
  let x = point.x();

  println!("{}", x);
  println!("{}", point.y);
  println!("{}", point.add());
}

Trait:定义共同行为

trait 定义了某个特定类型拥有可能与其他类型共享的功能。

可以通过 trait 以一种抽象的方式定义共享的行为。

可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

trait 类似于其他语言中的常被称为 接口(interfaces)的功能,虽然有一些不同。

一个类型的行为由其可供调用的方法构成,如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。

这里的类型暂时可以理解为 struct 结构体,这个结构体的行为是由它的方法构成的。

但如果,不同的结构体都实现了同一个方法签名(也就是相同的方法名、入参类型、返回值类型),那这些类型就可以共享这个方法签名了。

方法签名确定了,行为也就确定了。

trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

trait 就是把一个方法签名定义一下,由结构体去实现,只要实现了这个 trait 的结构体,就必须实现了这个方法。

使用 trait

声明的 trait 中的每一项,都是一个方法签名,只需要有方法名称、入参类型、返回值类型。

下面定义了 Summary 这个 trait 后,又定义了两个结构体,实现这两个结构体的方法时,也指明了是要实现 Summary 这个 trait。

所以里面的方法都要是 Summary 中定义过方法签名的那些方法,否则编译会直接报错。

这就保证了一点:所有实现了 trait 的结构体,必然都能使用 trait 中定义的方法!

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

// 第一个结构体
struct NewsArticle {
  pub headline: String,
  pub location: String,
  pub author: String,
  pub content: String,
}

// 为 NewsArticle 实现 Summary 这个trait
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,
}
// 为 Tweet 实现 Summary 这个trait
impl Summary for Tweet {
  fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
  }
}

pub fn run() {
}

限制:

为某类型实现某 trait 时,有一个限制是:类型和 trait,至少有一项是此 crate 本地的作用域中的(是本地代码写的,而不是从某个依赖的 ctate 引入的)。

所以,我们不能为外部的类型实现外部的trait。

例如,不能在自己的代码中,为 Vec<T> 实现 Display trait,这是因为 Display 和 Vec 都定义于标准库中,它们并不位于 crate 本地作用域中。

这个限制是被称为 相干性(coherence)的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于不存在父类型。

这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。

没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

默认实现

有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。

这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。

例如上面的 Summary trait:

// 声明 trait
pub trait Summary {
  fn summarize(&self) -> String {
    String::from("暂无")
  }
}

// 结构体
struct NewsArticle {
  pub headline: String,
  pub location: String,
  pub author: String,
  pub content: String,
}

// 为 NewsArticle 实现 Summary 这个trait
impl Summary for NewsArticle {
  // 并没有实现 summarize 这个方法也不会报错
}
pub fn run() {
}

trait 作为参数

可以探索一下如何使用 trait 来接受多种不同类型的参数。

比如我们声明一个方法,可以限制这个方法的参数是一个实现了某个 trait 的结构体。

实现了某个 trait 的结构体,说明这个结构体实例上面必然已经实现了 trait 定义的方法。

这样,在这个方法里面就可以使用这个参数的某些方法了。

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

// 这个方法要求传入的参数 item,是实现了 Summary 这个 trait 的类型
fn get(item: &impl Summary) -> String {
  item.summarize()
}

Trait Bound 语法

impl Trait 语法适用于直观的例子,但它实际上是一种较长形式语法的语法糖。

这种比较长形式的写法,我们称为 trait bound,它看起来像:

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

这与之前的例子相同,不过稍微冗长了一些,trait bound 与泛型参数声明在一起,位于尖括号中的冒号后面。

impl Trait 很方便,适用于短小的例子,而更长的 trait bound 则适用于更复杂的场景,比如:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

// 可以用 trait bound 改写为下面的形式
pub fn notify<T: Summary>(item1: &T, item2: &T) {}

通过 + 指定多个 trait bound

如果 notify 需要显示 item 的格式化形式,同时也要使用 summarize 方法,那么 item 就需要同时实现两个不同的 trait:Display 和 Summary,这可以通过 + 语法实现:

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

+ 语法也适用于泛型的 trait bound:

pub fn notify<T: Summary + Display>(item: &T) {}

通过指定这两个 trait bound,notify 的函数体可以调用 summarize 并使用 {} 来格式化 item。

通过 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 {}

还可以像这样使用 where 从句:

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,
  }
}

通过使用 impl Summary 作为返回值类型,我们指定了 returns_summarizable 函数返回某个实现了 Summary trait 的类型,但是不确定其具体的类型。

不过这只适用于返回单一类型的情况。

例如,这段代码的返回值类型指定为返回 impl Summary,但是返回了 NewsArticle 或 Tweet 就行不通:

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,
    }
  }
}

这不能编译,因为 impl Trait 工作方式的限制。第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分会介绍如何编写这样一个函数。

使用 trait bound 有条件地实现方法

通过使用带有 trait bound 的泛型参数的 impl 块,可以有条件地只为那些实现了特定 trait 的类型实现方法。

use std::fmt::Display;

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

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

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);
        }
    }
}
PreviousRust 基本NextRust 调用 Object-C 的API

Last updated 3 months ago

Was this helpful?