0


Rust——关于Option详解

前言:

Option是组成Rust程序的基石,熟练使用Rust的Option可以帮助我们进行程序的开发。但是Option这里的知识和细节比较绕,说白了就是各种套娃,本篇文章意在梳理Option的一些细节。

关于Option的基本构成,这里不讲了,想必读者应当都会。

首先,提供Rust标准库的官方文档供读者查阅。

Option in std::option - Rust (rustwiki.org)

区分Option中的T为&的情况

fn work_1() {
    let foo1 = Foo;
    let foo2  = Foo;
    let val_some = Some(foo1);
    let ref_some = Some(&foo2);
}

Option<T>,对于val_some的类型,T为Foo,对于ref_some的类型T为Foo&。也就是说

val_some: Option<Foo>

ref_some: OPtion<&Foo>

对于后续的文章,我会将其两者分开说明。

Some包装遵守赋值操作符的规则

让我们回想一个规则。Rust中的某一个类型如果没有实现Copy trait,那么其赋值操作是所有权的转移,如果实现了,就是复制。如果对一个变量进行包装,同样遵循这个道理。

#[allow(unused)]
fn work_1() {
    let foo = Foo;
    let some = Some(foo);
    let ref_foo = &foo; //error

    let a = 10;
    let some = Some(a);
    let ref_a = &a;
}
jan@jan:~/code/rust/option_$ cargo run
   Compiling option_ v0.1.0 (/home/jan/code/rust/option_)
error[E0382]: borrow of moved value: `foo`
  --> src/main.rs:11:19
   |
9  |     let foo = Foo;
   |         --- move occurs because `foo` has type `Foo`, which does not implement the `Copy` trait
10 |     let some = Some(foo);
   |                     --- value moved here
11 |     let ref_foo = &foo; //error
   |                   ^^^^ value borrowed here after move

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

所有的内置类型都实现了Copy trait,所以上面的三行不能通过编译,下面的三行可以。

区别&mut; mut &; mut & mut

这里的问题就像C++中的const *, * const, const * const一样。你完全可以类比。

#[allow(unused)]
fn work_2() {
    let mut a = 10;
    {
        let b = 20;
        let ref_a = &a; // -> &i32
        ref_a = &b;            //将引用指向b不可以
        let mut mut_ref_a = &a; //mut &i32
        *mut_ref_a = 20;       //更改变量本身,不可以
        mut_ref_a = &b;        //将引用指向b,可以
    }
    let ref_mut_a = &mut a; // -> &mut i32
    *ref_mut_a = 20;  //更改变量本身的值,可以
}
  • & mut代表着对一个变量的可变引用,引用的变量是可变的,但是引用本身是不可变的,也就是说当我确定引用一个变量的时候,就不能再引用其他变量了。
  • mut &代表着引用本身是可变的,即这个引用既可以引用a,又可以引用b,但是引用的变量是不可变的。
  • & mut & 即代表着上述两者的结合,引用本身是可变的,并且引用的变量也是可变的。
jan@jan:~/code/rust/option_$ cargo run
   Compiling option_ v0.1.0 (/home/jan/code/rust/option_)
error[E0384]: cannot assign twice to immutable variable `ref_a`
  --> src/main.rs:24:9
   |
23 |         let ref_a = &a; // -> &i32
   |             -----
   |             |
   |             first assignment to `ref_a`
   |             help: consider making this binding mutable: `mut ref_a`
24 |         ref_a = &b;            //将引用指向b不可以
   |         ^^^^^^^^^^ cannot assign twice to immutable variable

error[E0594]: cannot assign to `*mut_ref_a`, which is behind a `&` reference
  --> src/main.rs:26:9
   |
25 |         let mut mut_ref_a = &a; //mut &i32
   |                             -- help: consider changing this to be a mutable reference: `&mut a`
26 |         *mut_ref_a = 20;       //更改变量本身,不可以
   |         ^^^^^^^^^^^^^^^ `mut_ref_a` is a `&` reference, so the data it refers to cannot be written

Some errors have detailed explanations: E0384, E0594.
For more information about an error, try `rustc --explain E0384`.
error: could not compile `option_` due to 2 previous errors

match和Option

match匹配Option是开发中经常使用的组合。

#[allow(unused)]
fn match_ref_some() {
    let some = Some(String::from("hello"));
    let ref_some = &some;
    match ref_some {
        Some(s) => println!("{}",s),
        None => println!("no string"),
    }

    match some {
        Some(s) => println!("{}",s),
        None => println!("no string"),
    }

    println!("{}",some.unwrap()); //error
}

对于引用来说,匹配出来的值依旧是引用,也就是&T,对于变量本身来说,匹配出来的值就是T本身 。

jan@jan:~/code/rust/some__$ cargo check
    Checking some__ v0.1.0 (/home/jan/code/rust/some__)
error[E0382]: use of partially moved value: `some`
   --> src/main.rs:188:19
    |
184 |         Some(s) => println!("{}",s),
    |              - value partially moved here
