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);
}

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

posted @ 2019-11-28 19:17  liuhe688  阅读(199)  评论(0编辑  收藏  举报