zoukankan      html  css  js  c++  java
  • 三个方法解决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");
    }
  • 相关阅读:
    LeetCode 40. 组合总和 II(Combination Sum II)
    LeetCode 129. 求根到叶子节点数字之和(Sum Root to Leaf Numbers)
    LeetCode 60. 第k个排列(Permutation Sequence)
    LeetCode 47. 全排列 II(Permutations II)
    LeetCode 46. 全排列(Permutations)
    LeetCode 93. 复原IP地址(Restore IP Addresses)
    LeetCode 98. 验证二叉搜索树(Validate Binary Search Tree)
    LeetCode 59. 螺旋矩阵 II(Spiral Matrix II)
    一重指针和二重指针
    指针的意义
  • 原文地址:https://www.cnblogs.com/dhcn/p/12152116.html
Copyright © 2011-2022 走看看