...
188 |     println!("{}",some.unwrap());
    |                   ^^^^ value used here after partial move
    |
    = note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
help: borrow this field in the pattern to avoid moving `some.0`
    |
184 |         Some(ref s) => println!("{}",s),
    |              +++

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

这里匹配的是一个&Option,所以s是一个&String,不会造成所有权的转移。

    match ref_some {
        Some(s) => println!("{}",s),
        None => println!("no string"),
    }

而这里匹配的是Option,所以s为String,会发生所有权的转移,就是原来的some:Option<String>变量中的值,被转移到了匿名Option<String>中,就是代码Some(s)中的s,编译给出了一个部分移动的警告,也就是所some本身并没有被移动,而是其中的值被移动了,但是Option枚举中的Some就那么一个值,所以看着像是整个some都移动了,实则不然。

    match some {
        Some(s) => println!("{}",s),
        None => println!("no string"),
    }

Option和迭代器

就连Option上也有迭代器,真是不可思议。和其他的迭代器一样,只不过因为Option中只能是Some或者None,当是Some的时候,第一次调用next返回Some中的值,其余情况,包括None,均返回None。

#[allow(unused)]
#[test]
fn work_4() {
    let some = Some(String::from("hello"));
    let mut i = some.into_iter();
    assert_eq!(i.next().as_deref(),Some("hello"));
    assert_eq!(i.next(),None);

    let none: Option<String> = None;
    assert_eq!(none.iter().next(),None);

}

as系列方法

as系类方法提供在不结构的请款下改变T的类型,这些as方法十分的方便,但是却有些不好掌握。

在了解as系列方法前:请先记住一个规则:所谓的as_XXX,均对于T来说,而不是Option<T>来as说,这样可能更好的理解。

as_ref和map

pub const fn as_ref(&self) -> Option<&T>
从 &Option<T> 转换为 Option<&T>。

就是将T变为 &T。

我想你一定有个疑问,什么情况下需要这样转变。答案是你想使用Option中存放的值,但是却又不想失去其所有权的情况下,也就是平常所说的按照引用的方式传参。

例如标Option的impl中有一个名为map的方法,就和迭代器的map功能是一样的,但注意,Option的此方法非缓式评估,或者说非惰性求值,因为完全没有必要,我们看其函数原型。

pub fn map<U, F>(self, f: F) -> Option<U>
where
    F: FnOnce(T) -> U, 

如果我们将Option<T> 传入,那么就原先的Some就会失去所有权,就像是这样。

#[allow(unused)]
#[test]
fn work_5() {
    let some = Some(String::from("hello"));
    let size = some.map(|s| s.len());
    println!("{}",some.unwrap());
}
error[E0382]: use of moved value: `some`
   --> src/main.rs:64:19
    |
62  |     let some = Some(String::from("hello"));
    |         ---- move occurs because `some` has type `Option<String>`, which does not implement the `Copy` trait
63  |     let size = some.map(|s| s.len());
    |                     ---------------- `some` moved due to this method call
64  |     println!("{}",some.unwrap());
    |                   ^^^^ value used here after move
    |
note: this function takes ownership of the receiver `self`, which moves `some`
   --> /home/jan/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:903:28
    |
903 |     pub const fn map<U, F>(self, f: F) -> Option<U>
    |                            ^^^^
help: consider calling `.as_ref()` to borrow the type's contents
    |
63  |     let size = some.as_ref().map(|s| s.len());
    |                     +++++++++

For more information about this error, try `rustc --explain E0382`.

但如果我们将Option<T>变为Option<&T>就是不一样的了,对于引用来说,仅仅就是一个指针而已,也无所谓移动不移动的了,就像是这样。

#[allow(unused)]
#[test]
fn work_5() {
    let some = Some(String::from("hello"));
    let size = some.map(|s| s.len());
    // println!("{}",some.unwrap()); //error

    let some = Some(String::from("hello"));
    let size = some.as_ref().map(|s| s.len());
    println!("{}",some.unwrap()); 
}

标准库的实现也是非常的简单。

    pub const fn as_ref(&self) -> Option<&T> {
        match *self {
            Some(ref x) => Some(x),
            None => None,
        }
    }

as_deref

pub fn as_deref(&self) -> Option<&<T as Deref>::Target>
从 Option<T> (或 &Option<T>) 转换为 Option<&T::Target>。

将原始 Option 保留在原位,创建一个带有对原始 Option 的引用的新 Option,并通过 Deref 强制执行其内容。

也就是说,as_deref相当于对T进行了一次解引用操作并加上引用。当然,T必须实现了Deref这个trait。

如果你对None调用这个方法,结果依旧是None。

#[allow(unused)]
#[test]
fn work_3() {
    let s = String::from("hello");
    let some = Some(s);
    assert_eq!(some.as_deref(),Some("hello"));
    println!("{:?}",some);

    let some: Option<i32> = None;
    // some.as_deref(); //error

    let some: Option<String> = None;
    assert_eq!(some.as_deref(),None);
}

