写给rust初学者的教程(二):所有权、生存期

这系列RUST教程一共三篇。这是第二篇,介绍RUST语言的关键概念,主要是所有权和生存期等。

第一篇:写给rust初学者的教程(一):枚举、特征、实现、模式匹配

在写第一篇中的练习代码时,不知道你有没有尝试过连续两次执行vec_min函数。这种做法在大部分其他语言中都属于正常行为,但如果你对rust这样做了,立即就得到一个error,编译都通不过:
image
“值在被移走后用掉了”!怎么会这样?

rust的宗旨是内存安全。为了实现这个任务,rust指定了一条铁律:通过别名访问数据时不能修改数据。这就是大名鼎鼎的“所有权”!

实际上这就是内存安全问题的根源:可变性以及起别名。可以看一下这些文章:Aliasing is what makes mutable types risky (Java), Aliasing and “Mutation at a Distance” (Python)

ownership

为什么要这样呢?我们通过一段C++程序来看这个问题:

  void foo(std::vector<int> v) {
      int *first = &v[0];
      v.push_back(42);
      *first = 1337;
  }

传统程序里面,C++的内存需要我们程序员来管理。这里变量first指向了参数v的首地址,然后给这个数组插入一个新元素;如果插入的时候数组是满的,v就会指向一个新的更大的数组,这样first指向的内存实际是无效的了。这就是超名昭著的“悬垂指针”问题。
那rust怎么做的呢?
看一个小程序:

    fn handle_vector(v: Vec<i32>) {  }
    fn ownership_test() {
        let v = vec![1,2,3,4];
        handle_vector(v);
        println!("{}", v[0]);
    }

肉眼看这个程序好像非常正常,但是给cargo build瞅了一眼,它说最后一句有问题,编译不了! Son of a biscuit.
rust中,当你把一个变量传到其他函数中时,rust认为你主动出让了所有权,出让后你就再也不能访问这个变量了。在其他函数执行完成后,这个传进去的变量处的内存会被回收掉。如果允许你访问,就和上面说的“悬垂指针”一样的问题了。
那这和上面说的所有权规则有啥关系?所有权规则是:通过其他别名访问数据时不可修改。可是传给其他函数后(必然会起别名),数据可能被修改了,所以你就不能再访问了。

可是Java里面就是这样传进去的啊!也没出问题啊

rust也能实现Java这样的效果。——当然能,必须能;如果不能,我相信没人会使用rust了。
还是以我们的vec_min函数为例,来看一下rust中的引用“借用”。

&

目前的函数签名是这样的:

    fn vec_min<T: Minimum>(vec: Vec<T>) -> SomethingOrNothing<T> {}

假设你有一部iPad,你的朋友们都可以借用它来浏览网页,用完还给你,期间他的朋友可能也会借用一会;但是他们借走期间,你没法使用它了 —— 当然是这样,毕竟我们也遵循“泡利不相容原理”,噗。
rust也模仿的这种现实:传递参数时可以指明是在“borrowing”而非“moving”。之前我们写的代码都是对所有权进行了move,要改成borrow需要在参数类型前面增加一个&

fn vec_min<T: Minimum>(vec: &Vec<T>) -> SomethingOrNothing<T> {}

现在vec不再是集合类型了,而是&Vec类型。要想拿到引用对应的变量值,需要使用解引用符号*

    fn vec_min<T: Minimum>(vec: &Vec<T>) -> SomethingOrNothing<T> {
        let mut min = Nothing;

        for e in vec {
            min = Something(match min {
                Something(i) => { e.compare(i) }
                Nothing => { *e }
            })
        }

        min
    }   

这里还涉及两处其他的改动:

  1. 你可以通过观察或者运行来发现,为什么e是一个引用类型,但是只有Nothing的分支进行了解引用,Something分支却直接调用了它的compare方法。所以这里需要去修改comapre方法,将第一个参数self改成&self
    pub trait Minimum : Copy {
        fn compare(&self, s: Self) -> Self;
    }

    impl Minimum for i32 {
        fn compare(&self, s: Self) -> Self {
            if *self < s { *self } else { s }
        }
    }

相应的,实现的地方在返回的时候也要判断,返回self就使用*self,返回s则不用加*

  1. 第二个问题比较难发现,需要给元素类型实现Copy特征,上面代码中已经增加了。

