• Rust-Lang Book Ch.4 Ownership


    Ownership

    Ownership使得Rust能够无需额外的garbage collector线程就确保内存安全。在编译时,Rust就通过一系列规则并确定Ownership。Ownership与Borrowing, slices和Rust在内存中如何排列数据有关。

    在许多编程语言中,数据在stack上还是在heap上不会对性能有很大影响。但是在Rust中则不然。存在stack中的数据必须已知具体所占空间大小,否则就应该放到heap上。Stack与Heap在性能上的不同主要有如下两点:1. 推送数据到Stack上要比在heap分配空间要更快,这是因为allocator不需要去寻找能够容纳当前数据的地址。2. 在stack中获取数据要比在heap中更快。这是因为heap可能要通过不紧邻的指针来取值

    Ownership Rules

    Rust中的每个值都有一个管理其生命周期的变量称为Owner。同一时间只能有一个Owner。当Owner出了作用域后,该值就从内存中抹去。

    Variable Scope

        {                      // s is not valid here, it’s not yet declared
            let s = "hello";   // s is valid from this point forward
    
            // do stuff with s
        }                      // this scope is now over, and s is no longer valid
    

     

    以String为例阐释Rust的OwnerShip机制

    string literal所占空间已知,同时数值已知,但是是immutable的。可改的String变量必须要从string literal转化而来。显而易见的,string literal的具体内容编译的时候就已知了,所以Rust就直接将literal放入到最终的目标格式文件中,因此,string literal本身能够保持快速+高效。不过,这也导致了这些literal都是immutable的。

    而在String这个类型中,则能够支持可变的text,编译器会在heap上分配所需空间,而这些在编译的时候是未知的。为此,这些memory需要被memory allocator在运行的时候请求,并且当这个String变量出了作用域之后被释放掉。具体地,Rust会在Variable出了作用域之后调用一个名为drop的函数。drop函数的基本模式与c++的RAII(Resource Acquisition Is Initialization,资源获取即是初始化)十分相似。

    String具体由三部分构成: 1. 一个指向内容所在内存的指针 2. 内容的长度 3. 目前的容量capacity。对于`let s2 = s1`这样一句话,不会发生内容的拷贝,只会发生指针的拷贝。在指针拷贝后,这片内存就有多个值同时使用,当出了作用域之后,也就会引发两次free。如何避免double free呢?1. 在s2=s1这句之后,编译器会标记s1是个无效变量,如果这之后再去用s1,Rust会直接报错:borrow of moved value: `s1`。2. s2=s1时发生了Move(类似Shadow Copy)

        let s1 = String::from("hello");
        let s2 = s1;
    
        println!("{}, world!", s1);
    //^^ value borrowed here after move

    如果希望Deep Copy这个String,那么我们应该调用clone方法。要注意clone方法相对于move更耗时耗空间。

    String类型的变量s1是空间未定且存储在heap上的,而对于空间大小已经由类型给定,且存储在stack上的变量,Copy是可以轻松进行的,也即此时Move(shadow copy)和Clone(deep copy)本质上是相同的。例如let i1=5;let i2 = i1;就不需要invalidate i1。Rust有一个特别的Annotation名为Copy,可以让类似Integer的类型自动拷贝赋值,而且不会使得赋值后旧的变量变得无效。注意如果要实现Copy的类型不能有任何成员实现了Drop特性,如果实现了Drop特性,也即要求该类型在出作用域时做出什么特定行为,就会得到编译错误。注意Tuples如果成员都是可以copy的,那么Tuple本身也可以Copy。

    Ownership and Functions

    传参这一行为和赋值也非常相似,也就是说,将一个变量传入、传出函数就会发生move或者copy这种行为。如果将s1: String传入一个函数,并且在这之后仍然要使用s1,编译器就会报错。如果一个变量包含在heap上的数据出了作用域,那么在heap上的数据就会被清理掉,除非这个变量之前被moved,并且将ownership转交给了另外一个变量。

    那么如果我们希望一个函数使用一个变量,但是不要发生ownership的转移呢?传入之后再把所有变量返回并且赋值实在是太麻烦了。这时,我们可以使用reference,即引用,符号&。

    References and Borrowing

    像c语言一样,Reference使用符号&,而Dereference使用符号*。引用本身相当于一个指向变量的指针,使得变量本身的控制权不会发生改变。Rust称使用references来作为函数参数的行为为borrowing,即借用。

    fn calculate_length(s: &String) -> usize { // s is a reference to a String
        s.len()
    } // Here, s goes out of scope. But because it does not have ownership of what
      // it refers to, nothing happens.
    

      

        let s1 = String::from("hello");
    
        let len = calculate_length(&s1);
    

      

    要区分&String与c++中的reference。c++中的reference是能够修改变量内容的,而Rust中的&String则仅是不会发生所有权的Move。如果想要修改变量内容,依然要加上mut这个关键字,使之成为可修改的引用,即必须传给函数&mut String类型的变量,函数才能修改String而且不发生变量所有权转让。注意mutable reference有个限制,那就是在一个作用域中,一次只能存在一个有效的mutable reference。(难道mutable reference是单例的?)

    let r1 = &mut s;
             ------ first mutable borrow occurs here
    let r2 = &mut s;
             ^^^^^^ second mutable borrow occurs here
    println!("{}, {}", r1, r2);
                      -- first borrow later used here
    

      

    此外,immutable reference和mutable reference也存在如下限制:要么同时多个用于读的immutable reference,要么单个mutable reference。

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

    这样限制的目的是为了避免data race。

    Data race发生在如下场景:

    1. 多个指针同时使用同块数据。

    2. 至少其中一个执行写操作

    3. 程序员没有附加该如何处理这种读写冲突或者写写冲突的说明

    这种data race可能导致非常隐秘的bug,所以Rust直接规定只准有一个mutable reference,只准有一个指针写这块数据,从根本上避免冲突。

    不过,reference用过了不再使用就可以声明新的,并不是要一直出了作用域才能声明新的。

        let mut s = String::from("hello");
    
        let r1 = &s; // no problem
        let r2 = &s; // no problem
        println!("{} and {}", r1, r2);
        // r1 and r2 are no longer used after this point
    
        let r3 = &mut s; // no problem
        println!("{}", r3);
    

      

    在Rust中,编译器保证不会产生Dangling Reference,也即在数据出了作用域之后,reference本身也就不能再使用,否则编译器报错。

    fn main() {
        let reference_to_nothing = dangle();
    }
    
    fn dangle() -> &String {
                         ^ help: consider giving it a 'static lifetime: `&'static`
        let s = String::from("hello");
    
        &s
    }
    

      

    Slice类型

    Slice在传入传出的时候是无需转换Ownership的。Slice使得你能够使用一个集合中的部分元素,而不需要直接传入整个集合。

    String的slice的类型是&str。

    fn first_word(s: &String) -> &str {
        let bytes = s.as_bytes();
    
        for (i, &item) in bytes.iter().enumerate() {
            if item == b' ' {
                return &s[0..i];
            }
        }
    
        &s[..]
    }
    
    fn main() {
        let mut s = String::from("hello world");
    
        let word = first_word(&s);
    
        s.clear(); // error!
    //^^^^^^^^^ mutable borrow occurs here
    
        println!("the first word is: {}", word);
    //                                             ---- immutable borrow later used here
    
    }
    

      

    编译器会保证引用类型-也即这里的tuple,在作用期间保持有效。如果将String s的slice赋给word这个变量,那么在word使用完毕结束生命周期之前,s本身就不能再改动。

    这里要说明的是,对于let s = "Hello world!"这样一句赋值,s也是一个slice指向内存中的这块string literal。所以s的类型就是&str。

    此外,在只读的函数first_word中,与其传入&String,不如传入&str,这样就能够在string slice(和string literal)上进行操作,而String转为string slice不耗费什么时间空间,string slice转化为String却是相反的。

    //fn first_word(s: &String) -> &str {
    //改为
      fn first_word(s: &str) -> &str {
    

      

        let word = first_word(&my_string_literal[..]);
    

      

    其他类型的slice,作为array的一部分,通常类型名称为&[T],例如&[i32]。所有slice的操作和原理都一样,存储第一个元素和其总长。

  • 相关阅读:
    或许你不知道的10条SQL技巧
    windows7下php5.4成功安装imageMagick,及解决php imagick常见错误问题。(phpinfo中显示不出来是因为:1.imagick软件本身、php本身、php扩展三方版本要一致,2.需要把CORE_RL_*.dll多个文件放到/php/目录下面)
    如何循序渐进、有效地学习JavaScript?
    php数组学习记录01
    安装laravel框架
    数据库水平切分的实现原理解析——分库,分表,主从,集群,负载均衡器(转)
    PHP 获取今日、昨日、上周、本月的起始时间戳和结束时间戳的方法
    找回了当年一篇V4L2 linux 摄像头驱动的博客
    Selective Acknowledgment 选项 浅析 2
    Selective Acknowledgment 选项 浅析 1
  • 原文地址:https://www.cnblogs.com/xuesu/p/13861015.html
Copyright © 2020-2023  润新知