将原始 Option 保留在原位,创建一个带有对原始 Option 的引用的新 Option,并通过 Deref 强制执行其内容。这句话是不是让你很不解,我们一点点分析。

所谓的保留在原位,即是不发生move语义,也就是我们上面所说的as_ref的情形。我们进入源码,可看见这样简短的实现方法。

    pub fn as_deref(&self) -> Option<&T::Target> {
        self.as_ref().map(|t| t.deref())
    }

所说的并通过 Deref 强制执行其内容,就是调用deref方法而已。总结的来说,就是获得Option<&T>然后再进行解引用(注意,deref返回值为&T::Target,所以返回值并没有什么好疑惑的)。

或者说更加新版的标准库是这样实现的

    pub const fn as_deref(&self) -> Option<&T::Target>
    where
        T: ~const Deref,
    {
        match self.as_ref() {
            Some(t) => Some(t.deref()),
            None => None,
        }
    }

这里t的类型为&T。

as_deref_mut

和as_deref很像,就对在返回类型的可变性进行了更改。

pub fn as_deref_mut(&mut self) -> Option<&mut <T as Deref>::Target>
从 Option<T> (或 &mut Option<T>) 转换为 Option<&mut T::Target>。

在这里保留原始的 Option,创建一个包含对内部类型的 Deref::Target 类型的可变引用的新的 Option。

实战演练

as系列方法能够帮助我们做什么呢, 难道仅仅是令人头疼的类型转换吗? 为了能够更好的理解,我们可以看一下这个题,合并链表。

  1. 合并两个有序链表 - 力扣(LeetCode)

这是一份实现代码——你可以在题解中找到这份答案,这份代码并不知作者写的。

// Definition for singly-linked list.
// #[derive(PartialEq, Eq, Clone, Debug)]
// pub struct ListNode {
//   pub val: i32,
//   pub next: Option<Box<ListNode>>
// }

// impl ListNode {
//   #[inline]
//   fn new(val: i32) -> Self {
//     ListNode {
//       next: None,
//       val
//     }
//   }
// }
impl Solution {
    pub fn merge_two_lists(list1: Option<Box<ListNode>>, list2: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
        //考虑使用转移所有权的方法,这样构造的新的链表效率会更高
        //因为要转移所有权,所以list应当更改mut属性
        let mut list1 = list1;
        let mut list2 = list2;
        //新的链表
        let mut ret = ListNode::new(0);
        //一个mut & mut ,当作指针
        let mut p = &mut ret;
        //我们不应当获得list的所有权,因为是在一个loop中
        while let (Some(n1), Some(n2)) = (list1.as_ref(),list2.as_ref()) {
            if n1.val < n2.val {
                //转移所有权
                p.next = list1;
                //p指向list1:就是指针向后移动,因为p值&mut,所以应当使用as_mut
                p = p.next.as_mut().unwrap();
                //list1领导list1剩余的尾部
                list1 = p.next.take();
        } else {
                //逻辑同上
                p.next = list2;
                p = p.next.as_mut().unwrap();
                list2 = p.next.take();
            }
            //这里这样写是因为隐式解引用规则,全部样貌应当是
            //p = p.next.as_mut().unwrap().as_mut();
            //或者是
            // &mut **p.next.as_mut().unwrap();
            // p = &mut **p.next.as_mut().unwrap();
            //首先next返回Option<T>,
            //使用as_mut方法,-> Option<&mut T>
            //再使用unwrap方法得到的应当是一个&mut Box<ListNode>
            //根据隐式解引用转换规则,可实现Box<T> -> &T
            p = p.next.as_mut().unwrap();
        }
        p.next = if list1.is_some() { list1 } else {list2 };
        ret.next
    }

常用方法

filter

一个过滤器

pub fn filter<P>(self, predicate: P) -> Option<T>
where
    P: FnOnce(&T) -> bool, 
如果选项为 None,则返回 None; 否则,使用包装的值调用 predicate 并返回:

predicate指的是一个一元谓词。可以这样使用。

#[allow(unused)]
fn is_even(x: &i32) -> bool {
    x % 2 == 0
}
#[allow(unused)]
#[test]
fn work_6() {
    let some = Some(3);
    assert_eq!(some.filter(|x| is_even(x)),None);

    let some = Some(4);
    assert_eq!(some.filter(|x| is_even(x)),Some(4));

    assert_eq!(None.filter(|x| is_even(x)),None);
}

or

pub fn or(self, optb: Option<T>) -> Option<T>
如果包含值,则返回选项,否则返回 optb。

传递给 or 的参数会被急切地评估; 如果要传递函数调用的结果,建议使用 or_else,它是延迟计算的。

Box和Option

todo

总结

关于Option的用法还有很多,不能一一列举,如果日后作者在开发过程中踩坑,还会来继续更新的。


本文转载自: https://blog.csdn.net/JAN6055/article/details/125774473
版权归原作者 __JAN__ 所有, 如有侵权,请联系我们删除。

“Rust——关于Option详解”的评论:

还没有评论