Rust 语言从入门到实战 唐刚--读书笔记08

08|Option<T>与Result<T, E>、迭代器

学习高频使用的  Option<T> 、Result<T, E> 、迭代器,夯实集合中所有权相关的知识点。

Option<T> 和 Result<T, E> 不是 Rust 的独创设计,OCaml、Haskell、Scala ;Kotlin、Swift C++17 。使用 Option<T> 和 Result<T, E> 成了编程语言的新共识。

迭代器是目前几乎所有主流语言的标配。

Option<T> 与 Result<T, E>

Option<T> 与 Result<T, E> 随处可见,实际是带类型参数的枚举类型。

Option<T> 的定义

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

Option<T> 定义:含两个变体的枚举。不带负载的 None,带一个类型参数作为其负载的 Some。Option<T> 的实例在 Some 和 None 中取值, 表示这个实例有取空值的可能。

Option<T> 把空值单独提出来了一个维度。没有 Option<T> 的语言中,空值是分散在其他类型中的。比如空字符串、空数组、数字 0、NULL 指针等。有的语言还把空值区分为空值和未定义的值,如 nil、undefined 等。

  • Rust 中的变量定义后使用前都必须初始化,不存在未定义值情况。
  • Rust 把空值单独提出来统一定义成 Option<T>::None,在标准库层面上做好了规范,上层的应用在设计时遵循这个规范。

示例。

let s = String::from("");
let a: Option<String> = Some(s);

变量 a 是携带空字符串的 Option<String> 类型。空字符串""的“空”与 None 的“无”表达了不同的意义。

 Tony Hoare :空引用(Null references)是“十亿美元”的错误。

Rust 通过所有权并借用检查器、Option<T>、Result<T, E> 等一整套机制全面解决了 Hoare 想解决的问题。

Result<T, E> 的定义

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

Result<T, E> 定义:包含两个变体的枚举。两个变体各自带一个类型参数作为其负载。Ok(T) 表示结果正确,Err(E) 表示结果有错误。

对比其他语言函数错误返回的约定,C、CPP、Java 语言里有时用返回 0 来表示函数执行正确,有时又不是这样,你需要根据代码所在的上下文环境来判断返回什么值代表正确,返回什么值代表错误。

Go 语言强制对函数返回值做出了约定。

ret, err := function()
if err != nil {

约定要求函数返回两个值,正确,ret 放返回值,err 为 nil。要返回错误值,给 err 变量填充具体的内容 --> 经典的满屏 if err != nil 代码,成了 Go 语言圈的一个梗。Go 语言已经朝着把错误信息和正常返回值类型剥离开来的方向走出了一步。

Rust 像 Go 那样设计:Rust 不存在单独的 nil 这种空值,Rust 用带类型参数的枚举就可达到这个目的。

一个枚举实例在一个时刻只能是某一个变体。函数返回值,不论正确还是错误,都能用 Result<T, E> 类型统一表达,更紧凑。Result<T, E>  是一种类型,可在它之上添加很多操作,很方便。

let r: Result<String, String> = function();

表示将函数返回值赋给变量 r,返回类型是 Result<String, String>。正确,返回为 String 类型;错误,返回的错误类型也是 String。这两个类型参数可以被任意类型代入。

Result<T, E> 用来支撑 Rust 的错误处理机制,非常重要。

第 18 讲,基于 Result<T, E> 的错误处理。

解包

Option<u32>::Some(10) 和 10u32 明显不是同一种类型。通过枚举变体,真正想要的值被“包裹”。想获取被包在里面的值该怎么做?

其实有很多办法,先讲一类解包操作。

三种方法, expect()、unwrap()、unwrap_or()。解包的具体操作和示例代码,有什么不同。

// Option
let x = Some("value");
assert_eq!(x.expect("fruits are healthy"), "value");
// Result
let path = std::env::var("IMPORTANT_PATH")
    .expect("env variable `IMPORTANT_PATH` should be set by `wrapper_script.sh`");

// Option
let x = Some("air");
assert_eq!(x.unwrap(), "air");
// Result
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.unwrap(), 2);

// Option
assert_eq!(Some("car").unwrap_or("bike"), "car");
assert_eq!(None.unwrap_or("bike"), "bike");

// Result
let default = 2;
let x: Result<u32, &str> = Ok(9);
assert_eq!(x.unwrap_or(default), 9);

let x: Result<u32, &str> = Err("error");
assert_eq!(x.unwrap_or(default), default);

// Option
let x: Option<u32> = None;
let y: Option<u32> = Some(12);