现在你可以试一下了,调用两次vec_min并不会报错了。

&mut

借出去的所有权,数据可以被修改吗?
是可以的。
你可以尝试在vec_min中给参数中push或者remove。应该会报错,根据错误信息响应的调整代码即可。你会发现,参数类型前面也可以加mut,变成了这样:

fn vec_min<T: Minimum>(vec: &mut Vec<T>) -> SomethingOrNothing<T> {}

甚至这样写:

    for e in v.iter_mut() {
        *e += 1;
    }

但是由于是引用类型,并不能赋值。你可以在调用前多次vec = vec![2];vec = vec![3];,但是在vec_min里面却不能执行这样的语句。

借用引用和可变引用

上面说的实际是两种不同的引用类型。rust中严格区分了他们。跟编程中的加锁一样,借用引用(用&来开启)同一时刻是可以存在多个的,他们互相不影响,因为只有读操作 —— 就和读锁一样;而可变引用(用&mut开启)同一时刻只能存在一个,而且使用了可变引用就不能使用借用引用了,因为什么?“不可重复读”。所以可变引用就是排它锁,可以称为 唯一引用


再来一个例子。
这次我们构造一个对象,类似于Java中的BigInteger。这种对象能够表示非常大的数,内存有多大数就有多大(哈哈😄),因为里面是用数组的多个元素来存储的。

自定义类型的创建使用struct关键字。

struct

struct和C中的结构体几乎一样,就是用来定义一个新类型的:

    pub struct BigInteger {
        data: Vec<u8>,
    }

我给这个struct前置了pub,但是字段data没加,所以这个字段是私有的。然后通过getter和setter来访问它:

    impl BigInteger {
        pub fn get_data(&self) -> &Vec<u8> {
            &self.data
        }

        pub fn set_data(&mut self, data: Vec<u8>) {
            self.data = data;
        }
    }

getter中的参数是借用引用,因为只是读没有写;setter中的self参数是可变引用,因为要覆盖data字段。

这个对象的字段类型是Vec<u8>。u8是8位长度的无符号类型,前面使用的i32是32位的有符号类型。这里当然使用任何整数类型都可以,我们只是用数组来存储大整数的每一位,所以实际上4位就足够了。因为一位数最大是9,4位二进制可以表示16。而8位整数是rust提供的最短整数了,所以我选择了u8,你也可以用i8来提供存储负数的能力(但是可能会有问题,你需要更多的防护代码,来防止中间的数字是负数)。

u8能表示的最大数已经是256了,所以超过9的时候要记得判断

一个常见的经验做法就是从个位开始存储大数,因为你不能确定大数的位数,计算起来不对齐的话就十分困难。比如一个数1459,我们放在Vec中是先放9,然后依次是5,4,1。这样我们计算的时候就可以从开始取到个位数开始算。由此引出的另一个经验做法就是,数组中不能存前置0。虽然给一个数增加几个前置0都不改变大小,但是会影响我们的存储和计算判断。

    impl BigInteger {
        pub fn default() -> Self {
            BigInteger {data: vec![0]}
        }

        pub fn test_invariant(&self) -> bool {
            if self.data.len() <= 1 {
                true
            } else {
                self.data[self.data.len() - 1] != 0
            }
        }

        pub fn from_vec(mut v: Vec<u8>) -> Self {
            let mut p = v.pop();
            while p.is_some() && p.unwrap() == 0 {
                p = v.pop()
            }
            if p.is_none() {
                return BigInteger::default();
            }
            v.push(p.unwrap());
            let mut data = vec![];
            for mut b in v {
                data.push(b % 10);
            }
            BigInteger {data}
        }
    }

这里给BigInteger增加了三个函数和方法。defaultfrom_vec都是用来创建实例的,不存储其他数据的时候默认是0。from_vec只是把参数复制一遍存起来,但是会去掉末尾的0(对应大数开头的0):pop方法可以弹出并返回Vec的最后一个元素,并以Option返回。如果参数中有元素不是一位数,可以报错。这里我们只取其个位数(通过除以10取余数)。
test_invariant是个实例方法,用来验证大数是否以0开头。

