专栏首页江湖安得便相忘一起学Rust-理解所有权

一起学Rust-理解所有权

上期学习回顾:上期学习的结构体的结尾留了一个小的问题,这一期的开始来学习一下。

原问题是这样的: &str 类型通过mem::size_of::<&str>()进行打印内存,始终为16字节。(这里不严谨了,应该是在64位机器上是16字节)

为啥呢?其实答案就藏在rustdoc中,位于 std/primitive.str.html#representation.

原文:A &str is made up of two components: a pointer to some bytes, and a length. You can look at these with the as_ptr and len methods

use std::slice;
use std::str;

let story = "Once upon a time...";

let ptr = story.as_ptr();
let len = story.len();

// story has nineteen bytes
assert_eq!(19, len);

// We can re-build a str out of ptr and len. This is all unsafe because
// we are responsible for making sure the two components are valid:
let s = unsafe {
    // First, we build a &[u8]...
    let slice = slice::from_raw_parts(ptr, len);

    // ... and then convert that slice into a string slice
    str::from_utf8(slice)
};

assert_eq!(s, Ok(story));

这个原文中的例子就是证明 &str 组成的两部分,下面进行简单解析:

ptr通过as_ptr方法获取,是 *const u8 类型,占用8字节,len变量是usize类型在64位机器中是8字节。slice变量从from_raw_parts中获取,主要返回的是Repr结构中的rust成员,T指代类型是u8:

#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub unsafe fn from_raw_parts<'a, T>(data: *const T, len: usize) -> &'a [T] {
    debug_assert!(data as usize % mem::align_of::<T>() == 0, "attempt to create unaligned slice");
    debug_assert!(mem::size_of::<T>().saturating_mul(len) <= isize::MAX as usize,
                  "attempt to create slice covering half the address space");
    Repr { raw: FatPtr { data, len } }.rust
}

下面是union Repr结构,其中rust、rust_mut、raw共用同一块内存空间。

#[repr(C)]    
union Repr<'a, T: 'a> {
    rust: &'a [T],
    rust_mut: &'a mut [T],
    raw: FatPtr<T>,
}

下面是FatPtr结构

#[repr(C)]
struct FatPtr<T> {
    data: *const T,
    len: usize,
}

实际的FatPtr则是一个8 + 8 = 16字节的结构体,那么Repr的union就是16字节。同时由于ptr变量是* const u8类型,所以T为u8,因此from_raw_parts方法返回类型为* const [u8],大小为16字节。而接下来的方法内仅做来utf8的检查以及类型转换的工作,大小未发生变化,所以在64位的机器上 &str 类型是16字节。

本期正题

所有权的概念,是在Rust初学时需要面对的一个难题,总是在编写代码的过程中出现各种的问题

error[E0382]: use of moved value: `s`

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable

所有权规则:

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

所有权规则解释:

  • 在Rust中,每一个值都会对应一个叫做所有者的变量。
  • 一次运行中的值只能存在一个所有者
  • 当所有者离开作用域,它的值会被释放掉。
fn main() {
    let arr = vec![1,2];
    if arr.len() > 0 {
        let arr1 = arr;
        println!("{:?}", arr1);  //打印 [1,2]
    }
    println!("{:?}", arr);  //编译报错

    println!("{:?}", arr1);  //编译报错
}

在上面的例子中,可知以下几条信息

arr是vec![1,2]的所有者。

main函数的代码块是一个作用域,if 的语句块也是一个单独的作用域。

在if 代码块中 vec![1,2]的所有者变成了arr1。

所以如果注释掉底部的两个错误语句,第5行是可以打印arr1的值。而下面打印arr失败的原因就是arr变量已经从内存释放,无法访问。而打印arr1出现错误的原因就是arr1是属于if代码块的,当离开if 的作用域后,内存释放。

移动和复制

当创建一个不定长的值的情况下会在堆内存中申请空间,此类值的变量在重新赋值给另外一个变量时会发生所有权的移动 move ,移动的结果就是原有的变量释放,新变量指向值的堆内存地址,成为此值的唯一所有者,将来在离开作用域后释放此变量以及其值的内存空间。(由于Rust内无垃圾回收机制,如果不是移动所有权,那么会有两个或多个变量指向值的堆内存,则在离开作用域释放内存时可能会出现多次释放,可能存在内存安全的问题,所以为了防止出现内存安全的问题,使用了唯一对应的所有者,释放内存时也仅一次性完成)

let string = String::new();
let new_string = string;  //会发生所有权转移,string变量释放。

对于原则1有一点是需要关注的,看以下例子:

let num1 = 5;  //以下均可以正常编译运行。
// let num1 = "abc";
// let num1 = (2,3);
// let num1 = [23,54];
// let num1 = ("sdk", 3);
// let num1 = true;
// let num2 = num1;
println!("{},{}", num1, num2);  //正常打印

可以正常运行的原因是因为num2赋值时发生了值的复制,观察可发现num1变量的值均是标量值,固定大小,是存储在栈内存中的,所以复制相对容易很多,所以Rust提供了复制的功能,在离开作用域时分别释放各自的内存,不会出现多次释放的内存安全问题,而且也同样满足所有权第一条的规则。

如果变量中包含有需要申请堆内存的值就会进行发生所有权移动,而不是复制,因为堆内的数据大小无法确定,复制可能会造成大量资源的消耗:

