• Rust: 所有权


    初学 Rust 的小伙伴,很容易就写出下面这样的代码:

    fn main() {
        let a = String::from("hello");
    
        // ...
    
        // 将a赋给b
        let b = a;
    
        // ...
    
        // 编译报错 变量a不能继续使用
        println!("{} {}", a, b);
    }
    

    运行代码,就会发现控制台编译报错,提示变量 a 不能再继续使用了。

    什么鬼,赋值之后原来的变量就不能用了?三十年的编程经验要被颠覆 ...

    其实啊,这是 Rust 区别于其他编程语言最具代表性的特性了,涉及到一个非常重要的概念:所有权机制。

    在 Rust 中,每个 数据 只能有一个 变量 与之绑定,这个 绑定的变量 就是该值的 所有者,我们也可以说,该变量拥有这个数据的 所有权。绑定一旦被更改,所有权将 转移 到新的变量,旧变量将不能继续使用(除非重新获得所有权)。

    我们回过头来分析一下上面的代码:

    首先,一个内容为 "hello" 的字符串对象被创建,并与变量 a 进行绑定,此时,a 拥有了该字符串的所有权。

    然后,我们将 a 赋给了 b,此时变量 b 获得了 "hello" 的访问权,同时 a 也就失去了自己对该数据的所有权。

    请记住,在同一时刻,每个数据只能对应一个所有者。访问数据的所有权只进行转移,从不共享。

    为什么 Rust 要这么设计呢?因为共享数据实在是太危险了。

    前面我们介绍过,在 Rust 中,字符串有两种形式:&strString,前者一旦创建便不可更改,而对于后者,可以通过一些方法修改字符串的内容。

    对于 String 类型的数据,它们会被创建并存储在 空间中,同时,在 空间中,会有一个变量指向这块数据。

    如果有两个变量同时都指向同一块数据,就可能会出现预期之外的结果,就像下面这段 Java 代码一样:

    // Java示例代码
    
    // 栈上的a指向堆中的StringBuilder实例
    StringBuilder a = new StringBuilder("hello");
    
    // ...
    
    // 栈上的b也指向了同一份实例对象
    StringBuilder b = a;
    
    b.append(" world");
    
    // ...
    
    // 变量a的值被修改了 打印结果:"hello world"
    System.out.println(a);
    

    所以,以安全著称的 Rust 怎么可能允许此类情况发生呢?

    在 Rust 中,赋值操作一旦完成,对数据的访问权将直接从旧变量转移到新变量,其结果是,旧变量再也不能使用了。

    下面代码演示了所有权转移的过程:

    fn main() {
        let a = String::from("hello");
    
        // 所有权从a转移到b
        let mut b = a;
        
        // 有mut修饰 变量b可以修改内容
        b.push_str(" world");
    
        // 打印结果 "hello world"
        println!("{}", b);
    }
    

    上面我们看到的,只是在同一个作用域内的所有权转移,在块级作用域内外,也存在相同的现象:

    fn main() {
        let a = String::from("hello");
    
        {
            let b = a;
            
            println!("{}", b);
        }
    
        // 报错 变量a不能访问
        println!("{}", a);
    }
    

    在块级作用域结束时,里面的变量和对应的数据都要被销毁,这时,我们应该归还所有权:

    fn main() {
        let a = String::from("hello");
    
        let a = {
            let b = a;
            
            println!("{}", b);
            
            // 销毁之前 归还所有权
            // 变量b:出来混迟早是要还的
            b
        };
    
        println!("{}", a);
    }
    

    在块级作用域结束时,我们使用了一个表达式,将 b 作为结果返回到外层作用域,归还所有权,变量 a 重新获得了所有权,可以继续使用了。

    此外,在调用函数时,所有权也会进行转移。变量的所有权,其实是和作用域相关联的,当一个变量作为函数的参数传递到函数内部时,它的所有权也会转移到该函数的内部作用域,因此在原作用域中,该变量就不能继续使用了。

    我们来看下面演示:

    fn main() {
        let s = String::from("hello");
    
        // 变量s:劳资进去就再没出来过
        let len = get_len(s);
    
        // 报错:变量s所有权已转移到函数内部
        println!("{} {}", s, len);
    }
    
    // 获取字符串长度
    fn get_len(s: String) -> usize {
        s.len()
    }
    

    可以看到,在函数调用结束后,变量 s 已经无法继续使用了,原因是所有权转移到 get_len(s) 函数内部了。

    那么这个问题,可该如何解决呢?不急,办法还是有滴,不就是转移进去了么,再转移出来不就行了嘛:

    fn main() {
        let s = String::from("hello");
    
        // 变量s:我胡汉三又回来了
        let (len, s) = get_len(s);
    
        println!("{} {}", s, len);
    }
    
    fn get_len(s: String) -> (usize, String) {
        // 使用一个元组 可以将s所有权归还
        (s.len(), s)
    }
    

    不过需要注意的是,元组内的顺序可不是随便排列的,下面代码在编译时会报错:

    fn main() {
        let s = String::from("hello");
    
        let (s, len) = get_len(s);
    
        println!("{} {}", s, len);
    }
    
    fn get_len(s: String) -> (String, usize) {
        // 调用s.len()报错
        // 变量s:劳资都要出去了 你还扯我后腿
        (s, s.len())
    }
    

    在调用 s.len() 时,变量 s 的所有权已被转移,如果我们想保留结果返回的顺序,可以像下面这样改进:

    fn main() {
        let s = String::from("hello");
    
        let (s, len) = get_len(s);
    
        println!("{} {}", s, len);
    }
    
    fn get_len(s: String) -> (String, usize) {
        let len = s.len();
        (s, len)
    }
    

    到这里,相信大家都明白了所有权机制的这套规则了吧。

    不过有些朋友可能觉得疑惑,你说所有权会转移,那下面代码为什么能编译通过呢:

    fn main() {
        let a = 3;
        let b = a;
        
        // 3 3
        println!("{} {}", a, b);
        
        let a = "hello";
        let b = a;
        
        // "hello" "hello"
        println!("{:?} {:?}", a, b);
    }
    

    原因在于,基础类型都实现了 Copy 特性,不会出现同时修改一份数据的问题,原变量也不会失去所有权,所以上面代码没毛病,可以大胆地写。关于 Copy 特性,我们后续会做介绍。

    对于 String 类型,它没有实现 Copy 特性,我们必须显式调用 clone() 方法进行复制:

    fn main() {
        let a = String::from("hello");
        let b = a.clone();
        println!("{} {}", a, b);
    }
    

    以上就是对所有权相关知识的介绍,引用类型涉及内存安全操作,所以会受到所有权机制的限制,处理逻辑也因此显得有些繁琐,那么有没有别的方式,可以简化这些处理逻辑呢,在下一篇文章中,我们准备探索一下 借用 这个概念。

  • 相关阅读:
    关于MVC与MVP的理解
    使用JDBC连接数据库
    常见DML语句汇总
    java字符常量
    java中Map,List与Set的区别
    面向对象重写(override)与重载(overload)区别
    嵌入式系统的体系结构 图解
    使用webpack + momentjs时, 需要注意的问题
    联想电脑屏幕亮度无法调节
    树形数据构造
  • 原文地址:https://www.cnblogs.com/liuhe688/p/13416101.html
Copyright © 2020-2023  润新知