Rust 所有权探索

变量在函数调用过程中的传递

fn main() {
    let data = vec![5, 7, 3, 1];
    let v = 7;

    match find_pos(data, v) {
        Some(pos) => println!("Found {} at {}", v, pos),
        None => println!("{} Not found", v),
    }
}

fn find_pos(data: Vec, v: u32) -> Option {
    for (pos, item) in data.iter().enumerate() {
        if *item == v {
            return Some(pos);
        }
    }

    None
}

在上面的示例代码中,main()函数定义了一个动态数组data和一个值v,然后将其传递给函数find_pos,在data中查找v是否存在,如果存在则返回v在data中的下标,如果不存在则返回None。

data 作为动态数组,因为大小在编译期无法确定,所以放在堆上,并且在栈上有一个包含了长度和容量的胖指针指向堆上的内存。

在调用find_pos函数时,main()函数中的局部变量data和v 作为参数传递给了find_pos(),因此,它们会被放在 find_pos()调用栈中的参数区:

参数引用

像Java等大多数编程语言的做饭,现在堆上的内存就有了两个引用,并且每次把data作为参数传递一次,堆上的内存就会多一次引用。

那么造成的问题就是,无法得知这些引用到底能做什么操作,堆上内存什么时候能够释放,也无法确认。如果有多个调用栈引用堆上的内存时,给内存管理带来很大挑战。同时,引用是可以隐式产生的,随意性太大,例如Java中随处可见的按引用传参,它们可读可写,有很大的权限。

对于这种堆内存多次引用的问题,传统语言有如下解决方案:

以上方案,都各有利弊,都是从管理引用的角度来考虑的。

但是,此问题本质上是因为堆上的内存会被随意引用

所有权和Move语义

Rust对于值的使用,给出了以下规则:

在这三个所有权规则的约束下,上面示例的引用问题可以这样解决:

所有权和move

原先main()函数中的data,被移动到find_pos()后,就失效了,Rust编译器会保证main()函数随后的代码无法访问这个变量,就确保了堆上的内存依旧只有唯一的引用。解决了堆上数据的多重引用。

main()函数中在把data传递给find_pos()后,还想让main()函数能够访问的化,可以调用clone()方法,把data复制一份出来。

但是这种手动复制,会让代码变得复杂,一些只存储在栈上的简单类型数据,如果要避免所有权转移之后不能访问的情况,只能频繁的手动clone复制。

Rust给出了两种方案:

Copy语义和Copy trait

符合Copy语义的类型,在赋值或传参时,值会自动按位拷贝。

如果数据类型没有实现Copy trait,在赋值或者函数调用的时候无法Copy,那么就按默认的Move语义。

也就是说,要移动一个值,如果值的类型实现了Copy trait,就会自动使用Copy语义进行拷贝,否则使用Move语义进行移动

但是Copy trait也有一定的限制,Copy trait和Drop trait 不能共存。一旦你实现了Copy trait,就无法实现Drop trait。反之亦然。

实现了Copy trait 的数据结构

fn is_copy() {}

fn types_impl_copy_trait() {
    is_copy::();
    is_copy::();

    // 所有的整数类型都是 copy
    is_copy::();
    is_copy::();
    is_copy::();
    is_copy::();
    is_copy::();
    is_copy::();
    is_copy::();
    is_copy::();
    is_copy::();

    is_copy::();
    is_copy::();

    // 函数指针是copy
    is_copy::();

    // 裸指针是 copy
    is_copy::<*const String>();
    is_copy::<*mut String>();

    // 不可变引用是 copy
    is_copy::<&String>();
    is_copy::<&[Vec]>();

    //对于数组/元组,如果其内部类型是 copy,那么它们就是 copy
    is_copy::<[u8; 4]>();
    is_copy::<(&str, &str)>();
}
fn types_not_impl_copy_trait() {
    // DST 类型不是 copy
    // is_copy::();
    // is_copy::<[u8]>();

    // 有堆内存的类型不是 copy
    //is_copy::>();
    //is_copy::();

    //可变引用不是 copy
    //is_copy::<&mut String>();

    // 对于数组/元组,如果其内部类型不是 copy,那么它们也不是copy
    //is_copy::<[Vec;4]>;
    //is_copy::<(String,u23)>();
}

