• 三个方法解决error: use of moved value


    https://www.tuicool.com/articles/UfIvemA

    2015 年 6 月 4 日

    概述

    “error: use of moved value”,相信最近开始玩rust的同学看到这个报错都能会心一笑了。

    rust做到了不依赖运行期垃圾回收的安全内存管理,但这个特别爽快的特性也引入了一些不便,这报错就是常见的麻烦之一。

    这报错要是想展开说清楚,需要完整解释rust的ownership、borrowing、lifetime等概念,那是一篇太长的文章。

    我们先暂时放下概念,用三个不同的方法动手解决这个报错。

    错误

    我们以下面这段程序为基础展开我们的讨论,这里面主要定义的就是一个Info结构体。

    struct Info {
        s: String,
    }
    
    fn fn_a(info: Info) {
        println!("in fn_a");
    }
    
    fn main() {
        let foo = Info {s : "abc".to_string() };
        fn_a(foo);
    }

    首先,我们要制造出报错“use of moved value”。很简单,我们只需要以foo为参数再调用一次fn_a()就好。

    struct Info {
        s: String,
    }
    
    fn fn_a(info: Info) {
        println!("in fn_a");
    }
    
    fn main() {
        let foo = Info {s : "abc".to_string() };
        fn_a(foo);
        fn_a(foo); // 只有这行是新加入的
    }

    现在,我们得到了编译器报错。

    src/main.rs:12:10: 12:13 error: use of moved value: `foo`
    src/main.rs:12     fn_a(foo);
                            ^~~
    src/main.rs:11:10: 11:13 note: `foo` moved here because it has type `Info`, which is non-copyable
    src/main.rs:11     fn_a(foo);
                            ^~~

    编译器说,我们新加入的行里用了“移动了的值”。

    啥叫“移动了的值”呢?

    说白了就是用过了的值,foo已经给第一个fn_a()用过了,到了第二个fn_a()的时候就是moved value了。然后就不让用了。

    至于为什么要制定这样的规则,我另撰文解释。

    现在我们开始动手来解决这个问题。

    方法一:引用

    因为我们传给函数的是value,所以value被move了。

    我们可以通过传引用给函数来解决这问题。

    代码如下:

    struct Info {
        pub s: String,
    }
    
    fn fn_a(info: &mut Info) {
        info.s = "bbb".to_string();
    }
    
    fn main() {
        let mut foo = Info {s : "abc".to_string() };
        println!("1: {}", foo.s);
        fn_a(&mut foo);
        println!("2: {}", foo.s);
        fn_a(&mut foo);
        println!("3: {}", foo.s);
    }

    运行结果如下:

    1: abc
    2: bbb
    3: bbb

    我们把这段程序用impl改写一下。下面这段程序和上面的程序其实是等价的:

    struct Info {
        pub s: String,
    }
    
    impl Info {
        fn fn_a(&mut self) {
            self.s = "bbb".to_string();
        }
    }
    
    fn main() {
        let mut foo = Info {s : "abc".to_string() };
        println!("1: {}", foo.s);
        foo.fn_a();
        println!("2: {}", foo.s);
        foo.fn_a();
        println!("3: {}", foo.s);
    }

    运行结果显然是一样的:

    1: abc
    2: bbb
    3: bbb

    改写成这样之后是不是很眼熟了呢。

    没错,大多数的标准库就是用的这种方法来避免moved value问题的。

    impl里面的第一个参数其实就是struct的引用,所以我们用struct.fn()这种写法的时候,传递给方法实际都是引用。

    方法二:引用计数

    rust在标准库里提供了引用计数,它是另一个可以解决move value的方法。先列出来代码吧。

    use std::rc::Rc;
    use std::cell::RefCell;
    
    struct Info {
        s: String,
    }
    
    impl Info {
        fn new(a: &str) -> Info {
            Info {
                s: a.to_string(),
            }
        }
    }
    
    fn abc(a: Rc<RefCell<Info>>) {
        a.borrow_mut().s = "bbbbb".to_string();
    }
    
    fn main() {
        let bar = Rc::new(RefCell::new(Info::new("abc")));
        println!("1 : {}", bar.borrow().s);
        abc(bar.clone());
        println!("2 : {}", bar.borrow().s);
        abc(bar.clone());
        println!("3 : {}", bar.borrow().s);
    }

    这段代码有点稍稍复杂。

    其实需要注意的地方只有两个:

    1、使用了Rc之后,传递变量都要使用.clone()方法来增加引用计数;减少引用计数不用管,rust会根据作用域自己搞定;

    2、变量不用声明为mut了。使用的时候,如果不更改,使用.borrow()方法得到真正的struct;如果需要更改,则使用.borrow_mut()方法得到真正的struct。

    另外还有两点需要再说明一下:

    1、如果变量根本不需要改变,则不用套里面的RefCell::new()那层;

    2、如果涉及多线程之间的传参,要放弃Rc,使用Arc。

    方法三:实现Trait Clone

    从方法二中可以看到,我们使用了Rc,Rc就帮助我们实现了一个.clone()方法,让我们得以避免错误。

    那我们能不能自己实现.clone()呢,答案当然是肯定的。

    Rc做的是把数据和计数都放到了堆上,它提供的.clone()实际是对计数的复制。

    我们不搞那么复杂,我们做个简单点的,我们直接做对struct的复制。

    代码如下:

    struct Info {
        s: i32,
    }
    
    impl Info {
        fn new(a: i32) -> Info {
            Info {
                s: a,
            }
        }
    }
    
    impl Clone for Info {
        fn clone(&self) -> Info {
            Info {s: self.s}
        }
    }
    
    fn abc(a: Info) -> Info {
        Info {s: a.s + 1}
    }
    
    fn main() {
        let mut foo = Info::new(111);
        println!("1 : {}", foo.s);
        abc(foo);
        println!("2 : {}", foo.s);
        abc(foo);
        println!("3 : {}", foo.s);
    }

    运行结果:

    1 : 111
    2 : 111
    3 : 111

    为什么我们这里没有写foo.clone()而直接写foo编译器也没抱怨什么呢?

    我们可以看看最初编译器说的话:note: foo moved here because it has type Info , which is non-copyable

    它说,foo因为不能复制,所以它被move了。

    所以当我们让Info变得可复制了之后,它就不会被move了。

    当然要写成foo.clone(),那也是没问题的。

    但是注意因为我们的值是复制进去的,所以最原始的foo不会被改变了。

    最后

    以上我们用了三种方法解决了move value的报错。

    实际还有很多种方法可以用于在不同的场景中避免这个错误。

    有兴趣的同学可以继续玩耍,最后提供一个跟踪变量生命周期的方法以供愉快玩耍。

    实现Drop Trait在变量被释放或重分配空间时打印日志,代码如下:

    use std::fmt;
    use std::rc::Rc;
    use std::cell::RefCell;
    
    struct Info {
        s: String,
    }
    
    impl Info {
        fn new(a: &str) -> Info {
            println!{"new:{}", a};
            Info {
                s: a.to_string(),
            }
        }
    }
    
    impl Drop for Info {
        fn drop(&mut self) {
            println!("drop:{}", self.s);
        }
    }
    
    impl fmt::Display for Info {
        fn fmt(&self, f:&mut fmt::Formatter) -> fmt::Result {
            write!(f, "Info.s = {}", self.s)
        }
    }
    
    impl fmt::Debug for Info {
        fn fmt(&self, f:&mut fmt::Formatter) -> fmt::Result {
            write!(f, "Info.s = {}", self.s)
        }
    }
    
    fn main() {
        let foo = Info::new("abc");
        let mut bar = Rc::new(RefCell::new(Info::new("abc")));
        println!("1 : {:?}", bar);
        println!("code exit");
    }
  • 相关阅读:
    两数之和
    数组,链表,跳表
    第二讲:增长全景图
    三数之和
    第一讲:增长的本质
    移动零
    八十忆双亲+师友杂记
    java:从命令行接收多个数字,求和之后输出结果
    编程的精义读后感
    java语言基础第三讲作业
  • 原文地址:https://www.cnblogs.com/dhcn/p/12152116.html
Copyright © 2020-2023  润新知