写给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
}
这里还涉及两处其他的改动:
- 你可以通过观察或者运行来发现,为什么
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则不用加*
。
- 第二个问题比较难发现,需要给元素类型实现
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
增加了三个函数和方法。default
和from_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默认也分配了(不用显式写称为“生存期消除”),因为每个引用都必须有生存期。但是默认的生存期不一定合适,编译报错的原因就是生存期不兼容:默认返回值和参数是相同生存期,所以f
和vec1
生存期相同,一直到 f
被打印完。如果我们进行push
,会再一次引用,并且生存期相同,这样first
函数和push
方法的生存期就重叠了:first
也是持续到main
函数结束,push
也是。
针对这个例子,我们可以先print
再push
,这样他们的生存期不就不重叠了吗?你试一下看。
在集合求最小值的时候,前面也用到了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初学者的教程(二):所有权、生存期的更多相关文章
- 10篇写给Git初学者的最佳教程(转)
身为网页设计师或者网页开发者的你,可能已经听说过Git这个正快速成长的版本控制系统.它由GitHub维护:GitHub是一个开放性的.存储众人代码的网站.如果你想学习如何使用Git,请参考本文.在文章 ...
- CRL快速开发框架系列教程二(基于Lambda表达式查询)
本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...
- C#微信公众号开发系列教程二(新手接入指南)
http://www.cnblogs.com/zskbll/p/4093954.html 此系列前面已经更新了两篇博文了,都是微信开发的前期准备工作,现在切入正题,本篇讲解新手接入的步骤与方法,大神可 ...
- 无废话ExtJs 入门教程二十[数据交互:AJAX]
无废话ExtJs 入门教程二十[数据交互:AJAX] extjs技术交流,欢迎加群(521711109) 1.代码如下: 1 <!DOCTYPE html PUBLIC "-//W3C ...
- Laravel教程 二:路由,视图,控制器工作流程
Laravel教程 二:路由,视图,控制器工作流程 此文章为原创文章,未经同意,禁止转载. View Controller 上一篇教程我们走了那么长的路,终于把Laravel安装好了,这一篇教程我们就 ...
- Android高手进阶教程(二十八)之---Android ViewPager控件的使用(基于ViewPager的横向相册)!!!
分类: Android高手进阶 Android基础教程 2012-09-14 18:10 29759人阅读 评论(35) 收藏 举报 android相册layoutobjectclassloade ...
- mongodb入门教程二
title: mongodb入门教程二 date: 2016-04-07 10:33:02 tags: --- 上一篇文章说了mongodb最基本的东西,这边博文就在深入一点,说一下mongo的一些高 ...
- 【Visual C++】游戏开发五十六 浅墨DirectX教程二十三 打造游戏GUI界面(一)
本系列文章由zhmxy555(毛星云)编写,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/16384009 作者:毛星云 ...
- 黄聪:Microsoft Enterprise Library 5.0 系列教程(二) Cryptography Application Block (初级)
原文:黄聪:Microsoft Enterprise Library 5.0 系列教程(二) Cryptography Application Block (初级) 企业库加密应用程序模块提供了2种方 ...
- Swift中文教程(二)--简单值
原文:Swift中文教程(二)--简单值 Swift使用let关键字声明常量,var关键字声明变量.常量无需在编译时指定,但至少要被赋值一次.也就是说,赋值一次多次使用: var myVariable ...
随机推荐
- Git reset 的hard、soft、mixed参数对比
目录 分区概念 1. --soft参数 2. --mixed参数 3. --hard参数 分区概念 先要清楚在本地,git会分三个区:工作区.暂存区.本地库. 当使用去做版本移动的时候,那么在使用[- ...
- Docker服务搭建个人音乐播放器Koel(及马里奥游戏)
Koel简介 Koel是一种简单的基于Web的个人音频流服务,用客户端的Vue和服务器端的Laravel编写.针对Web开发人员,Koel采用了一些更现代的Web技术来完成其工作 搭建步骤 docke ...
- postgresql性能优化3:分区表
一.分区表产生的背景 随着使用时间的增加,数据库中的数据量也不断增加,因此数据库查询越来越慢. 加速数据库的方法很多,如添加特定的索引,将日志目录换到单独的磁盘分区,调整数据库引擎的参数等.这些方法都 ...
- PCF 的 Npcf_PolicyAuthorization 服务化接口
目录 文章目录 目录 引用 前文列表 术语 PCF Npcf_PolicyAuthorization 服务化操作类型 服务化接口参数类型 创建 Application Session Context: ...
- PageOffice在线打开office文件添加盖章没反应或者提示本地服务ZSCService 可能未启动(系统无法找到指定的资源。)
盖章无反应 1.在控制面板的程序功能里面卸载印章客户端,然后重新打开文件,根据提示安装印章客户端sealsetup.exe,重新盖章试试. (注意:安装卸载的时候,先关闭所有的浏览器和所有的offic ...
- 新手【BUUCTF】逆向writeup()
0x00前言 在大三开始入门逆向,已学完小甲鱼解密篇,刚开始看<加密与解密>,现在沉浸在快 乐的刷题学习中..... buuctf_reverse地址 0x01刚接触的逆向题 revers ...
- springcloud整合geteway网关服务
geteway网关 1. 什么是 API 网关(API Gateway)分布式服务架构.微服务架构与 API 网关在微服务架构里,服务的粒度被进一步细分,各个业务服务可以被独立的设计.开发.测试.部署 ...
- c++ lambda学习举例
#include <iostream> #include<vector> #include<algorithm> #include<cmath> #in ...
- Java中双括号初始化是个什么操作
最近在阅读Mybatis源码的时候,看到了一种原来很少见到的语法: public class RichType { ... private List richList = new ArrayList( ...
- 深入理解 Swoole 的底层加载原理
首发原文链接:深入理解 Swoole 的底层加载原理 PHP 扩展加载 我们从 php-src/sapi/cli/php_cli.c:1159 文件的入口函数 int main(int argc, c ...