天天看点

RUST网络客户端的基本技术说明-所有权和借用

一、资源的管理

在几乎所有的语言中都存在一个根本的问题,那就是资源的管理问题,特别是内存资源和IO资源的管理,尤其是前者,人们对C/C++意见最大的就是指针的随意应用造成的悬垂指针和空指针的问题。而一旦出现这种问题,最终的体现就是整个程序的直接挂掉。特别是悬垂指针引起的程序异常崩溃,由于出现时机的不同以及C/C++对一些内存处理机制的限制(比如数组越界应用没问题但一旦释放就崩溃),导致问题异常的定位非常困难。之后Java异军突起,打得就是垃圾回收机制,这直接从前者嘴中掏走了半边的天空。之后在资源管理上的竞争是愈发激烈,各种语言层出不穷的管理机制也让人大开眼界。包括c/c++也提供了智能指针等初级的内存回收机制,RAII的封装机制等等。但这些都不足以完成普通程序员对资源管理的野望,这时候儿Rust跑出来要分一杯羹了,它提出了所有权和借用机制。

而所有权(Ownership)和借用(Borrowing),其实就是对整个内存资源的生命周期(Lifetimes)的管理。也就是说,这三个概念,是Rust解决内存资源管理的基础世界观。大家不要被生命周期这个名词吓倒,其实就是其它语言中作用域或者说作用范围的意思。

二、所有权

所有权这个字面意思,应该是非常好理解的,但是应用到Rust语言中,这可是初学者必须跨越的第一座大山,真的是一座大山,这也是为什么Rust要想快速普及难的原因之一。所有权其实是也是作用域的一种应用表现。严格来讲,在Rust中没有变量这种说法(这可是官方的说法啊),它只有一个标识符,然后可以通过Binding的机制把资源绑定到这个标识符上。如果是这样说的话,Rust中的Move和Copy(Clone)就非常容易理解了。因为任何标识的绑定资源的改变,一定是二者中的其一。

所有权的Move操作一般是使用let来实现:

{
    let a: String = String::from("xyz");
    let b = a;
    println!("{}", a);
}
           

被Move以后的变量就无法再操作了。所以上面的代码会报一个编译错误。可是看一下如下的代码:

let a: i32 = 100;
    let b = a;
    println!("{}", a);
           

可这段代码跑得却没有问题,啥跟啥?这是因为Rust的另外一个特性,Copy。在Rust,有一些类型默认已经实现了这个特性,它们有i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等。如果想查看更多的相关细节可以查看:

https://doc.rust-lang.org/std/marker/trait.Copy.html(官网)

需要说明的是,Rust中也会有深拷贝和浅拷贝之说。比如字符串之类的引用类型要想实现深拷贝就得用clone来实现。

同样Copy和Move都有高级特性。那么如何自定义实现Copy特性呢:

1、实现derive属性

#[derive(Copy, Clone)]
 struct Foo {
     a: i32,
     b: bool,
 }
           

2、自定义实现

#[derive(Debug)]
 struct Foo {
     a: i32,
     b: bool,
 }
 impl Copy for Foo {}
 impl Clone for Foo {
     fn clone(&self) -> Foo {
         Foo{a: self.a, b: self.b}
     }
 }
 fn main() {
     let x = Foo{ a: 100, b: true};
     let mut y = x;
     y.b = false;
     println!("{:?}", x);  //打印:Foo { a: 100, b: true }
     println!("{:?}", y);  //打印:Foo { a: 100, b: false }
 }
           

同样,Move也有它的高级特性,在前面提到过,通过let实现所有权转移后,如果资源实现了Copy那么仍然可以同时访问;否则资源就会被转移走,无法访问。但Move的更多的用途在于闭包中的使用,这个在前面已经分析过,这里就不再赘述。

三、借用

在Rust中的数据有两种类型,即值类型和引用类型。而引用类型一般对于学习过其它语言的都明白,引用其实就是对同一对象起的别名,但在Rust中引用和借用有什么区别呢?没啥本质的明确的区别,都是使用&符号来实现。若是非要强调二者的不同,就在于,引用的原意只是用,而借用意味着要还。这个时机非常重要,重要到和下面提到的生命周期息息相关。

借用是有要求的:

1、同只有一个可变借用(&mut T)或者0~多个不可变借用(&T)(不允许可变借用)

2、借用在超出作用域后会被回收。

3、可变借用情况下,只允许访问可变借用不允许访问被借用者(所有权)。

fn main() {
    let mut x: Vec<i32> = vec!(1i32, 2, 3);
    //更新数组
    //push中对数组进行了可变借用,并在push函数退出时销毁这个借用
    x.push(10);
    {
        //可变借用1
        let mut y = &mut x;
        y.push(100);
        //可变借用2,注意:此处是对y的借用,不可再对x进行借用,
        //因为y在此时依然存活。
        let z = &mut y;
        z.push(1000);
        println!("{:?}", z); //打印: [1, 2, 3, 10, 100, 1000]
    } //y和z在此处被销毁,并释放借用。
    //访问x正常
    println!("{:?}", x); //打印: [1, 2, 3, 10, 100, 1000]
}
           

需要说明的是,正如实际的现实世界一样,借用者只是临时对资源的引用并不会发生所有者的转移,但在应用中借用者可以操作资源反而所有权者不允许并必须保证在借用过程中资源的安全性(释放或者回收)。不过,如果如果是在非变可租借的情况下,还是允许继续非变借用的。借用完成后,所有者回收操作权限 。

四、生命周期

其实明白了上面的所有权和借用,基本也就弄明白了生命周期这个意义:

//隐式生命周期
fn foo(x: &str) -> &str {
    x
}

//显示生命周期
fn foo<'a>(x: &'a str) -> &'a str {
    x
}
//显示生命周期
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
    if true {
        x
    } else {
        y
    }
}
           

其实说白了就是,生命周期与依赖的所有相关变量的生命周期中最小的一个一致,或者说木桶原理大家就明白了。同样,在结构体中,如果应用到了借用,也必须重视这个问题,看一个例子:

//有问题
struct Person {
    age: &u8,
}
//修改后
struct Person<'a> {
    age: &'a u8,
}
//在实现时也需要有生命周期的标志
impl<'a> Person<'a> {
    fn print_age(&self) {
        println!("Person.age = {}", self.age);
    }
}
           

以上代码均来自《RustPrimer》。

五、总结

Rust的优秀不优秀不是一两个人说了算,也不是一两个小团队说了算,没有调查就没有发言权,不深入学习一下Rust怎么能够真正的来好好讨论一下Rust呢。

努力吧,归来的少年!

RUST网络客户端的基本技术说明-所有权和借用