下面写几个case测试一下:

    fn main() {
        let mut bi = BigInteger::default();
        bi.print();
        bi = BigInteger::from_vec(vec![23, 4]);
        bi.print();
        bi = BigInteger::from_vec(vec![3, 6, 0]);
        bi.print();
        let data = vec![0, 1 << 7, 0, 0];
        let p = data[3];
        bi = BigInteger::from_vec(data);
        // bi = BigInteger::from_vec(data);
        bi.print();
        // println!("3 ==> {}, {}", p, data.len());
    }

里面的print方法是这样的

    impl BigInteger {
        pub(crate) fn print(&self) {
            print!("stored len: {}, value: ", self.data.len());
            let mut  index = 0;
            while index < self.data.len() {
                index += 1;
                print!("{}", self.data[self.data.len() - index])
            }
            println!()
        }
    }

输出如下

stored len: 1, value: 0
stored len: 2, value: 43
stored len: 2, value: 63
stored len: 2, value: 80

第一个0没啥疑问。第二个是43,因为十位是4,个位取23的个位;第三个去掉末尾的0,所以是63。第四个里面,1 << 7表示左移7位,1变成了128,个位是8,所以是80。


现在类型定义好了,开始正题。
在case中我们看到,第四次定义的参数data传给了函数from_vec。而参数在函数中会被消费掉,发生了所有权转移,所以函数返回后变量data就没用了。你可以把上面代码中注释掉的任何一行放开,都会编译报错。
这可咋办呢?

其实很简单:我们把变量赋值一份传给函数就好了嘛。

clone

由于rust中不变性和所有权的限制,clone操作使用得非常普遍。当然它好带来性能问题,我们后面再说。

img
可以看到,不适用clone的话,第二次调用函数就会报错;使用了clone自然没问题。

一个对象要想能够被clone,需要实现Clone 特征。Vec已经实现了这个特征。
img

你可能注意到了,clone的receiver是一个借用引用,为啥我们没写&呢?不是应该是BigInteger::from_vec((&data).clone());吗?因为如果借用是发生在实例自身上,rust做了特殊处理,让我们可以不写&。

我们给BigInteger也实现这个特征:

    impl Clone for BigInteger {
        fn clone(&self) -> Self {
            BigInteger {data: self.data.clone()}
        }
    }

简单得不能再简单,复制数据塞进一个新实例中。

再练习一下,给之前的枚举 SomethingOrNothing 实现这个trait:

    impl<T: Clone> Clone for SomethingOrNothing<T> {
        fn clone(&self) -> Self {
            match *self {
                Something(ref v) => { Something(v.clone()) }
                Nothing => { Nothing }
            }
        }
    }

ref

我们给泛型类型T定义了边界,必须也能clone,否则在第一个分支中我们就不能执行v.clone()了。
前面说过,clone的reciever是一个借用引用。要想把Something中的v设置成借用引用,需要使用ref
你可以自己试一下改改代码看看效果。

derive

Clone 这个 trait 太常用了,导致rust给他定义了一种更方便的实现方法。我们可以在类型上面写上注解#[derive(Clone)],rust就自动实现了这个接口:

    #[derive(Clone)]
    pub struct BigInteger {
        data: Vec<u8>,
    }

你可以给 SomethingOrNothing 头上写一下看看效果。

attribute

rust 中的这种注解称为“属性”(attribute),能给代码添加元数据,影响编译器的行为或提供其他类型的额外信息。常见的比如有

  • #[derive(Trait)]: 就是刚才用的,用于自动实现一些常见的trait。
  • #[test]: 属性用于标记一个函数为测试函数。
  • #[inline]:内联优化的提示。
  • 还有许多用来压制编译器告警的属性,如#[allow(lint_name)]、#[warn(lint_name)]、#[deny(lint_name)]、#[forbid(lint_name)]、#[must_use]等。
  • 还有很多其他的属性,有些可能将来能用到,有些可能一辈子也用不上。等着大家去探索。

继续我们的大数类型。