Borrow语义

Borrow语义通过引用语法(&或者&mut)来实现。

在Rust中,“借用”和“引用”是一个概念。所有的引用都只是借用了“临时使用权”,它并不破坏值的单一所有权约束。

在默认的情况下,Rust的借用都是只读的。

只读借用

本质上,引用是一个受控的指针,指向某个特定的类型。

其他传统语言,函数传参有两种方式:传值(pass-by-value)和 传引用(pass-by-reference)。

比如在Java,给函数传一个整数,这是传值,与Rust中的Copy语义一致。

给函数传一个对象,或者是任何堆上的数据结构,Java会自动隐式地传引用。

但是Java的引用是对象的别名,这也导致随着程序的运行,同一块内存的引用到处都是,不得不依赖GC进行内存回收。

Rust中没有传引用的概念,Rust所有的参数传递都是传值,不管是Copy还是Move。

所以,在Rust中,必须显式地把某个数据的引用,传给另一个函数。

Rust的引用实现了Copy trait,所以按照Copy语义,此引用会被复制一份交给要调用的函数。

对这个函数来说,它并不拥有数据本身,数据只是临时借给它使用。所有权还在原来的拥有者那里。

在Rust里,引用是一等公民,和其他数据类型地位相等。

fn main() {
    let data = vec![1, 2, 3, 4]; // data 的生命周期比sum()中对data的引用要长
    let data1 = &data; // 借用不能超过值的生存期
                       // 值的地址是什么?引用的地址又是什么?
    println!(
        "address of value: {:p}({:p}),address of data: {:p},data1: {:p}", //0x8aa19bf8a8(0x8aa19bf8a8),address of data: 0x8aa19bf948,data1: 0x8aa19bf8c0
        &data,                                                            //0x8aa19bf8a8
        data1,                                                            //0x8aa19bf8a8
        &&data,                                                           //0x8aa19bf948
        &data1                                                            //0x8aa19bf8c0
    );

    println!("sum of data1: {}", sum(&data)); // sum 函数处在 main() 函数下一层调用栈中,它结束后main函数还会继续执行

    // 堆上数据的地址是什么
    println!(
        "address of items:[{:p},{:p},{:p},{:p}]", //address of items:[0x1cc59705670,0x1cc59705674,0x1cc59705678,0x1cc5970567c]
        &data[0],                                 //0x1cc59705670
        &data[1],                                 //0x1cc59705674
        &data[2],                                 //0x1cc59705678
        &data[3],                                 //0x1cc5970567c
    );
}

fn sum(data: &[u32]) -> u32 {
    //值的地址会改变吗,引用的地址会改变吗
    println!(
        "address of value: {:p},addr of ref: {:p}", //address of value: 0x1cc59705670,addr of ref: 0x8aa19bf6d0
        data,                                       //0x1cc59705670
        &data                                       //0x8aa19bf6d0
    );
    data.iter().sum()
}

// 只读引用 实现了 Copy trait,也就意味着引用的赋值、传参都会产生新的浅拷贝
// 虽然data 有很多只读引用指向它,但是堆上的数据依旧只有data 一个所有者:值的任意多个引用并不影响所有权的唯一性。

借用

data1、&data和传到sum()里的data1' 都是指向data本身,这个值的地址是固定的。但是它们引用的地址都是不同的。这是因为 只读引用实现了Copy trait,也就是意味着引用的赋值、传参都会产生新的浅拷贝。堆上数据依旧只有data一个所有者。值的任意多个引用并不影响所有权的唯一性。

借用的生命周期

借用(引用)不能超过(overlive)值的生存期。

堆变量的生命周期不具备任意长短的灵活性,因为堆上内存的生死存亡,跟栈上的所有者牢牢绑定。而栈上内存的生命周期,又跟栈的生命周期相关,所以,我们只需要关心调用栈的生命周期。

可变借用/引用

为了保证内存安全,Rust对可变引用的使用做了严格的约束:

展开阅读全文

页面更新:2024-05-02

标签:所有权   语义   赋值   所有者   变量   生命周期   函数   内存   类型   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top