let var1 = (3, String::from("s"));
let var2 = var1;  //这里会发生所有权移动。

------------

struct Demo {
    a:char,
    b:i32
}
let var1 = Demo{a: '3', b:3};
let var2 = var1;  // 所有权移动

克隆

克隆可用于对堆内存的值的拷贝,堆内存数据在Rust内不存在深浅拷贝的说法,可以认为克隆就是深拷贝,完全拷贝堆内存数据,比如String类型就实现了Clone trait,可以通过调用clone方法拷贝一份数据:

let var1 = String::from("sd");
let var2 = var1.clone();  //克隆,
println!("{},{}", var1, var2);   //编译成功

-------
#[derive(Debug,Clone)]  //必须定义Clone才能调用clone方法,
struct Demo {
    a:char,
    b:i32
}
let var1 = Demo{a: '3', b:3};
let var2 = var1.clone();  //克隆堆内存
println!("{},{}", var1, var2);   //编译成功

------
#[derive(Debug,Copy,Clone)]  //实现Copy和Clone,在赋值时会发生赋值
struct Demo {
    a:char,
    b:i32
}
let var1 = Demo{a: '3', b:3};
let var2 = var1;  //克隆堆内存
println!("{},{}", var1, var2);   //编译成功

-----** but **----

//这里是编译不通过的,
//因为Demo中的b类型实现了Drop trait
#[derive(Debug, Copy, Clone)]  
struct Demo {
    a:char,
    b:Vec<i32>
}

如上例中,通过对全部是标量类型成员的结构体,实现Copy和Clone trait是可以在赋值时直接发生克隆操作的,不必要显示调用clone方法。但是对于成员中含有如Vec<T>类型的结构体,则无法实现Copy,因为其本身实现了Drop,与Copy trait互斥。

栈中的数据调用clone和不调用clone的效果是一样的,因为在重新赋值时就是完全拷贝的,所以可以省略clone的调用。

对于所有权的规则可以通过各种的变量组合进行测试,总结规律,才能印象深刻。

函数作用域

不仅仅是变量重新赋值,当值在不同作用域间传递时,也会发生所有权转移,下面的示例无法成功编译。

fn main() {
    let s = String::from("sd");
    test(s);//  s的所有权转移至test函数内
    println!("{}",s); 
}

fn test(s:String) {
    println!("{}", s);
}

由于第三条规则的原因,离开作用域会释放内存,所以发生所有权的转移同样是为了防止发生内存安全问题。

fn main() {
    let s = String::from("sd");
    let a = test(s);  //s所有权转移,原s失效,a接受test内转移的数据。
    println!("{}", a);
}

fn test(s:String) -> String {
    println!("{}", s);
    s      // s作为返回值返回,所有权转移出此方法
}//离开,作用域内变量释放

上面的例子说明了所有权转移的变量,只是变量失效,但并不影响值,将值转移给其他变量,函数的返回值也是同样可以转移所有权

本文分享自微信公众号 - 可回收BUG(way-of-full-stack),作者:江湖安得便相忘

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一起学Rust-变量及类型

    在Rust语言中,所有的变量默认均是不可变变量,不可变变量就是当变量完成值当初始化后不能再次重新赋值的变量。

    江湖安得便相忘
  • 简述:Rust-1.38.0 RELEASE NOTE

    mac OS更新,如果使用brew安装的,那么恭喜你,现在brew上面只能更新到1.37.0:

    江湖安得便相忘
  • 一起学Rust-引用 · 借用

    接续上一期的所有权的学习,所有权的内容中强调的是变量是资源的所有者,拥有对资源的控制权(例如移动,释放),但并不是所有的变量都拥有所指向的资源,那就是引用(Re...

    江湖安得便相忘
  • 一起学Rust-理解所有权

    原问题是这样的: &str 类型通过mem::size_of::<&str>()进行打印内存,始终为16字节。(这里不严谨了,应该是在64位机器上是16...

    MikeLoveRust
  • swift4.0 对象数据源根据属性分组,时间排序

    //dataArray:[OperationMaintenanceObject] 对象数组

    ZY_FlyWay
  • Unicode转中文,Unicode编码转换,ASCII转Unicode,Unicode转ASCII

    vivec
  • 以色列初创企业Skyline AI获300万美元种子融资,红杉资本领投

    【数据猿导读】Skyline AI是一家以色列初创公司,利用机器学习帮助房地产投资者识别有前景的房产。今日,公司宣布已从红杉资本获得了300万美元的种子资金。本...

    数据猿
  • HTML5 File API 配合 Web Worker 计算大文件 SHA3 Hash 值

    这学期的安全学课程有个作业,内容是写一个软件实现 SHA3 Hash 值的快速计算。想一想老师这么安排,大致上也有一种推广新的密码学算法的意图。既然希望应用起来...

    zgq354
  • ES6——解构赋值(Destructuring)

    ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。在ES6 之前为变量赋值,只能直接指定值。

    羊羽shine
  • 软件架构师的12项修炼[3]——个人技能修炼(1)——透明化

    当你在协作的环境中与别人一道工作时,你的自然反应就是想尽可能表现自己好的一面。看起来你能独立解决自己的所有问题,并把任何可能的问题隐藏在背包里—— “这里没有任...

    高广超

扫码关注云+社区

领取腾讯云代金券