我打算用它做vec_min函数的泛型类型,来求一系列大整数中的最小值。
因为vec_min的泛型类需要实现 trait Minimum,这样才能进行元素对比。我们先给它实现这个特征:

    impl Minimum for BigInteger {
        fn compare(&self, s: Self) -> Self {
            debug_assert!(self.test_invariant());
            debug_assert!(s.test_invariant());
            if self.data.len() < s.data.len() { self.clone() } else if self.data.len() > s.data.len() { s } else {
                let mut data1 = self.data.clone();
                let mut other = s.data.clone();
                let mut digit1 = data1.pop();
                while digit1.is_some() {
                    let this_digit = digit1.unwrap();
                    let other_digit = other.pop().unwrap();
                    if this_digit > other_digit { return s; } else if this_digit < other_digit { return self.clone(); } else {
                        digit1 = data1.pop()
                    }
                }
                s
            }
        }
    }

如果编译器说需要实现父特征Copy,就把Minimum的父特征删掉,现在有了克隆用不到它了。同时需要修改vec_min方法的Nothing分支变成 Nothing => { (*e).clone() }

首先对每个对比元素进行了一次 debug_assert。这是一个宏,接受一个 bool 类型的参数:参数是true才能继续,否则就 panic。所以我们用它判断每个参数都没有以0开头。

这个宏在打 release 包的时候会被忽略掉,所以只是用于代码调试的

然后对比两个大整数的位数,谁的位数长谁就大,因为有两点保证了这个做法:①非0开头,②非负。
当他们位数一样是,就可以从高位开始比较大小了。我使用 pop 方法拿到集合的最后一个元素,也就是整数的最高位。

最后来测试一下。

let bigs: Vec<BigInteger> = vec![BigInteger::new(3), BigInteger::from_vec(vec![5,1]), BigInteger::default(), BigInteger::new(8)];
vec_min(&bigs).print();

集合中有4个“大”数:3,15, 0, 8。拿到结果后打印出来。但是我们还没实现打印方法,所以给它加上:

        type BigIntOrNothing = SomethingOrNothing<BigInteger>;
        impl BigIntOrNothing {
            fn print(self) {
                match self {
                    Something(n) => { n.print(); }
                    Nothing => { println!("nothing") }
                }
            }
        }

你自己可以测试几次:每次去掉最小的大数,看剩余的对比结果。

why clone?

这个程序其实挺关键的。如果你不是只是浏览了一下这个程序的实现,而是自己去写了一遍,你可能会疑惑:为什么需要用到这么多的 clone ?无论是在 compare 方法里,还是在 vec_min 函数中。

问题的关键是,这些方法都接受的是引用类型的参数(参数类型都是&前缀的)。而我们处理或者返回的数据都是拥有所有权的。

你可以反复修改一下用到 clone 地方的代码,看一下编译器如何提示。

等一下!
为什么我们之前没这样啊?之前我们的 vec_min 还不能接受泛型类型,它的元素类型还是 i32。那时候天还蓝的,水还是绿的,拍电影是不用脱衣服的,小孩子的爸爸是谁也是明确的,求最小值还是不用克隆的。

Copy type

rust 中有一些类型,当你移动它的时候移动的并不是所有权,而是数据自身 —— 数据被复制了一份进行移动。前面用到的 i32 就是这种类型,还有 bool 也是。他们 move 的行为和 Vec 这些类型不一样,它们称为 “Copy 类型”。
Copy 是rust中的一个trait。我们常用的整数、小数、字符、布尔等都实现了它
img

Copy 是一个标记特征(marker trait),它自身没有任何方法,只是一个标记,给编译器用的。Java中也有类似的接口,比如 java.io.Serializable

实际上,把一个值传给一个函数后,rust执行的时候一定会做一次拷贝,不论参数是i32还是 Vec。只是 Vec 执行的是浅拷贝 (shallow copy),这点和C++很像。不同的是,rust 认为即使是浅拷贝也是破坏性的,所以传值后就不能再使用这个数据了。毕竟只是浅拷贝,它们可能还是共享了内存指针的。但如果参数是拷贝类型,rust就觉得没关系了,不会对原数据造成破坏了。
如果我们自己想要复制对象,请实现 Clone 并主动调用 clone 方法,这样就可以深拷贝对象,因为浅拷贝 rust已经替我们做了。

好了,你可以再休息一下,回味一下刚才的内容。请休息好,因为接下来我们将迎接更大的挑战!这个挑战可是“劝退级”的。