assert_eq!(x.unwrap_or_default(), 0);
assert_eq!(y.unwrap_or_default(), 12);

// Result
let good_year_from_input = "1909";
let bad_year_from_input = "190blarg";
let good_year = good_year_from_input.parse().unwrap_or_default();
let bad_year = bad_year_from_input.parse().unwrap_or_default();

assert_eq!(1909, good_year);
assert_eq!(0, bad_year);

解包操作挺费劲的。如果总是先用 Option<T> 或 Result<T, E> 把值包裹起来,用的时候再手动解包,那其实说明没有真正抓住到 Option<T> 和 Result<T, E> 的设计要义。在 Rust 中,很多时候不需要解包也能操作里面的值,不用多此一举的解包操作。

不解包的情况下如何操作?

不解包,要获取被包在里面的值,用 Option<T> 和Result<T, E> 的常用方法。

Option<T> 上的常用方法和示例:

1、map():

  • Option 是 Some ,通过 map 中提供的函数或闭包把 Option 里的类型转换成另一种类型
  • Option 是 None ,保持 None 不变。
  • map() 会消耗原类型,也就是获取所有权。
let maybe_some_string = Some(String::from("Hello, World!"));
let maybe_some_len = maybe_some_string.map(|s| s.len());
assert_eq!(maybe_some_len, Some(13));
 
let x: Option<&str> = None;
assert_eq!(x.map(|s| s.len()), None);

2、cloned():通过克隆 Option 里面的内容,把 Option<&T> 转换成 Option<T>。

let x = 12;
let opt_x = Some(&x);
assert_eq!(opt_x, Some(&12));
let cloned = opt_x.cloned();
assert_eq!(cloned, Some(12));

3、is_some():如果 Option 是 Some 值,返回 true。

let x: Option<u32> = Some(2);
assert_eq!(x.is_some(), true);
 
let x: Option<u32> = None;
assert_eq!(x.is_some(), false);

4、is_none():如果 Option 是 None 值,返回 true。

let x: Option<u32> = Some(2);
assert_eq!(x.is_none(), false);
 
let x: Option<u32> = None;
assert_eq!(x.is_none(), true);

5、as_ref():

  • 把 Option<T> 或 &Option<T> 转换成 Option<&T>。
  • 创建一个新 Option,里面的类型是原来类型的引用,就是从 Option<T> 到 Option<&T>。
  • 原来那个 Option<T> 实例保持不变。
let text: Option<String> = Some("Hello, world!".to_string());
let text_length: Option<usize> = text.as_ref().map(|s| s.len());
println!("still can print text: {text:?}");

6、as_mut():把 Option<T> 或 &mut Option<T> 转换成 Option<&mut T>。

let mut x = Some(2);
match x.as_mut() {
Some(v) => *v = 42,
None => {},
}
assert_eq!(x, Some(42));

7、take():把 Option 的值拿出去,在原地留下一个 None 值。非常有用。把值拿出来用,没有消解原来那个 Option。

let mut x = Some(2);
let y = x.take();
assert_eq!(x, None);
assert_eq!(y, Some(2));
 
let mut x: Option<u32> = None;
let y = x.take();
assert_eq!(x, None);
assert_eq!(y, None);

8、replace():在原地替换新值,同时把原来那个值抛出来。

let mut x = Some(2);
let old = x.replace(5);
assert_eq!(x, Some(5));
assert_eq!(old, Some(2));
 
let mut x = None;
let old = x.replace(3);
assert_eq!(x, Some(3));
assert_eq!(old, None);

9、and_then():

  • Option 是 None,返回 None;
  • Option 是 Some,把参数里面提供的函数或闭包应用到被包裹的内容上,返回运算后的结果。
fn sq_then_to_string(x: u32) -> Option<String> {
x.checked_mul(x).map(|sq| sq.to_string())
}
 
assert_eq!(Some(2).and_then(sq_then_to_string), Some(4.to_string()));
assert_eq!(Some(1_000_000).and_then(sq_then_to_string), None); // overflowed!
assert_eq!(None.and_then(sq_then_to_string), None);

Result<T, E> 上的常用方法和示例。

1、map():

  • Result 是 Ok ,把 Ok 里的类型通过参数里提供的函数运算并且可以转换成另外一种类型。
  • Result 是 Err ,原样返回 Err 和它携带的内容。
let line = "1\n2\n3\n4\n";
 
for num in line.lines() {
    match num.parse::<i32>().map(|i| i * 2) {
        Ok(n) => println!("{n}"),
        Err(..) => {}
    }
}

2、is_ok():如果 Result 是 Ok,返回 true。

