写给rust初学者的教程(三):闭包、智能指针、并发工具
这系列RUST教程一共三篇。这是最后一篇,介绍RUST语言的进阶概念。主要有闭包、智能指针、并发工具。
closure
“闭包”这个词在不少地方都有,前端有,后端有,数据库里也有。不知道美国小朋友怎么看待这个单词,反正中国的大小朋友看到这俩汉字都很懵。
Java中也有类似的概念,直接就叫“ lambda 表达式”。rust 中的闭包和 Java 的 lambda 表达式就是一个东西,所以这里演示几个例子就好了。
Java 中的函数式编程定义了一组函数式接口,rust 也类似,有三个常用的:Fn
, FnOnce
, FnMut
,不是和参数个数或是否有返回值有关,而是分别对应的是借用引用类型、值类型、可变引用类型。你可以看一下它们的源码,只有self
参数的修饰符有差异。
使用上的区别更关键。比如用
FnOnce
,调用一次闭包后闭包对象就被销毁了。一般创建新线程需要用到这种特征。
给BigInteger
增加一个方法:
impl BigInteger {
pub fn act_fn<A: FnMut(u8)>(&self, mut a: A) {
for d in self { // 我给BigInteger实现了IntoIterator,所以可以直接for。你可以自己搜索一下如何实现尝试尝试。如果有困难,就给self.data循环也行,但顺序会是反的。
a(d)
}
}
}
这个方法接收一个A
类型的参数,A
需要是FnMut
的一个实现类。变量名是a
,实际上就是一个函数,所以下面我们直接给它加括号,跟javascript的闭包一样,把整数的每一位传给它。
传给它以后,它会干啥?这就是需要调用的时候指定了。
在main函数中写一个看一下:
// 定义一个BigInteger类型的变量big_int
big_int.act_fn(|d| { println!("{}", d);});//只有一个表达式时大括号可以省略
这里就是把每一位打印出来。如果要把位置也打出来,就定义一个索引变量:
let mut i = 0;
int.act_fn(|d| {
println!("{} : {}", i, d);
i = i + 1;
});
如果你第一次接触闭包或Lambda表达式,可能疑惑这个
|d|
到底是什么东西。这个要看方法的实现,把什么传给了闭包。上面我们写了个for
循环调用了a(d)
,所以这里的d就传给了闭包去用。他们的名字不需要一样。
下面看两个对集合(对应 Java 中的 Stream )操作的例子。
map, filter, for_each, collect
- 对集合每个元素进行操作并过滤
fn inc_vec(vec: &Vec<i32>, off: i32, threshold: i32) {
vec.iter().map(|d| d+off).filter(|d| *d>= threshold).for_each(|d| println!("{}", d));
}
这个例子最简单,我就不解释了。
- 对集合处理后再收集
fn inc_vec(vec: &Vec<i32>, off: i32, threshold: i32) -> Vec<i32> {
vec.iter().map(|d| d+off).filter(|d| *d>= threshold).collect::<Vec<i32>>()
}
这个也简单。
rust 也跟JS一样提供了方法获取索引和值,就是enumerate
。
enumerate
for (i, t) in vec.iter().enumerate() {
println!("索引是 {},值是 {}", i, t);
}
或者
vec.iter().enumerate().for_each(|i| println!("{} {}", i.0, i.1));
很多年前,闭包最诱人的地方是实现“回调”。当时Java实现不了,坐山头上哭了好几年(现在也没正经实现出来)。现在我们来看看回调。它的写法不太简单,我们一步一步修正。
先写一个类来存储回调的闭包,所以大概写成了这样:
pub struct ClosureStorage<F: FnMut(i32)> {
callbacks: Vec<F>,
}
上面定义了一个泛型参数F,并要求其是闭包,接受整数i32参数。里面定义了一个字段callbacks,是集合类型。这样我们可以往里面添加几个闭包。
这样写的问题是啥?把泛型定义在顶层的话,我们在使用的时候就会确定下来泛型类型是谁。也就是说,如果两次传入闭包的话(往callbacks
字段里放),rust会认为它们的类型不同,不都是F
。类似于这样
上面一句说已经推定了泛型类型是那一大串,下面说又传入了其他类型。因为是匿名类,每次的类型都不一样。
很容易,我们会想到这样改:
pub struct ClosureStorage {
callbacks: Vec<FnMut(i32)> // 编译不了
}
这样会直接报错,说使用trait类型必须加上dyn
关键字。rust和Java有些区别,新版的rust(应该是从1.57开始),任何使用trait定义参数类型的(而不是使用具体类型)都必须加上dyn。dyn
是单词“动态”dynamic的缩写,表示这是一个动态引用,因为它实际上是啥类型要运行时才知道。
pub struct ClosureStorage {
callbacks: Vec<dyn FnMut(i32)>
}
但是这样编译还是报错,类似于“doesn't have a size known at compile-time”。因为多方面的考虑,大多时候rust要求必须提前知道对象要占据多大的内存。但是目前这样并不能知道FnMut将来的实现会是多大的,那Vec
在创建的时候该申请多大空间呢?和生存期一样,内存大小也是对象的内禀、默认、强制的要求,多数情况下编译器能够推断出来。推断不出来就报错了。
那我们怎么把“现在还不确定,将来才能知道大小的”FnMut(i32)
的实现放进集合里呢?rust也提供了一种类似C++的“智能指针”机制,称为“盒”。
Box
放进Box的对象会被Box拿走所有权,所以他们生存期默认是一样的。Box是一个引用,它指向动态创建的对象空间,这样对象的大小就无关紧要了,因为我们加到集合里的是Box:
pub struct ClosureStorage {
callbacks: Vec<Box<dyn FnMut(i32)>>,
}
注意
Box
的泛型参数中依然需要使用dyn
然后实现添加和使用回调的方法:
impl ClosureStorage {
pub fn default() -> Self {
ClosureStorage { callbacks: vec![] }
}
pub fn register(&mut self, c: Box<dyn FnMut(i32)>) {
self.callbacks.push(c)
}
pub fn call(&mut self, i: i32) {
self.callbacks.iter_mut().for_each(|c| (*c)(i))
}
}
测试一下:
let mut cs = ClosureStorage::default();
cs.register(Box::new(|a| println!("第一个回调 {}", a)));
cs.call(100);
cs.register(Box::new(|a| println!("第2个回调 {}", a)));
cs.call(200);
cs.call(300);
这个比较简单,你先想一个这个输出是啥样的,然后自己跑一遍看看符合你的预期吗。
闭包有一个能力上面忘记说了,就是可以使用外部对象。看这段代码, 我在第8行定义了一个i
:
let mut cs = ClosureStorage::default();
cs.register(Box::new(|a| println!("第一个回调 {}", a)));
cs.call(100);
cs.register(Box::new(|a| println!("第2个回调 {}", a)));
cs.call(200);
{
let mut i = 0;
cs.register(Box::new(move |b| {
i = i + 1;
println!("第三个回调 {} {}", i, b)
}));
}
cs.call(300);
cs.call(400);
打印结果:
第一个回调 100
第一个回调 200
第2个回调 200
第一个回调 300
第2个回调 300
第三个回调 1 300
第一个回调 400
第2个回调 400
第三个回调 2 400
看第三个回调的第二段输出,这里打印了i
,300的时候打印了1,400的时候打印了2。神奇不?
不知道你有没有这个疑问:为啥要传一个Box进register
方法,而不是传一个闭包,在方法里面封装成Box?
当然你可能以为我是随便写的,只是没这样实现。两种应该都可以吧?
但是你真的这样写了,rust会提示你:
连IDEA也说还是用Box吧,看来方法的参数也需要是确定大小的类型。
可以使用泛型来实现这个思路,因为泛型类型是具体的:
pub fn register_generic<FG: FnMut(i32) + 'static>(&mut self, c: FG) {
self.callbacks.push(Box::new(c));
}
这里我们传入一个闭包c
,在方法体里面进行了Box封装。c
的类型FG
的边界是FnMut(i32) + 'static
。'static
是rust中一个保留的生存期变量,表示和整个程序相同。为什么要加上这个约束呢?因为闭包是延迟执行的,如果不延迟对象的生存期,等到执行的时候发现里面有引用的对象已经失效了不就是重大bug了吗。
再测试一下:
{
let mut i = 0;
cs.register(Box::new(move |b| {
i = i + 1;
println!("第三个回调 {} {}", i, b)
}));
cs.register_generic(move |b| {
i = i + 1;
println!("第si个回调 {} {}", i, b)
});
}
cs.call(300);
cs.call(400);
你猜一下现在的输出是什么?尤其是变量i
那里会打印什么?我估计大概率会出乎你意料。
move
不知道你注意到没有,我们传入闭包的时候有时需要写上move
。你可以删掉它看一下编译器的报错。原因和上面使用'static
的原因一样,正常来讲i
不会到了大括号外面还能用,因为它会被销毁。但是这样我们执行cs.call(300);
时就麻烦了。所以我们需要把i
move 到闭包里面,变成static的生存期。
上面的两个
register
方法我们该用哪个?一般的建议是,能不用泛型就不用泛型。跟Java不同的是(Java是假泛型,“运行时擦除”),rust和c++一样,对于每个泛型实现都会生成一份代码(单态化)。所以实际上我们上面的第三和第四个回调已经产生了两份register_generic
方法,而第一个和第二个回调是共享同一个方法的。
网上有人总结了它们泛型实现上的差别,我赶紧fork了一份,你可以看看:https://gist.github.com/davelet/94373606d86e108bb584359408e6bbc3
接下来你可能会问的问题是:“C++中的智能指针可不止会独享(unique_ptr),还有共享指针(shared_ptr)呢。rust的Box能共享吗?”
我们可以试一下,直接给回调类型实现Clone
特征看看:
impl Clone for ClosureStorage {
fn clone(&self) -> Self {
ClosureStorage { callbacks: self.callbacks.clone() }
}
}
为什么这样就是在共享闭包了呢?因为闭包是不能克隆的,它就没提供这个能力。所以当我们克隆callbacks
的时候,只是克隆了集合中的Box,指针指向的闭包还是独一份。如果Box允许我们这样做,那就说明Box可以提供共享的能力。
试了一下,真的不行!
会不会是闭包类型的关系,我们用了FnMut
,改成Fn
可以吗:
pub struct ClosureStorage {
callbacks: Vec<Box<dyn Fn(i32)>>,
}
显然还是不行。
实际上,rust 真的提供了共享型智能指针,叫“引用计数指针”。
Rc
引用计数(Refrence Counting, RC)指针就是为了解决我们上面提到的问题的。它是一个共享型 的智能指针,每复制一次就增加一个引用计数,释放一次就减少一个计数;当计数为0的时候,引用的对象就会被销毁。
既然是共享引用,那就不能变更了,所以只能跟Fn
搭配。我们把我们前面的代码所有用到Box的地方都改成Rc
试一下:
pub fn register(&mut self, c: Rc<dyn Fn(i32)>) {
self.callbacks.push(c)
}
pub fn register_generic<'a, FG: Fn(i32) + 'static>(&mut self, c: FG) {
self.callbacks.push(Rc::new(c));
}
pub fn call(&mut self, i: i32) {
self.callbacks.iter_mut().for_each(|c| c(i))
}
既然会共享(回调会被克隆),那我们之前写的外部计数变量就不能用了。你可以注释掉,然后测试一下:
{
let mut i = 0;
cs.register(Rc::new(move |b| {
// i = i + 1;
println!("第三个回调 {} {}", i, b)
}));
cs.register_generic(move |b| {
// i = i + 1;
println!("第si个回调 {} {}", i, b)
});
}
cs.call(300);
cs.clone().call(400); // 注意这里的clone
没问题,正常输出。然后可能有人大喊一声“虽然,但是”。好的明白,你想说你的需求就是要打印出回调次数,去掉外部变量可咋办啊?!
没关系,我敢猜你的想法就敢给方案。rust 也提供了类似原子类的东西,你听到这仨字立马就明白了吧。
Cell
rust 的“原子类”叫 Cell
,也是接受一个泛型参数,表示引用的什么类型的数据。我们把外部变量改一下:
{
let mut i: Cell<i32> = Cell::new(0);
cs.register(Rc::new(move |b| {
i.set(i.get() + 1);
println!("第三个回调 {} {}", i.get(), b)
}));
i = Cell::new(0);
cs.register_generic(move |b| {
i.set(i.get() + 1);
println!("第si个回调 {} {}", i.get(), b)
});
}
cs.call(300);
cs.clone().call(400);
Cell
提供两个方法:
get
, 用来获取其中的数据拷贝,注意是Copy不是引用也不是克隆,所以要求泛型类必须支持Copyset
, 用来以内存安全的方式更新内存的数据
测试可证明,cs.clone()
和 Cell
可以共同使用。
这不搞呢吗?前面斩钉截铁说“别名和修改势同水火”,还说什么“铁律”,这才几分钟就打破“铁律”了!
这里面涉及一个概念“内部可变性”(interior mutability)。简单说就是rust认为共享后修改数据如果影响了其他共享者是不可以的,但是如果能够判断出来共享后的修改依然安全,就允许修改。哈!
可能还有人说:天啦撸,Cell
还有这好处呢?那我直接把回调放到Cell
里不就好了,还用担心这担心那的吗?
太聪明了,我咋没想到。我们一起来试试。
又有人说:你不是刚说Cell
的泛型参数需要是Copy Type吗,闭包是可以Copy的吗?
对啊,闭包还真不能被拷贝。可是我跟大家一样不想放弃使用Cell
。好在rust提供了另一个实现内部可变性的类型。
RefCell
Cell
返回的是数据的拷贝,而RefCell
顾名思义返回的是数据的引用,它不需要数据是拷贝类型。
天啦撸,你这样说那我再也不用
Cell
了可,反正一招RefCell
吃漫天。呃,目前来说你可以这样理解,随着对rust越来越了解,你会明白他们的使用场景的。RefCell
更笨重,使用不当还会引发运行时才能发现的错误。
pub struct ClosureStorage {
callbacks: Vec<Rc<RefCell<dyn FnMut(i32)>>>,
}
回调集合被改版成上面这样了:又加了一层指针,尖括号都三层了。相应的,再去改改方法实现。这次我太懒了,只保留一个泛型注册方法:
pub fn register<'a, FG: FnMut(i32) + 'static>(&mut self, c: FG) {
self.callbacks.push(Rc::new(RefCell::new(c)));
//这里没啥说是的,就是又new了一层
}
pub fn call(&mut self, i: i32) {
self.callbacks.iter().map(|c| c.borrow_mut()).for_each(|mut c| (&mut *c)(i))
}
我们重点看call
方法。对于每个回调的封装RefCell
,可以调用其borrow
或borrow_mut
方法拿到其数据的借用引用或可变引用,然后将其以闭包形式调用。这里拿到的是可变引用(是另一种智能指针RefMut
类型的引用)。调用时我写的是(&mut *c)(i)
,但那些修饰都可以省略。你写成(&mut c)(i)
甚至c(i)
都是可以的。
看一下测试代码:
{
let mut i = 0;
cs.register(move |b| {
i = i + 1;
println!("第三个回调 {} {}", i, b)
});
cs.register(move |b| {
i = i + 1;
println!("第si个回调 {} {}", i, b)
});
}
cs.call(300);
cs.clone().call(400);
我们又开心得使用克隆了而且不用使用Cell
了。
RefCell
没有get
和set
方法,而是get_mut
和replace
。
好了,现在可以再休息一下了。
但是可能有人不想休息说:“哎,都走到这里了,原子类都说了,还不说并发吗?”好吧,本来这篇文章我不想写并发的,毕竟篇幅已经挺长了。不过既然大家这样说了,马上安排!
thread
可以通过spawn
函数创建一个新线程:
thread::spawn(|| {
print!("我在新线程")
});
thread是一个模块,spawn 是里面的一个静态函数。
上手线程的入门例子一般是生产者消费者,我们来写一下。
假设流水线上生产产品,生产好了会放到仓库。仓库大小是10,满了就不能再生产了。消费者每次拿走一个产品去处理,没的拿了就得等着。流水线下班的时候会给最后一件产品挂上打烊的牌子。消费者看到牌子就知道后面不会再生产了,他就也去休息了。
我们这里让生产者读取一个文件,每一行就是一个产品,直到读完;消费者按行消费,对所有行进行排序后结束。
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::sync::mpsc::{Receiver, sync_channel, SyncSender};
use std::thread;
fn produce(file: String, channel: SyncSender<String>) {
let file = File::open(file);
match file {
Ok(file) => {
let file = BufReader::new(file);
for line in file.lines() {
match line {
Ok(line) => {
let send_rs = channel.send(line.clone());
match send_rs {
Ok(ok) => {}
Err(err) => { println!("sent line err :{}", err) }
}
}
Err(err) => { println!("read line err :{}", err) }
}
}
}
Err(err) => { println!("read file err: {}", err) }
}
}
fn consume(channel: Receiver<String>) {
let mut vec: Vec<String> = channel.iter().collect();
vec.sort();
for (i, line) in vec.iter().enumerate() {
println!("{}: {}", i, line)
}
}
这里有两个函数,一个用来生产,一个用来消费。生产者需要告诉它读取哪个文件,同时把读取数据发出去。因为我们上面说仓库有大小,所以这里使用了同步队列。生产者需要用到一个SyncSender
,消费者用到Receiver
,这俩是同一个队列的两头。
从代码可见,生产者调用channel.send()
发到队列,消费者使用channel.iter()
消费消息。那消费者怎么知道生产者停止了呢?
生产者停止后,produce
函数就结束了,结束后它持有的所有权变量都会被销毁,包括channel
。这时候channel
会被标记为无效,消费者就停止了。
sync_channel
sync_channel
函数可以创建这个队列通道,并返回这两个变量。我们写一个测试方法看一下效果
#[test]
fn p_c() {
let file = r#"src/main.rs"#.to_string();
let (sender, receiver) = sync_channel(10);//容量是10
thread::spawn(|| produce(file, sender));
thread::spawn(|| consume(receiver));
thread::sleep(Duration::from_secs(5));// 主线程休眠几秒等待跑完
}
slice
你应该一直有个疑问:像上面这样想用字符串的时候,直接使用双引号包围为什么不够,还得to_string
一下?rust中的字符串到底是什么类型:有时候好像叫String
—— rust 确实内置了这个结构体类型;有时候又不得不加to_string
,比如双引号包围的内容:
let file = r#"src/main.rs"#.to_string();
前面在讲Box
时说过,rust 编译的时候需要明确知道对象占据的空间大小,一方面是性能一方面是安全。如果使用了unsized
类型,编译器会报错。Box
是一种方案,将指针放到了堆上。rust 还提供了一种胖指针(fat pointer)方案,就是指针上面携带上对象的大小,这个称为“切片”(slice)。类型是中括号包裹着泛型类型[T]
。
双引号包围的字面量是在编译时确定下来的,生存期和程序相同;使用时以字符串切片访问。String
是分配在堆上的,和其他对象一样,生存期结束后会被销毁。静态字面量转成字符串时会在堆上生成一份(就像上面刚刚),字符串转成切片也使用中括号:
let s = String::from("Hello, world!");
let slice = &s[..]; // 获取整个字符串的切片
let substr = &s[0..5]; // 获取从索引0到索引5(不包含5)的子串
String
的切片是&str
,Vec<T>
和固定大小数组[T; n]
的切片都是&mut [T]
。记住,切片都是指针。
集合排序时如果用分治算法,就需要反复生成切片。我们来写一个快排看看:
fn quick_sort<T: PartialOrd>(list: &mut [T]) {
if list.len() < 2 { return; } // 递归出口
let mut lpos = 1;
let mut rpos = list.len() - 1;
loop {
if lpos > rpos { break; }
if list[0] >= list[lpos] {
lpos += 1
} else if list[0] < list[lpos] {
list.swap(lpos, rpos);
rpos -= 1;
}
}
list.swap(0, lpos - 1);
let parts = list.split_at_mut(lpos);
quick_sort(&mut parts.0[..lpos - 1]);
quick_sort(parts.1);
}
快排是一个递归算法,每次把集合分成三段:一段只有一个元素,称为“pivot” 基准值,每一轮结束后基准值会放到它合适的位置;第二段和第三段都可能是空的,分别是比基准值都小和大的两部分(其中一段里的值可以和基准值相等,不然有相等元素咋办)。然后分别对左右两段再次递归快排。
由于使用了泛型,所以要求元素类型必须能使用大小于号比较,就需要具备
PartialOrd
特征。
逻辑我们就不看了,直接看最后三行,这里进行了切片。切片自身提供了一个方法split_at_mut
来生成两个可变切片
我们把元组两个元素分别递归,不过左侧需要先把最后一个元素去掉,因为它就是基准值,已经排好序了。
你可以写个测试debug跟踪一下看看,每次这个函数的参数是啥样的,分片后又是啥样的。
现在你可以用这个函数去给上面的消费者使用一下看看效果:
// vec.sort();
sort(&mut vec[..]);
sync_channel
是我们用到的第一个线程同步工具,和其他语言一样,rust也提供了很多其他同步工具用于应对各种需求场景。现在来看一下信号量。
Mutex
假设有两个员工在生产,他们的经理在监督。产品放在公共区域,每一时刻只能有一个人访问这块区域。
没错就是“临界区”。
我们写两个线程,让他们分别去更新一个整数,主线程定期去查看整数的快照。为了锁住临界区,这里使用 Mutex
。竞争的数据会放在 Mutex 里面,要访问数据只能通过 Mutex。类型就是 Mutex<usize>
。不过他不是天然能被并发访问的,需要用 Rc
包裹起来引用。
Arc
线程间需要共享Rc对象的话,由于是多线程,引用计数在更新的时候可能会混乱。rust 提供了线程安全版本的Rc,叫 Arc
。Arc
会以原子操作方式更新引用计数。
当然了,如果你坚持不用
Arc
就是要用Rc
也是……不行的,rust能发现你的错误并报错
为了给共用整数增加方法,我们封装一个类型。
以前我们总是用struct
搭配大括号,其实还可以搭配小括号。
tuple struct
因为小括号是元组,所以这种方式称为“元组结构体”。跟结构体的区别是不用声明字段名,只要写类型就行。使用的时候是按照字段顺序访问:
#[derive(Clone)]
pub struct AtomicIncrement(Arc<Mutex<usize>>);
impl AtomicIncrement {
pub fn new(value: usize) -> Self {
AtomicIncrement(Arc::new(Mutex::new(value)))
}
pub fn inc(&self, step: usize) {
let lock = self.0.lock(); // 使用`.0`拿到字段
match lock {
Ok(mut int) => { *int = *int + step }
Err(err) => { println!("{}", err) }
}
}
pub fn get(&self) -> usize {
*self.0.lock().unwrap()
// *self.0.lock().unwrap_or_else(|e| { e.into_inner() }) // 这是干啥?在 IDE 中看一下过程变量是什么类型
}
}
这里提供了两个方法,我相信你能理解其中逻辑。
测试一下:
#[test]
fn increment() {
let atomic = AtomicIncrement::new(0);
let a1 = atomic.clone();
thread::spawn(move || {
for _ in 0..10 {// 一个线程跑200毫秒,给整数增加20
thread::sleep(Duration::from_millis(20));
a1.inc(2);
}
});
let a2 = atomic.clone();
thread::spawn(move || {
for _ in 0..40 {// 另一个线程跑1200毫秒,给整数增加40
thread::sleep(Duration::from_millis(30));
a2.inc(1);
}
});
for i in 0..30 {// 主线程每50毫秒输出一次,最后应该稳定到60
thread::sleep(Duration::from_millis(50));
println!("{}: {}", i, atomic.get());
}
}
你自己给
AtomicIncrement
写一个cas
方法测试一下,如果你知道CAS的工作流程的话。
这个工作逻辑不知道你满意吗?为什么读写都用同一把锁,连rust自身的借用引用都不是这样的。
当然,rust提供了增强版本 —— 读写锁,来区分共享和排他。
RwLock
简单场景下,这个同步工具在用法上和Mutex
几乎完全一样。所以我不重新写一遍了,这次你来吧。
把Arc<Mutex<usize>>
改成Arc<RwLock<usize>>
就行。获取读锁使用read
方法,获取写锁使用write
方法。
现在有些人可能又会有天大的问题了:你都使用RwLock
代替Mutex
了,竟然不用原子类?你上面不是说rust有原子类吗?干啥不用,Cell
行不行,不行给你个RefCell
!
rust 为了防止我们不合时宜的在多线程环境传递对象,提供了两个标记特征:Send
和Sync
。也许你在我们刚使用spawn
函数时看到过这个东西
Send
标记一个对象可以在线程间安全的传递(传递后还是只有一个线程在用),Sync
标记一个对象的引用可以被多个线程安全的访问。所以一个类型如果实现了Sync
,那它的借用引用就具备了Send
特征,可以在多个线程间传递(反之也有同样的要求)。而一个类型实现了Send
,则它的可变引用也就具备了Send
(反之也有同样的要求)。一个类型自身是Send
或者Sync
要求它的成员字段也是这样的。
你可以看一下源码,Arc
是实现了这个特征的
而Cell
和RefCell
是实现了Send
但是没有Sync
的
回到我们的问题,我们要共享Arc<T>
,这是一个共享引用,按照上面说的限制,就要求T
是可Sync
的。所以不能使用RefCell
:
文章的最后强调一点:rust 中是没有原生的 null
或者 nil
这种关键字的。但是经常在我们逻辑中需要使用一个空对象,有哪些方法呢?
用的最多的当然是Option<T>
了,或者有些时候使用Result<T>
在语义上可能更好。但是如果不想使用封装对象,就需要使用原始指针了。这是rust的unsafe,所以能不用尽量不用。
好了,全文结束。
如果你之前没能自己给BigInteger
实现迭代能力,可以参考下面的代码:
impl BigInteger {
pub fn iter(&self) -> DigitIter {
DigitIter::default(self)
}
}
impl<'a, 'b> IntoIterator for &'a BigInteger {
type Item = u8;
type IntoIter = DigitIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub struct DigitIter<'a> {
int: &'a BigInteger,
size: usize,
}
impl<'a> DigitIter<'a> {
pub fn default(b: &'a BigInteger) -> Self {
DigitIter { int: b, size: b.data.len() }
}
}
impl<'a> Iterator for DigitIter<'a> {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
if self.size == 0 {
None
} else {
self.size = self.size - 1;
Some(self.int.data[self.size])
}
}
}
写给rust初学者的教程(三):闭包、智能指针、并发工具的更多相关文章
- Rust中的Rc--引用计数智能指针
大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值.然而,有些情况单个值可能会有多个所有者.例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有.结 ...
- Rust中的智能指针:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak<T>
Rust中的智能指针是什么 智能指针(smart pointers)是一类数据结构,是拥有数据所有权和额外功能的指针.是指针的进一步发展 指针(pointer)是一个包含内存地址的变量的通用概念.这个 ...
- Rust入坑指南:智能指针
在了解了Rust中的所有权.所有权借用.生命周期这些概念后,相信各位坑友对Rust已经有了比较深刻的认识了,今天又是一个连环坑,我们一起来把智能指针刨出来,一探究竟. 智能指针是Rust中一种特殊的数 ...
- 10篇写给Git初学者的最佳教程(转)
身为网页设计师或者网页开发者的你,可能已经听说过Git这个正快速成长的版本控制系统.它由GitHub维护:GitHub是一个开放性的.存储众人代码的网站.如果你想学习如何使用Git,请参考本文.在文章 ...
- Swift2.0语言教程之闭包
Swift2.0语言教程之闭包 Swift2.0语言闭包 闭包是自包含的函数代码块,可以在代码中被传递和使用.Swift中的闭包与C和Objective-C中的代码块(blocks)以及其他一些编程语 ...
- 手把手教从零开始在GitHub上使用Hexo搭建博客教程(三)-使用Travis自动部署Hexo(1)
前言 前面两篇文章介绍了在github上使用hexo搭建博客的基本环境和hexo相关参数设置等. 基于目前,博客基本上是可以完美运行了. 但是,有一点是不太好,就是源码同步问题,如果在不同的电脑上写文 ...
- 专为设计师而写的GitHub快速入门教程
专为设计师而写的GitHub快速入门教程 来源: 伯乐在线 作者:Kevin Li 原文出处: Kevin Li 在互联网行业工作的想必都多多少少听说过GitHub的大名,除了是最大的开源项目 ...
- Android Studio系列教程三--快捷键
Android Studio系列教程三--快捷键 2014 年 12 月 09 日 DevTools 本文为个人原创,欢迎转载,但请务必在明显位置注明出处!http://stormzhang.com/ ...
- Laravel教程 三:视图变量传递和Blade
Laravel教程 三:视图变量传递和Blade 此文章为原创文章,未经同意,禁止转载. Blade 上一篇我们简单地说了Router,Views和Controllers的工作流程,这一次我就按照上一 ...
- Nginx教程(三) Nginx日志管理
Nginx教程(三) Nginx日志管理 1 日志管理 1.1 Nginx日志描述 通过访问日志,你可以得到用户地域来源.跳转来源.使用终端.某个URL访问量等相关信息:通过错误日志,你可以得到系统某 ...
随机推荐
- 解决浏览器打不开github网站常用方法
switchHost使用指南 https://blog.csdn.net/weixin_45022563/article/details/123922815 下载软件: https://github. ...
- Docker推送镜像到Dockerhub
登录docker hub官网注册账号 https://hub.docker.com/signup 登录账户,创建一个仓库 "Create Repository"--> 输入命 ...
- ios系统的css兼容问题处理和iOS上网页滑动不流畅问题
1.H5网页touch滑动的时候在苹果手机上出现不流畅的问题 -webkit-overflow-scrolling 用来控制元素在移动设备上是否使用滚动回弹效果. 解决办法:给所有网页添加如下样式 b ...
- 第十届山东省大学生程序设计竞赛题解(A、F、M、C)
部分代码define了long long,请记得开long long A. Calandar 把年份.月份.单个的天数全都乘以对应的系数转化成单个的天数即可,注意最后的结果有可能是负数,要转化成正数. ...
- mybaits-plus实现自定义字典转换
需求:字典实现类似mybatis-plus中@EnumValue的功能,假设枚举类中应用使用code,数据库存储对应的value 思路:Mybatis支持对Executor.StatementHand ...
- pageoffice6 实现提取数据区域为子文件(Word拆分)
在实际的开发过程中,有时会遇到希望提取Word文档中部分内容保存为子文件的需求,PageOffice支持提取Word文档数据区域中的内容为一个Word文件流,在服务器端创建PageOffice的Wor ...
- d3d12龙书阅读----绘制几何体(上) 课后习题
d3d12龙书阅读----绘制几何体(上) 课后习题 练习1 完成相应的顶点结构体的输入-布局对象 typedef struct D3D12_INPUT_ELEMENT_DESC { 一个特定字符串 ...
- 基于webapi的websocket聊天室(二)
上一篇 - 基于webapi的websocket聊天室(一) 消息超传缓冲区的问题 在上一篇中我们定义了一个聊天室WebSocketChatRoom.但是每个游客只分配了400个字节的发言缓冲区,大概 ...
- 8.26考试总结(NOIP模拟48)[Lighthouse·Miner·Lyk Love painting·Revive]
告诉我,神会流血吗?--神不会,但你会. 前言 我直接打娱乐赛 T1 Lighthouse 解题思路 子集反演(但是 fengwu 硬要说是二项式反演咱也没法...) 发现其实 \(m\) 的值非常的 ...
- 事件对象的属性 div点击移动事件
// 事件对象的相关属性 // e.target 触发事件的标签对象 // e.target支持所有标签对象的操作 // ...