欢迎回来。
我们来看你可能也一直疑惑的地方:rust 对数据所有权进行了这么严格的限制,导致我们不得不频繁使用深拷贝,那性能能好吗?rust 不是以性能标榜自己的吗?
我们回头再来看上面那个悬垂指针的问题:通过深拷贝我们解决了这个问题,但我们不想通过这种方法解决。怎么办?

先回忆一下。看一个例子:

        fn first<T>(vec: &Vec<T>) -> Option<&T> {
            if vec.len() > 0 { return Some(&vec[0]); }
            None
        }

这个函数是拿到一个非空集合的第一个元素。你留意一下参数和返回值类型,可以看到都有引用符号。所以我们期望一个函数在接收数据、吐出数据的时候,如果没有修改发生,就都使用引用类型。你想一想,这样是不是很好?

我们试一下这个函数:

    let f: Option<&i32> = first(&vec![1]);
    println!("{}", match f {
        None => { "none".to_string()}
        Some(i) => { (*i).to_string() }
    })

是不是应该输出 1 ?
—— Hold on! 这里有个trick,你运行一下就知道,不能把未命名变量穿进去,因为没命名,函数结束变量会被清理的,后面就没法再用了。
这倒没关系,我们提出变量来:

    let vec1 = &vec![3,1,21];
    let f: Option<&i32> = first(vec1);
    println!("{}", match f {
        None => { "none".to_string()}
        Some(i) => { (*i).to_string() }
    })

果然打印了3

我们的需求是拿到集合的第一个元素,并且再给集合添加一个元素。前面就知道,如果我们在拿到元素后处理集合,会引起悬垂引用,rust会禁止编译通过:
img
存在两处可变引用,而可变引用是唯一引用。这可咋整呀,难道除了克隆没其他办法了吗?

lifetime

当你只是把iPad借给朋友还不够,你还需要问他说会借多久。到期后需要把iPad还给你,毕竟这是你的财产。
rust 中的引用也有这个属性,称为“生存期”(lifetime)。

等一下,我们之前看的资料都说是“生命周期”啊!
的确,我这里理正了一下。lifetime 这个单词,和rust想表达的意思,跟生命周期(life cycle)没有关系,它不表示一个时间的往复或循环,而是表示这个时间是多久。当然”多久“不是说几秒几分钟,而是说具有相同生存期的引用能够存活的时间一样

生存期也是一个变量,需要跟引用类型搭配,以一个单引号连接一个变量名表示。比如上面的first函数,使用生存期完整写出来为:

    fn first<'a, T>(vec: &'a Vec<T>) -> Option<&'a T>

'a 就是生存期变量,表示参数vec 可以被借用多久。返回值也有这个变量,所以返回值和参数拥有相同的生存期。
上面我们没有使用生存期,rust默认也分配了(不用显式写称为“生存期消除”),因为每个引用都必须有生存期。但是默认的生存期不一定合适,编译报错的原因就是生存期不兼容:默认返回值和参数是相同生存期,所以fvec1 生存期相同,一直到 f 被打印完。如果我们进行push,会再一次引用,并且生存期相同,这样first函数和push方法的生存期就重叠了:first 也是持续到main 函数结束,push也是。

针对这个例子,我们可以先printpush,这样他们的生存期不就不重叠了吗?你试一下看。


在集合求最小值的时候,前面也用到了cloneimg
现在我们去掉这个克隆,看使用lifetime如何实现。

    fn vec_min<T: Minimum>(vec: &Vec<T>) -> SomethingOrNothing<&T> {
        let mut min = Nothing;
        for e in vec {
            min = Something(match min {
                Something(i) => { e.compare(i) }
                Nothing => { e }
            })
        }

        min
    }

现在返回类型的泛型参数是引用类型了,所以Nothing分支的*可以去掉了;而Something分支的里面需要也是引用类型。所以修改Minimum特征的方法为:

pub trait Minimum : Clone {
    fn compare(&self, s: &Self) -> &Self;
}

参数和返回类型也都携带了&符号。
可是,如果你现在去相应的修改实现:

    impl Minimum for i32 {
        fn compare(&self, s:&Self) -> &Self {
            if *self < *s { self } else { s }
        }
    }

