美文网首页
Rust核心设计之Ownership

Rust核心设计之Ownership

作者: moneyoverf1ow | 来源:发表于2020-02-07 11:17 被阅读0次

    Ownership in Rust

    背景

    目前主流编程语言管理内存的方式不外乎两种--gc或者手动. ownership是rust最独特的特性, 属于第三种解决方案. 它被用来管理内存以及跟踪代码使用的堆上数据, 最大化地减少堆上的重复数据. 由于这种方式在编译期间进行, 因此它的任何特性均不会拖慢程序运行时的性能.


    owner的规则

    1. 每个值都有一个变量, 称其为owner
    2. 他们同一时间只有一个owner
    3. 当owner走出scope时, 值将被释放

    简单的机制

    在owner走出scope时, rust会调用一个特殊的drop函数, 来释放该owner.


    实现该机制遇到的复杂场景

    ownership受到C++的RAII机制的启发. 看上去原理简单, 但是实现起来还是相当复杂的.
    以下是一些具体的场景:

    1. move, 类似浅拷贝
    let x = String.from("hello");
    let y = x;
    // 编译错误
    println!("{}", x);
    

    这里类似浅拷贝但又有所区别, 拷贝的是变量本身, 在栈中入了一份一样的变量, 但是指向的值在堆中, 是同一份. 所以问题来了, 假设此时有2个owner, 那么在退出该scope时, 需要释放一个内存两次, 这是不行的. 所以, 回到规则的第二条, owner只能有一个, 这就是move和浅拷贝的区别, 因为它让源变量的ownership传递到新的变量, 使源变量失效. 假使在上面两行后面再加一行对x的访问, 那么会在编译时报错, 提示borrow of moved value: x. (rust永远不会自动对数据使用深拷贝, 这种情况下的拷贝被认为是没什么代价的)

    1. 使用深拷贝
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2);
    

    既然是深拷贝, 那么值自然就是2个, 因此也就不存在违反规则的情况.
    在非手动的情况下, Rust避免使用深拷贝是出于对性能的考虑.

    1. 具有Copy特征(其他语言叫接口)的情况
    let x = 5;
    let y = x;
    println!("x = {}, y = {}", x, y);
    

    这里x仍然可用. 所有的整形, 浮点, 布尔, 字符类型以及元组都是有Copy特征的.
    为什么这么做呢? 因为这些变量size固定, 在编译期间被存入栈中, 这样做代价很低, 移动一下栈顶指针就可以了, 所以干脆copy一下值.

    1. 函数
    let s = String::from("hello");
    some_function(s);
    let x = 5;
    another_function(x);
    

    在语义上, 等同于赋值给变量, 使用move或者copy.

    1. 函数返回值
    let s1 = String::from("hello");
    let s2 = some_function(s1);
    let s3 = another_function(s2);
    

    函数返回值同样可以将ownership传递到赋值的变量.

    总结起来其实遵循的规律是一样的, 当值由一个变量转到另一个变量时, 使用move. 指向堆中数据的变量出scope时, 值将会被清除, 除非此值已被move.


    引用&借用

    当变量传入函数, 如果是move, 则该变量已失效, 那么如何获取原变量的值呢?

    使用元组获取原ownership

    fn main() {
        let s1 = String::from("hello");
        let (s2, len) = calculate_length(s1);
        println!("The length of '{}' is {}.", s2, len);
    }
    
    fn calculate_length(s: String) -> (String, usize) {
        let length = s.len();
        (s, length)
    }
    

    这种方式有点麻烦, 写多了肯定会吐
    于是有了下面这种:

    fn main() {
        let s1 = String::from("hello");
        let len = calculate_length(&s1);
        println!("The length of '{}' is {}.", s1, len);
    }
    
    fn calculate_length(s: &String) -> usize {
        s.len()
    }
    

    这里创建了一个引用指向s1, 结构看上去是这样的&s1->s1->data, 因为引用并没有获取这个值的ownership, 因此在引用退出scope时, 它的值不会被drop. 这种引用作为函数参数的方式称为borrowing. 这个名字非常形象, 因为这表示这样东西的所有权并不是我们的, 并且有借就有还.

    引用也是有可变和不可变的, 可变就加关键字mut. 这里有一个约束, 同一个scope中, 同一个值, 只能有一个可变引用, 这是为了规避数据竞争(它的条件: 1.有多个指针同时访问相同变量 2.其中至少有一个可以写数据 3.没有同步机制).

    let mut s = String::from("hello");
    {
        let r1 = &mut s;
    }
    let r2 = &mut s;
    

    这是可以的, 因为r1已经退出scope.

    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
    println!("{}, {}, and {}", r1, r2, r3);
    

    错误, s已被借为不可变量, 不能同时被借为可变量.

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    let r3 = &mut s;
    println!("{}", r3);
    

    可以, 因为r1, r2已经不再被使用, 他们的scope没有交集.


    悬挂指针

    编译期间会杜绝这种情况的发生,保证了引用指向的变量一定在scope内。

    fn main() {
        let dangling = dangle();
    }
    
    fn dangle() -> &String {
        let s = String::from("hello");
        &s
    }
    

    因为s已经退出scope,返回s的引用是无法通过编译的。


    切片

    切片没有ownership,因为假设它有,那么这个ownership将被2个owner拥有,即slice与原集合,违反了owner的基本原则。编译器会保证切片引用的变量一定不会退出scope,看个例子。

    fn main() {
        let mut s = String::from("hello world");
        let a_slice = slice_of(&s); // 省略函数定义
        s.clear(); // error
        println!("the slice is: {}", a_slice);
    }
    

    这里会报出一个编译错误,rustc --explain E0502看一下原因,

    This error indicates that you are trying to borrow a variable as mutable when it
    has already been borrowed as immutable.

    哪里有mutable的借用呢?
    看下clear()函数的源码:

    #[inline]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn clear(&mut self) {
        self.vec.clear()
    }
    

    可以看到入参是自身的可变借用。之前提到过,可变与不变引用不能同时出现的同样的scope中,或者这么说,它们的scope有交集,因为这样会满足数据竞争的条件,这是严格禁止的。因此,从编译层面保证了切片指向的值一定是有效的。

    总结

    说到底,其核心思想就是将内存占用与变量的生命周期绑定,当变量生命周期结束,内存也将释放。
    伟人总是站在伟人的肩膀上,我们总是站在伟人的肩膀上。向伟大的前辈致敬。这种设计非常巧妙,即保证的效率,又方便了开发者。不过凡事都有两面性,编译期间搞的这么6,编译速度比起C++怕是不遑多让:)

    参考文献

    “The Rust Programming Language”, by Steve Klabnik and Carol Nichols

    相关文章

      网友评论

          本文标题:Rust核心设计之Ownership

          本文链接:https://www.haomeiwen.com/subject/wdxmxhtx.html