rust静态方法 用实例学习Rust(1)

上文通过共享所有权解决了变量移动语义的问题,但在最后我们也提到了共享所有权是无法修改原始数据的。但有的时候我们又确实需要修改原始数据怎么办?Rust 同样为这种场景提供了解决方案。当然这个解决方案也是要付出代价的。学习过 Rust 的人知道,如果出现了变量的可变引用,那么它就只能是唯一的存在,也就是说,不能再有另一个可变引用,也不能再有不可变引用了。下面我们来看看是如何做到的。

这里依然是创建库项目,为了使用性能测试宏,需要切换 Rustnightly。使用如下命令:rustup default nightly。然后加入如下代码到文件的顶端:#![feature(test)]

下面我们来创建几个函数:

fn using_refcell(min: i32, v: &RefCell<Vec<i32>>) { let sum: i32 = v.borrow().iter().sum(); if sum < min { v.borrow_mut().push(min - sum); } } fn using_cell(min: i32, v: &Cell<Vec<i32>>) { let mut vec = v.take(); let sum: i32 = vec.iter().sum(); if sum < min { vec.push(min - sum); } v.set(vec); }

这两个函数都完成了一个功能,就是传入一个值和一个数组,如果数组之和小于这个值,就把它们的差放进数组里。也就是说我们在函数中修改了外面数组的值。这两个函数分别使用了两种数据类型,RefCell 和 Cell。下面通过单元测试来看看 RefCellCell 是如何运作的。

#[test] fn testing_refcell_cell() { let ref_cell = RefCell::new(vec![10, 20, 30]); using_refcell(70, &ref_cell); assert!(ref_cell.borrow().eq(&vec![10, 20, 30, 10])); let cell = Cell::from(vec![10, 20, 30]); using_cell(70, &cell); let v = cell.into_inner(); assert_eq!(v, vec![10, 20, 30, 10]); }

首先看看 using_refcell 函数,我们知道传数组参数时传的是引用 &,我们又知道传引用是无法修改原始值的,但我们的数组值是被 RefCell 包裹的,传的引用是RefCell的引用,RefCell 有两个函数 borrowborrow_mut。顾名思义,borrow 函数可以获取 RefCell 包裹中的值的不可变引用,borrow_mut 函数可以获取 RefCell 包裹中的值的可变引用。在求和的时候我们不需要修改原始数组,因为使用的是 borrow 函数;最后在把新值 push 进数组中时使用的就是 borrow_mut 函数了,非常容易理解。在函数外面,我们比较的是引用。因为我们从 RefCell 只能得到引用,就像它的名字所表示的一样。

再来看看 using_cell 函数。数组是被 Cell 数据类型包裹起来的。在函数中,我们需要使用 Celltake 函数把包裹的值取出来,然后使用 mut 修饰后才可以改变数组中的值。在函数的外面,通过使用 Cellinto_inner 函数,我们可以得到 Cell 包裹中的值,而不是引用,这就是它和 RefCell 的区别所在。

然而,RefCell 的使用是有隐患的,会引起 panic,为什么呢?先来看代码:

#[test] #[should_panic] fn failing_cells() { let ref_cell = RefCell::new(vec![10, 20, 30]); let _v = ref_cell.borrow(); refcell_test(60, &ref_cell); refcell_test(70, &ref_cell); }

我们知道不可变引用是可以有无数多次的。因此在把数组传入函数之前,我们先以不可变引用借用给了 _v 变量。然后我们把数组传递给了函数,在函数内部,首先再一次通过不可变引用借用给了变量 sum,这没有任何问题,完全合法。因为 sum 的值和 min 的值相等,所以函数执行完毕,回到主函数。紧接着,我们又一次调用了函数,但这次的 min 值是 70,大于 sum 值,就会进入 if 语句,以可变引用的方式借用了原始数组,这时,panic 发生了。为什么?之前我们已经说过,以可变引用的方式借用,是唯一的存在,在它借用之前,或者借用以后没归还之前,是不能有任何可变或者不可变引用方式的借用的。由于我们在调用函数之前已经借用给了变量 _v,按照规则,就不能再以可变引用的方式借用了,就会发生 panic。但编译器并没有给我们提示,也就是说,RefCell 会绕过 borrow checker,借用检查器。请大家一定要谨记这一点,编程方便了,不稳定性也就增加了。

就像 Rc 和 Arc 一样,这样在原始数据类型外面包裹一层肯定是有性能损耗的,下面我们来看看性能测试:

extern crate test; use test::Bencher; #[bench] fn regular_push(b: &mut Bencher) { let mut v = vec![]; b.iter(|| { for _ in 0..1_000 { v.push(10); } }); } #[bench] fn refcell_push(b: &mut Bencher) { let v = RefCell::new(vec![]); b.iter(|| { for _ in 0..1_000 { v.borrow_mut().push(10); } }); } #[bench] fn cell_push(b: &mut Bencher) { let v = Cell::new(vec![]); b.iter(|| { for _ in 0..1_000 { let mut vec = v.take(); vec.push(10); v.set(vec); } }); }

这里我们测试了标准数组,RefCellCellpush 操作,在笔者的电脑上结果分别是:12147 ns/iter,16370 ns/iter, 25492 ns/iter。我们看到 RefCell 和原始性能相差无几,而 Cell 则 2 倍于原始性能。这也符合我们的预期,因为 Cell 每次迭代都要移动整个数组。

如有任何问题,请添加微信公众号“读一读我”

,