或者

    impl Minimum for BigInteger {
        fn compare<'a>(&'a self, s:&'a Self) -> &'a Self {
            debug_assert!(self.test_invariant());
            debug_assert!(s.test_invariant());
            if self.data.len() < s.data.len() { self } else if self.data.len() > s.data.len() { s } else {
                let i = self.data.len();
                let mut j = i - 1;
                while j >= 0 {
                    let this_digit = self.data[j];
                    let other_digit = s.data[j];
                    if this_digit > other_digit { return s; } else if this_digit < other_digit { return self; } else {
                        j = j - 1;
                    }
                }
                s
            }
        }
    }

从所有权角度看似乎没问题,但是rust会告诉你这两个实现有相同的问题:生存期不匹配
img
因为默认情况下,两个引用类型参数的生存期并不相同。我们使用克隆的时候,用的是克隆后的新数据,没有生存期的问题。不克隆的时候,使用引用类型就会有这个问题。
所以我们强制两个参数使用同一个生存期变量:

    pub trait Minimum {
        fn compare<'a>(&'a self, s:&'a Self) -> &'a Self;
    }

相应地,实现也加上生存期参数:

    impl Minimum for i32 {
        fn compare<'a>(&'a self, s:&'a Self) -> &'a Self {
            if *self < *s { self } else { s }
        }
    }

    impl Minimum for BigInteger {
        fn compare<'a>(&'a self, s:&'a Self) -> &'a Self {
            debug_assert!(self.test_invariant());
            debug_assert!(s.test_invariant());
            if self.data.len() < s.data.len() { self } else if self.data.len() > s.data.len() { s } else {
                let i = self.data.len();
                let mut j = i - 1;
                while j >= 0 {
                    let this_digit = self.data[j];
                    let other_digit = s.data[j];
                    if this_digit > other_digit { return s; } else if this_digit < other_digit { return self; } else {
                        j = j - 1;
                    }
                }
                s
            }
        }
    }

只是方法签名上增加了'a,实现的代码一点没变,就可以正常执行了!


继续给BigInteger增加能力。

add

既然是数,那就可以进行相加(我们没处理它的符号,所以就不相减了)。rust 中可以进行操作符重载,加法可以应用在任何类型上。要提供相加的能力,需要实现 Add trait:

    use std::ops::Add;
    impl Add for BigInteger {
        type Output = BigInteger;

        fn add(self, rhs: Self) -> Self::Output {
            let max_len = cmp::max(self.data.len(), rhs.data.len());
            let mut res_vec: Vec<u8> = Vec::with_capacity(max_len + 1);
            let mut carry_bit = 0_u8;
            for i in 0..max_len {
                let left = if i < self.data.len() { self.data[i] } else { 0 };
                let right = if i < rhs.data.len() { rhs.data[i] } else { 0 };
                let added = left + right + carry_bit;
                let bit = added % 10;
                carry_bit = if added >= 10 { 1 } else { 0 };
                res_vec.push(bit);
            }
            if carry_bit == 1 { res_vec.push(1) }
            BigInteger::from_vec(res_vec)
        }
    }

逻辑应该比较简单,就是从个位开始按位相加,超过10加进一位;为了防止集合扩容,一开始就把位数加了1。这里额外说两点:

  • 加法操作可以应用在两个不同类型的变量上,所以你看 add 方法需要指定参数的类型;并且,结果可以是第三个类型,所以需要指定结果的类型
  • 结果的类型是一个 Output,你写这段代码的时候能发现,这个类型是从特征里带过来的,函数签名里是固定的Self::Output。它就像Java中的抽象方法,需要实现类去指明。rust 没把它叫做“抽象类型”,而是称为“关联类型” (Associated Types)。

我们测试一下这段代码。

#[test]

我说“测试”是真的测试,不是用main函数去跑一下。
换个函数跑一下,头上增加一个属性#[test]就叫测试函数了。

    #[test]
    fn add_them() {
        let a = BigInteger::from_vec(vec![7, 3, 1]);
        let b = BigInteger::from_vec(vec![3, 9, 2, 8]);
        let integer = a + b;
        println!("{}", integer);
        println!("done")
    }

