这系列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不是引用也不是克隆,所以要求泛型类必须支持Copy
  • set, 用来以内存安全的方式更新内存的数据

测试可证明,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,可以调用其borrowborrow_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没有getset方法,而是get_mutreplace

好了,现在可以再休息一下了。

但是可能有人不想休息说:“哎,都走到这里了,原子类都说了,还不说并发吗?”好吧,本来这篇文章我不想写并发的,毕竟篇幅已经挺长了。不过既然大家这样说了,马上安排!

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 的切片是 &strVec<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,叫 ArcArc 会以原子操作方式更新引用计数。

当然了,如果你坚持不用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 为了防止我们不合时宜的在多线程环境传递对象,提供了两个标记特征:SendSync。也许你在我们刚使用spawn函数时看到过这个东西



Send标记一个对象可以在线程间安全的传递(传递后还是只有一个线程在用),Sync 标记一个对象的引用可以被多个线程安全的访问。所以一个类型如果实现了Sync,那它的借用引用就具备了Send特征,可以在多个线程间传递(反之也有同样的要求)。而一个类型实现了Send,则它的可变引用也就具备了Send(反之也有同样的要求)。一个类型自身是Send或者Sync要求它的成员字段也是这样的。

你可以看一下源码,Arc是实现了这个特征的



CellRefCell是实现了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初学者的教程(三):闭包、智能指针、并发工具的更多相关文章

  1. Rust中的Rc--引用计数智能指针

    大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值.然而,有些情况单个值可能会有多个所有者.例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有.结 ...

  2. Rust中的智能指针:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak<T>

    Rust中的智能指针是什么 智能指针(smart pointers)是一类数据结构,是拥有数据所有权和额外功能的指针.是指针的进一步发展 指针(pointer)是一个包含内存地址的变量的通用概念.这个 ...

  3. Rust入坑指南:智能指针

    在了解了Rust中的所有权.所有权借用.生命周期这些概念后,相信各位坑友对Rust已经有了比较深刻的认识了,今天又是一个连环坑,我们一起来把智能指针刨出来,一探究竟. 智能指针是Rust中一种特殊的数 ...

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

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

  5. Swift2.0语言教程之闭包

    Swift2.0语言教程之闭包 Swift2.0语言闭包 闭包是自包含的函数代码块,可以在代码中被传递和使用.Swift中的闭包与C和Objective-C中的代码块(blocks)以及其他一些编程语 ...

  6. 手把手教从零开始在GitHub上使用Hexo搭建博客教程(三)-使用Travis自动部署Hexo(1)

    前言 前面两篇文章介绍了在github上使用hexo搭建博客的基本环境和hexo相关参数设置等. 基于目前,博客基本上是可以完美运行了. 但是,有一点是不太好,就是源码同步问题,如果在不同的电脑上写文 ...

  7. 专为设计师而写的GitHub快速入门教程

    专为设计师而写的GitHub快速入门教程 来源: 伯乐在线 作者:Kevin Li     原文出处: Kevin Li 在互联网行业工作的想必都多多少少听说过GitHub的大名,除了是最大的开源项目 ...

  8. Android Studio系列教程三--快捷键

    Android Studio系列教程三--快捷键 2014 年 12 月 09 日 DevTools 本文为个人原创,欢迎转载,但请务必在明显位置注明出处!http://stormzhang.com/ ...

  9. Laravel教程 三:视图变量传递和Blade

    Laravel教程 三:视图变量传递和Blade 此文章为原创文章,未经同意,禁止转载. Blade 上一篇我们简单地说了Router,Views和Controllers的工作流程,这一次我就按照上一 ...

  10. Nginx教程(三) Nginx日志管理

    Nginx教程(三) Nginx日志管理 1 日志管理 1.1 Nginx日志描述 通过访问日志,你可以得到用户地域来源.跳转来源.使用终端.某个URL访问量等相关信息:通过错误日志,你可以得到系统某 ...

随机推荐

  1. 解决浏览器打不开github网站常用方法

    switchHost使用指南 https://blog.csdn.net/weixin_45022563/article/details/123922815 下载软件: https://github. ...

  2. Docker推送镜像到Dockerhub

    登录docker hub官网注册账号 https://hub.docker.com/signup 登录账户,创建一个仓库 "Create Repository"--> 输入命 ...

  3. ios系统的css兼容问题处理和iOS上网页滑动不流畅问题

    1.H5网页touch滑动的时候在苹果手机上出现不流畅的问题 -webkit-overflow-scrolling 用来控制元素在移动设备上是否使用滚动回弹效果. 解决办法:给所有网页添加如下样式 b ...

  4. 第十届山东省大学生程序设计竞赛题解(A、F、M、C)

    部分代码define了long long,请记得开long long A. Calandar 把年份.月份.单个的天数全都乘以对应的系数转化成单个的天数即可,注意最后的结果有可能是负数,要转化成正数. ...

  5. mybaits-plus实现自定义字典转换

    需求:字典实现类似mybatis-plus中@EnumValue的功能,假设枚举类中应用使用code,数据库存储对应的value 思路:Mybatis支持对Executor.StatementHand ...

  6. pageoffice6 实现提取数据区域为子文件(Word拆分)

    在实际的开发过程中,有时会遇到希望提取Word文档中部分内容保存为子文件的需求,PageOffice支持提取Word文档数据区域中的内容为一个Word文件流,在服务器端创建PageOffice的Wor ...

  7. d3d12龙书阅读----绘制几何体(上) 课后习题

    d3d12龙书阅读----绘制几何体(上) 课后习题 练习1 完成相应的顶点结构体的输入-布局对象 typedef struct D3D12_INPUT_ELEMENT_DESC { 一个特定字符串 ...

  8. 基于webapi的websocket聊天室(二)

    上一篇 - 基于webapi的websocket聊天室(一) 消息超传缓冲区的问题 在上一篇中我们定义了一个聊天室WebSocketChatRoom.但是每个游客只分配了400个字节的发言缓冲区,大概 ...

  9. 8.26考试总结(NOIP模拟48)[Lighthouse·Miner·Lyk Love painting·Revive]

    告诉我,神会流血吗?--神不会,但你会. 前言 我直接打娱乐赛 T1 Lighthouse 解题思路 子集反演(但是 fengwu 硬要说是二项式反演咱也没法...) 发现其实 \(m\) 的值非常的 ...

  10. 事件对象的属性 div点击移动事件

       // 事件对象的相关属性         // e.target   触发事件的标签对象         //            e.target支持所有标签对象的操作         // ...