let x: Result<i32, &str> = Ok(-3);
assert_eq!(x.is_ok(), true);
 
let x: Result<i32, &str> = Err("Some error message");
assert_eq!(x.is_ok(), false);

3、is_err():如果 Result 是 Err,返回 true。

let x: Result<i32, &str> = Ok(-3);
assert_eq!(x.is_err(), false);
 
let x: Result<i32, &str> = Err("Some error message");
assert_eq!(x.is_err(), true);

4、as_ref():

  • 创建一个新 Result,两种类型变为原类型的引用,从 Result<T, E> 到 Result<&T, &E>。
  • 原来的 Result<T, E> 实例保持不变。
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.as_ref(), Ok(&2));
 
let x: Result<u32, &str> = Err("Error");
assert_eq!(x.as_ref(), Err(&"Error"));

5、as_mut():

  • 创建一个新 Result,两种类型变为原类型的可变引用,从 Result<T, E> 到 Result<&mut T, &mut E>。
  • 原来的 Result<T, E> 实例保持不变。
fn mutate(r: &mut Result<i32, i32>) {
    match r.as_mut() {
        Ok(v) => *v = 42,
        Err(e) => *e = 0,
    }
}

let mut x: Result<i32, i32> = Ok(2);
mutate(&mut x);
assert_eq!(x.unwrap(), 42);

let mut x: Result<i32, i32> = Err(13);
mutate(&mut x);
assert_eq!(x.unwrap_err(), 0);

6、and_then():

  • Result 是 Ok ,把方法提供的函数或闭包应用到 Ok 携带的内容上,并返回一个新的 Result。
  • Result 是 Err ,这个方法直接传递返回这个 Err 和它的负载。
  • 常用于一路链式操作,前提是过程里的每一步都要返回 Result。
fn sq_then_to_string(x: u32) -> Result<String, &'static str> {
    x.checked_mul(x).map(|sq| sq.to_string()).ok_or("overflowed")
}
 
assert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4.to_string()));
assert_eq!(Ok(1_000_000).and_then(sq_then_to_string), Err("overflowed"));
assert_eq!(Err("not a number").and_then(sq_then_to_string), Err("not a number"));

7、map_err():

  • Result 是 Ok ,传递原样返回。
  • Result 是 Err ,对 Err 携带的内容使用这个方法提供的函数或闭包进行运算及类型转换。
  • 常用于转换 Result 的 Err 的负载的类型,在错误处理流程中大量使用。
fn stringify(x: u32) -> String { 
    format!("error code: {x}") 
}

let x: Result<u32, u32> = Ok(2);
assert_eq!(x.map_err(stringify), Ok(2));

let x: Result<u32, u32> = Err(13);
assert_eq!(x.map_err(stringify), Err("error code: 13".to_string()));

Option<T> 与 Result<T, E> 的相互转换

Option<T> 与 Result<T, E> 可以互相转换。

注意,Result<T, E> 比 Option<T> 多一个类型参数,带的信息比 Option<T> 多一份,核心要点就是要注意信息的添加与抛弃

从Option<T> 到 Result<T, E>:ok_or()

  • Option<T> 实例是 Some,直接把内容重新包在 Result<T, E>::Ok() 里。
  • 是 None,使用 ok_or() 里提供的参数作为 Err 的内容。
let x = Some("foo");
assert_eq!(x.ok_or(0), Ok("foo"));

let x: Option<&str> = None;
assert_eq!(x.ok_or(0), Err(0));

从 Result<T, E> 到 Option<T>:ok()

  • Result<T, E> 是 Ok,就把内容重新包在 Some 里。
  • Result<T, E> 是 Err,直接换成 None,丢弃 Err 里的内容,同时原 Result<T, E> 实例被消费。
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

从 Result<T, E> 到 Option<T>:err()

  • Result<T, E> 是 Ok,直接换成 None,丢弃 Ok 里的内容。
  • Result<T, E> 是 Err,把内容重新包在 Some 里,同时原 Result<T, E> 实例被消费。
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

迭代器

迭代器,对一个集合类型进行遍历。如对 Vec<T>、对 HashMap<K, V> 等进行遍历。

用迭代器的好处:

  • 按需使用,不需要把目标集合一次性全部加载到内存,用一点加载一点。
  • 惰性计算,可用来表达无限区间,如 range 表达 1 到无限这个集合,有些语言中很难表达。
  • 安全地访问边界,不需要使用有访问风险的下标操作符。

next() 方法