用IDE开发一般能直接运行这个测试函数并看到结果;如果是命令行就使用cargo test,但是cargo test只会告诉你测试的结果,不会看到执行的结果,所以看不到print出来的东西。你应该知道测试的结果应该使用断言,所以我们改一下:

    #[test]
    fn add_them() {
        let a = BigInteger::from_vec(vec![7, 3, 1]);
        let b = BigInteger::from_vec(vec![3, 9, 2, 8]);
        let integer = a + b;
        println!("{}", integer);
        println!("done");
        assert_eq!(integer, BigInteger::from_vec(vec![0, 3, 4, 8]));
        assert!(integer == BigInteger::from_vec(vec![0, 3, 4, 8]))
    }

一执行就报错,为啥呢?我们只给 BigInteger 增加了相加的能力,没增加相等的能力,不能用==比较。

eq

要允许类型进行双等号比较,需要实现另一个 trait,叫PartialEq

    impl PartialEq for BigInteger {
        fn eq(&self, other: &Self) -> bool {
            self.data == other.data
        }
    }

Vec 已经实现了这个trait,所以我们之间判断它们是否相等就可以了。
再运行还报错:img
因为assert_eq!这个宏是比较的两个字符串是否相等,所以需要告诉rust如何格式化对象。

这两个宏在打release的时候都会被忽略掉

fmt

rust 中有两个常用 trait 都提供了fmt方法:

  • Display,print!宏会用到这个实现((注意for-in中的..=)):
    use std::fmt::{Debug, Display, Formatter};
    impl Display for BigInteger {
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
            let mut num = "".to_string();
            for index in 1..=self.data.len() {
                num.push_str(&(self.data[self.data.len() - index]).to_string());
            }
            write!(f, "{}", num)
        }
    }
  • Debug,assert_eq!宏要用到这个实现:
    impl Debug for BigInteger {
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
            self.data.fmt(f)
        }
    }

现在可以去跑测试函数了。

有时候,我们期望程序的输出更友好,比如这样:

    let a = BigInteger::from_vec(vec![7, 3, 1]);
    let b = BigInteger::from_vec(vec![3, 9, 2, 8]);
    let integer = a + b;
    println!("{} + {} = {}", a, b, integer);

我们当然希望它打印1 + 2 = 3这样好看的结果,但实际上你可能已经想到了,rust会编译失败:因为+的时候把两个变量消费掉了。
好吧,再来看看怎么用生存期解决这个问题。

    impl<'a,'b> Add<&'a BigInteger> for &'b BigInteger {
        type Output = BigInteger;

        fn add(self, rhs: &'a BigInteger) -> Self::Output {
            let max_len = cmp::max(self.data.len(), rhs.data.len());
            let mut res_vec: Vec<u8> = Vec::with_capacity(max_len + 1);
            let mut carry_bit = 0_u8;
            for i in 0..max_len {
                let left = if i < self.data.len() { self.data[i] } else { 0 };
                let right = if i < rhs.data.len() { rhs.data[i] } else { 0 };
                let added = left + right + carry_bit;
                let bit = added % 10;
                carry_bit = if added >= 10 { 1 } else { 0 };
                res_vec.push(bit);
            }
            if carry_bit == 1 { res_vec.push(1) }
            BigInteger::from_vec(res_vec)
        }
    }

我们定义了两个生存期变量,给第一个加数赋予'b的生存期,第二个加数赋予'a的生存期。这样我们需要明确Add的泛型参数 —— Add是有泛型参数的,只是默认和第一个加数一样,之前我们没写。现在测试代码改成

    let a = &BigInteger::from_vec(vec![7, 3, 1]);
    let b = &BigInteger::from_vec(vec![3, 9, 2, 8]);
    let integer = a + b;
    println!("{} + {} = {}", a, b, integer);

可以正常输出了。

你可能说:不对吧,两个加数没有任何地位上的差异,他俩的生存期本来就是一样的,为啥要使用两个?你可真聪明,其实这里使用一个生存期变量就可以了。所以解决所有权转移的关键是使用引用而非使用生存期。但是为什么这里我们必须加上生存期呢?因为跟函数不同,特征方法的实现必须明确生存期,不可消除或省略。

可能你又疑惑了:为什么之前没有呢?你咋又不聪明了,因为生存期是给引用类型使用的,当你使用&的时候才会用到。所以这带来一个“劝退”问题:rust的代码总是大量出现&'_这样的写法,太丑了!

posted @ 2024-03-15 17:28  大卫小东(Sheldon)  阅读(21)  评论(0编辑  收藏  举报