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

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

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



“值在被移走后用掉了”!怎么会这样?

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操作使用得非常普遍。当然它好带来性能问题,我们后面再说。



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

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

你可能注意到了,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。我们常用的整数、小数、字符、布尔等都实现了它

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会禁止编译通过:



存在两处可变引用,而可变引用是唯一引用。这可咋整呀,难道除了克隆没其他办法了吗?

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,这样他们的生存期不就不重叠了吗?你试一下看。


在集合求最小值的时候,前面也用到了clone

现在我们去掉这个克隆,看使用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会告诉你这两个实现有相同的问题:生存期不匹配



因为默认情况下,两个引用类型参数的生存期并不相同。我们使用克隆的时候,用的是克隆后的新数据,没有生存期的问题。不克隆的时候,使用引用类型就会有这个问题。

所以我们强制两个参数使用同一个生存期变量:

    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,所以我们之间判断它们是否相等就可以了。

再运行还报错:

因为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的代码总是大量出现&'_这样的写法,太丑了!

写给rust初学者的教程(二):所有权、生存期的更多相关文章

  1. 10篇写给Git初学者的最佳教程(转)

    身为网页设计师或者网页开发者的你,可能已经听说过Git这个正快速成长的版本控制系统.它由GitHub维护:GitHub是一个开放性的.存储众人代码的网站.如果你想学习如何使用Git,请参考本文.在文章 ...

  2. CRL快速开发框架系列教程二(基于Lambda表达式查询)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  3. C#微信公众号开发系列教程二(新手接入指南)

    http://www.cnblogs.com/zskbll/p/4093954.html 此系列前面已经更新了两篇博文了,都是微信开发的前期准备工作,现在切入正题,本篇讲解新手接入的步骤与方法,大神可 ...

  4. 无废话ExtJs 入门教程二十[数据交互:AJAX]

    无废话ExtJs 入门教程二十[数据交互:AJAX] extjs技术交流,欢迎加群(521711109) 1.代码如下: 1 <!DOCTYPE html PUBLIC "-//W3C ...

  5. Laravel教程 二:路由,视图,控制器工作流程

    Laravel教程 二:路由,视图,控制器工作流程 此文章为原创文章,未经同意,禁止转载. View Controller 上一篇教程我们走了那么长的路,终于把Laravel安装好了,这一篇教程我们就 ...

  6. Android高手进阶教程(二十八)之---Android ViewPager控件的使用(基于ViewPager的横向相册)!!!

      分类: Android高手进阶 Android基础教程 2012-09-14 18:10 29759人阅读 评论(35) 收藏 举报 android相册layoutobjectclassloade ...

  7. mongodb入门教程二

    title: mongodb入门教程二 date: 2016-04-07 10:33:02 tags: --- 上一篇文章说了mongodb最基本的东西,这边博文就在深入一点,说一下mongo的一些高 ...

  8. 【Visual C++】游戏开发五十六 浅墨DirectX教程二十三 打造游戏GUI界面(一)

    本系列文章由zhmxy555(毛星云)编写,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/16384009 作者:毛星云 ...

  9. 黄聪:Microsoft Enterprise Library 5.0 系列教程(二) Cryptography Application Block (初级)

    原文:黄聪:Microsoft Enterprise Library 5.0 系列教程(二) Cryptography Application Block (初级) 企业库加密应用程序模块提供了2种方 ...

  10. Swift中文教程(二)--简单值

    原文:Swift中文教程(二)--简单值 Swift使用let关键字声明常量,var关键字声明变量.常量无需在编译时指定,但至少要被赋值一次.也就是说,赋值一次多次使用: var myVariable ...

随机推荐

  1. Git reset 的hard、soft、mixed参数对比

    目录 分区概念 1. --soft参数 2. --mixed参数 3. --hard参数 分区概念 先要清楚在本地,git会分三个区:工作区.暂存区.本地库. 当使用去做版本移动的时候,那么在使用[- ...

  2. Docker服务搭建个人音乐播放器Koel(及马里奥游戏)

    Koel简介 Koel是一种简单的基于Web的个人音频流服务,用客户端的Vue和服务器端的Laravel编写.针对Web开发人员,Koel采用了一些更现代的Web技术来完成其工作 搭建步骤 docke ...

  3. postgresql性能优化3:分区表

    一.分区表产生的背景 随着使用时间的增加,数据库中的数据量也不断增加,因此数据库查询越来越慢. 加速数据库的方法很多,如添加特定的索引,将日志目录换到单独的磁盘分区,调整数据库引擎的参数等.这些方法都 ...

  4. PCF 的 Npcf_PolicyAuthorization 服务化接口

    目录 文章目录 目录 引用 前文列表 术语 PCF Npcf_PolicyAuthorization 服务化操作类型 服务化接口参数类型 创建 Application Session Context: ...

  5. PageOffice在线打开office文件添加盖章没反应或者提示本地服务ZSCService 可能未启动(系统无法找到指定的资源。)

    盖章无反应 1.在控制面板的程序功能里面卸载印章客户端,然后重新打开文件,根据提示安装印章客户端sealsetup.exe,重新盖章试试. (注意:安装卸载的时候,先关闭所有的浏览器和所有的offic ...

  6. 新手【BUUCTF】逆向writeup()

    0x00前言 在大三开始入门逆向,已学完小甲鱼解密篇,刚开始看<加密与解密>,现在沉浸在快 乐的刷题学习中..... buuctf_reverse地址 0x01刚接触的逆向题 revers ...

  7. springcloud整合geteway网关服务

    geteway网关 1. 什么是 API 网关(API Gateway)分布式服务架构.微服务架构与 API 网关在微服务架构里,服务的粒度被进一步细分,各个业务服务可以被独立的设计.开发.测试.部署 ...

  8. c++ lambda学习举例

    #include <iostream> #include<vector> #include<algorithm> #include<cmath> #in ...

  9. Java中双括号初始化是个什么操作

    最近在阅读Mybatis源码的时候,看到了一种原来很少见到的语法: public class RichType { ... private List richList = new ArrayList( ...

  10. 深入理解 Swoole 的底层加载原理

    首发原文链接:深入理解 Swoole 的底层加载原理 PHP 扩展加载 我们从 php-src/sapi/cli/php_cli.c:1159 文件的入口函数 int main(int argc, c ...