写给rust初学者的教程(三):闭包、智能指针、并发工具

这系列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。类似于这样
img
上面一句说已经推定了泛型类型是那一大串,下面说又传入了其他类型。因为是匿名类,每次的类型都不一样。
很容易,我们会想到这样改:

    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会提示你:
img
连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)>>,
    }

img
显然还是不行。

实际上,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]
img
双引号包围的字面量是在编译时确定下来的,生存期和程序相同;使用时以字符串切片访问。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来生成两个可变切片
img
我们把元组两个元素分别递归,不过左侧需要先把最后一个元素去掉,因为它就是基准值,已经排好序了。

你可以写个测试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函数时看到过这个东西
img
Send标记一个对象可以在线程间安全的传递(传递后还是只有一个线程在用),Sync 标记一个对象的引用可以被多个线程安全的访问。所以一个类型如果实现了Sync,那它的借用引用就具备了Send特征,可以在多个线程间传递(反之也有同样的要求)。而一个类型实现了Send,则它的可变引用也就具备了Send(反之也有同样的要求)。一个类型自身是Send或者Sync要求它的成员字段也是这样的。
你可以看一下源码,Arc是实现了这个特征的
img
CellRefCell是实现了Send但是没有Sync
img
img
回到我们的问题,我们要共享Arc<T>,这是一个共享引用,按照上面说的限制,就要求T是可Sync的。所以不能使用RefCell:
img


文章的最后强调一点: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])
            }
        }
    }
posted @ 2024-03-28 16:40  大卫小东(Sheldon)  阅读(39)  评论(0编辑  收藏  举报