迭代器上标准方法 next(),返回 Option<Item>,其中 Item 是组成迭代器的元素。迭代出下一个元素。集合被迭代完了,最后一次执行会返回 None。

在迭代器上调用 .next() 返回 u32 数字。

fn main() {
    let a: Vec<u32> = vec![1, 2, 3, 4, 5];
    let mut an_iter = a.into_iter();    // 将Vec<u32>转换为迭代器
    
    while let Some(i) = an_iter.next() {  // 调用 .next() 方法
        println!("{i}");
    }
}
// 输出
1
2
3
4
5

实际上,Rust 中不止 into_iter() 这一种将集合转换成迭代器的方法。

iter()、iter_mut()、into_iter()与三种迭代器

Rust 的迭代器根据所有权三态,分三种。

  1. 获取集合元素不可变引用的迭代器,对应方法为 iter()。
  2. 获取集合元素可变引用的迭代器,对应方法为 iter_mut()。
  3. 获取集合元素所有权的迭代器,对应方法为 into_iter()。

Rust 标准库约定,

  • 类型上实现了 iter() 方法,会返回获取集合元素不可变引用的迭代器;
  • 类型上实现了 iter_mut() 方法,会返回获取集合元素可变引用的迭代器;
  • 类型上实现了 into_iter() 方法,会返回获取集合元素所有权的迭代器。调用了这个迭代器后,迭代器的执行会消耗掉原集合。

三种迭代器对比。

fn main() {
    let mut a = [1, 2, 3];    // 一个整数数组

    let mut an_iter = a.iter();  // 转换成第一种迭代器
    
    assert_eq!(Some(&1), an_iter.next());
    assert_eq!(Some(&2), an_iter.next());
    assert_eq!(Some(&3), an_iter.next());
    assert_eq!(None, an_iter.next());

    let mut an_iter = a.iter_mut();  // 转换成第二种迭代器
    
    assert_eq!(Some(&mut 1), an_iter.next());
    assert_eq!(Some(&mut 2), an_iter.next());
    assert_eq!(Some(&mut 3), an_iter.next());
    assert_eq!(None, an_iter.next());

    let mut an_iter = a.into_iter();  // 转换成第三种迭代器,并消耗掉a
    
    assert_eq!(Some(1), an_iter.next());
    assert_eq!(Some(2), an_iter.next());
    assert_eq!(Some(3), an_iter.next());
    assert_eq!(None, an_iter.next());

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

与字符串数组进行对比加深理解。

fn main() {
    let mut a = ["1".to_string(), "2".to_string(), "3".to_string()];
    let mut an_iter = a.iter();
    
    assert_eq!(Some(&"1".to_string()), an_iter.next());
    assert_eq!(Some(&"2".to_string()), an_iter.next());
    assert_eq!(Some(&"3".to_string()), an_iter.next());
    assert_eq!(None, an_iter.next());

    let mut an_iter = a.iter_mut();
    
    assert_eq!(Some(&mut "1".to_string()), an_iter.next());
    assert_eq!(Some(&mut "2".to_string()), an_iter.next());
    assert_eq!(Some(&mut "3".to_string()), an_iter.next());
    assert_eq!(None, an_iter.next());

    let mut an_iter = a.into_iter();
    
    assert_eq!(Some("1".to_string()), an_iter.next());
    assert_eq!(Some("2".to_string()), an_iter.next());
    assert_eq!(Some("3".to_string()), an_iter.next());
    assert_eq!(None, an_iter.next());
    
    println!("{:?}", a);    // 请你试试这一行有没有问题?
}

对整数数组 [1,2,3] ,调用 into_iter() 实际会复制一份这个数组,再将复制后的数组转换成迭代器,并消耗掉这个复制后的数组,因此最后的打印语句能把原来那个 a 打印出来。

对字符串数组 ["1".to_string(), "2".to_string(), "3".to_string()] ,调用 into_iter() 会直接消耗掉这个字符串数组,因此最后的打印语句不能把原来那个 a 打印出来。

为什么会有这个差异呢?第 2 讲所有权相关知识。

for 语句的真面目

Rust 里 ,for 语句是一种语法糖。语句 for item in c {} 会展开成这样:

let mut tmp_iter = c.into_iter();
while let Some(item) = tmp_iter.next() {}

for 语句默认使用获取元素所有权的迭代器模式,自动调用了 into_iter() 方法。for 语句会消耗集合 c。要将一个类型放在 for 语句里进行迭代,需要这个类型实现了迭代器 into_iter() 方法。

标准库中常见的 Range、Vec、HashMap、BtreeMap 等都实现了 into_iter() 方法,可以放在 for 语句里进行迭代。

for 语句作为一种基础语法,它会消耗掉原集合。

有时候希望不获取原集合元素所有权,如只打印一下,只需要获取集合元素的引用 ,怎么办?

Rust 中也考虑到了这种需求,提供了配套的辅助语法。

  • 用 for  in &c {} 获取元素的不可变引用,相当于调用 c.iter()。
  • 用 for  in &mut c {} 获取元素的可变引用,相当于调用 c.iter_mut()。

用这两种形式就不会消耗原集合所有权

示例。

fn main() {
    let mut a = ["1".to_string(), "2".to_string(), "3".to_string()];
    
    for item in &a {  
        println!("{}", item);
    }
    
    for item in &mut a {
        println!("{}", item);
    }
    
    for item in a {    // 请想一想为什么要把这一句放在后面
        println!("{}", item);
    }
    
    // println!("{:?}", a);  // 你可以试试把这一句打开
}
// 输出
1
2
3
1
2
3
1
2
3

into_iter() 会消耗集合所有权,把它放在最后去展示。

获取集合类型中元素的所有权

想要获取 Vec 里的一个元素,只需要下标操作。

fn main() {
    let s1 = String::from("aaa");
    let s2 = String::from("bbb");
    let s3 = String::from("ccc");
    let s4 = String::from("ddd");
    
    let v = vec![s1, s2, s3, s4];
    let a = v[0];    // 这里,我们想访问 s1 的内容
}

这段代码稀松平常,在 Rust 中却没办法编译通过

error[E0507]: cannot move out of index of `Vec<String>`
  --> src/main.rs:11:13
   |
11 |     let a = v[0];  
   |             ^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
   |
help: consider borrowing here
   |
11 |     let a = &v[0]; 
   |             +

提示不能从 Vec<String> 中用下标操作符移出元素。改一下代码。

fn main() {
    let s1 = String::from("aaa");
    let s2 = String::from("bbb");
    let s3 = String::from("ccc");
    let s4 = String::from("ddd");
    
    let v = vec![s1, s2, s3, s4];
    let a = &v[0];  // 明确a只获得v中第一个元素的引用
}

明确 a 只获得 v 中第一个元素的引用,编译通过。思考,对于 Vec<u32> 这种类型的动态数组,let a = v[0]; 这种代码可以编译通过吗?

在上面示例中,你可能为了从集合中获得 s1 的所有权,而不得不使用 let a = v[0].clone()。用 into_iter() 就可以拿到并操作上述动态数组 v 中元素的所有权。

fn main() {
    let s1 = String::from("aaa");
    let s2 = String::from("bbb");
    let s3 = String::from("ccc");
    let s4 = String::from("ddd");
    
    let v = vec![s1, s2, s3, s4];
    for s in v {      // 这里,s拿到了集合元素的所有权
        println!("{}", s);
    }
}

这也体现了 Rust 对权限有相当细致的管理。对于下标索引这种不安全的操作,禁止获得集合元素所有权;对于迭代器这种安全的操作,允许它获得集合元素所有权

小结

Rust 中的 Option<T> 和 Result<T, E> 的定义及常见操作。如何以解包和不解包方式使用它们。

迭代器的概念。迭代器的 next() 方法会返回一个 Option<Item> 类型。在所有权三态理论的指导下 Rust 中的迭代器也划分成了三种,可以通过不同的方法获取到集合元素的不可变引用、可变引用和所有权,这是 Rust 和其他语言的迭代器很不一样的地方。每种迭代器都有各自适用的场景,正确使用迭代器能提高代码的可靠性和可阅读性。

更深刻地感受到所有权概念在 Rust 语言中的支配地位。

思考题

如何拿到 HashMap 中值的所有权。

https://doc.rust-lang.org/std/collections/struct.HashMap.html

答:

Vec<String> []索引不能move的原因思考: Vec实现了Index trait的index方法,该方法返回一个引用。然后使用[]语法糖的时候,编译器会自动解引用:v[0]变成*v.index(&0)。其实错误的本质是borrowed value不能move out。

答:

let arr = vec![1,3,3,4];
    let a = arr[0];
    println!("{}", a);
    println!("{:?}", arr);
对于Vec<u32>是可以用v[0]的,因为u32是可以Copy的
作者回复: 是的

答:

for (k, v) in myhash { //`myhash` moved due to this implicit call to `.into_iter()`
    // todo:
    // 这里会获得v的所有权,并且消耗掉myhash
}

println!("{:?}", myhash); //value borrowed here after move
作者回复: 👍👍

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值