4.Rust所有权

这期我们将带来所有权(Ownership)的相关知识,所有权是Rust很重要的一个概念,必须好好掌握哟!

一、内存安全

对于C/C++程序员来说,可能一直在跟内存安全打交道,这对我们来说也是一个不可避免的问题,我在面试过程中,90%的面试官都对这个问题深入地提了问题。内存泄漏呀、智能指针呀什么的,如果有人感兴趣的话可以专门针对C++中的内存安全问题专门写一期文章,后台留言告诉我哈。

对于一些别的语言来说,会有垃圾回收(garbage collector)机制。

上面两种方式各有优缺点。

Rust则是通过所有权和借用来保证内存安全。很多人不理解为啥说Rust是内存安全的,其实就是在默认情况下,你是写不出内存不安全的代码的。

二、堆和栈

对于系统编程语言来说,这是傻子都知道的东西,简单介绍一下C++中的堆和栈

堆:由程序员手动分配和释放,完全不同于数据结构中的堆,分配方式类似链表。由malloc或者new来分配,free和delete来释放。若程序员不释放,程序结束时由系统释放
栈:由编译器自动分配和释放的,存放函数的参数值、局部变量的值等。操作方式类似数据结构中的栈

这就是C++相比于垃圾回收机制语言的优势,灵活高效。但是也会带来内存安全问题,虽然智能指针通过引用计数的方式避免了很多问题,但是这是最优的吗?

注:我个人建议所有C++程序员使用智能指针,如果你嫌弃stl的那一套,你也可以自己造。

三、Rust中的所有权

弄一段英格利息:

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.

翻译一下:

每一个值都有一个变量,这个变量就是它的所有者。
每一个值在同一时间只能有一个所有者。
作用域结束时,值就会被销毁
fn main(){
    {
        let str = String::from("shuai");
    }
    println!("{}", str);
}

编译出错:

error[E0423]: expected value, found builtin type `str`
 --> src\main.rs:6:20
  |
6 |     println!("{}", str);
  |                    ^^^ not a value

这个应该还是很好理解的。

我们在大括号(作用域)内声明了变量str,然后用String对str进行初始化,str就成了这个字符串的所有者。当作用域结束时,str被析构,它所管理的内存被释放。

我们一般把这个变量从出生到死亡的整个阶段称为它的“生命周期”。

fn main(){
    let str = String::from("shuai");
    let str2 = str;
    println!("{}", str);
}

发现编译错误:

error[E0382]: borrow of moved value: `str`
 --> src\main.rs:4:20
  |
2 |     let str = String::from("shuai");
  |         --- move occurs because `str` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let str2 = str;
  |                --- value moved here
4 |     println!("{}", str);
  |                    ^^^ value borrowed here after move

咦,woc,咋还报错了呢,这就是我们上期说复制语义时,涉及到所有权的问题。move occurs because str has type std::string::String, which does not implement the Copy trait。这个又涉及到一个新的知识点trai,这里我们先不细究,总之就是默认下,会产生移动语义。

str把这个字符串给了str2了,而Rust同一时间只能有一个所有者,所以现在str2是这个字符串的所有者,str啥也不是!str的生命周期在move的时候就结束了。

c++的赋值其实也是一个比较复杂的事情,以后有机会也可以专门写一期。。。

#include <iostream>

int main()
{
	std::string str = "shuai";
    std::string str2 = str;
    std::cout << str << std::endl;
    std::cout << str2 << std::endl;
    return 0;
}

运行结果:

shuai
shuai

这里在str2=str时,调用拷贝构造函数复制出一个新的字符串,内存空间和原来的是不同的。

在Rust中模拟这一行为:

fn main(){
    let str = String::from("shuai");
    let str2 = str.clone();
    println!("str: {}", str);
    println!("str2: {}", str2);
}

运行结果:

str: shuai
str2: shuai
四、移动语义

上面已经讲过了,默认情况下的赋值语句会导致移动语义,即“所有权转移”

在C++中我们知道,函数调用也会产生一系列拷贝构造之类的问题。const引用可以避免不必要的拷贝什么的。那Rust中的函数调用会不会存在类似的问题呢。

fn main(){
    let str = createAstring_fromFn();
    println!("str: {}", str);
    consumeAstring(str);
}

fn createAstring_fromFn() -> String {
    let str = String::from("I am a string");
    return str;
}

fn consumeAstring(str : String) {
    println!("consume: {}", str);
}

运行结果:

str: I am a string
consume: I am a string

我们来捋一下这个过程:

首先、main函数调用createAstring_fromFn函数,这个函数创建了一个字符串,所有者是局部变量str。然后通过return语句将str移动到函数外面。main函数里的str变量接收了这个字符串。
然后能够正常打印str
然后调用consumeAstring函数,通过函数参数调用,将str转移到函数内部,调用完后,并没有将str转移处理,此时,str的生命周期也就结束了。

也就是说,Rust中的变量绑定操作,默认是移动语义,一旦被新的变量绑定后,原理的变量就不能再被使用了!

而C++中就允许赋值构造函数、运算符重载,因此具体会发生什么情况,取决于程序员如何实现重载。

Rust就是让我们必须明确的指出来,你如果是复制,你得显示地告诉我!

注:语义不等于最终的执行情况。编译器很有可能去做优化,但是并不影响我们通过语义理解。

五、复制语义
fn main() {
    let a = 1;
    let b = a;
    println!("a: {}", a);
}

运行结果:

a: 1

咦,woc,怎么没报错!

这是因为Rust对一些简单类型,如整数、bool,赋值默认复制操作。

Rust对这些类型实现了std: :marker: : Copy trait

对于自定义类型来说,这里我们先超前用一个struct,默认是不会实现Copy trait的。

struct haha {
    data : i32
}

impl Copy for haha {}
fn main() {
    let ha = haha { data : 20};
    let hahei = ha;
    println!("{}", ha.data);
}

编译错误:

error[E0277]: the trait bound `haha: std::clone::Clone` is not satisfied
 --> src\main.rs:5:6
  |
5 | impl Copy for haha {}
  |      ^^^^ the trait `std::clone::Clone` is not implemented for `haha`

error: aborting due to previous error

其实是Copy继承了Clone,因此实现Copy trai的同时需要实现Clone trait

struct haha {
    data : i32
}

impl Clone for haha {
    fn clone(&self) -> haha {
        return haha { data : self.data};
    }
}

impl Copy for haha {}
fn main() {
    let ha = haha { data : 20};
    let hahei = ha;
    println!("{}", ha.data);
}

运行结果:

20

这样自定义类型haha也拥有了复制语义。

我们还可以使用#[derive (Copy, Clone ]让编译器帮我们实现Clone trait

#[derive(Copy, Clone)]
struct haha {
    data : i32
}

fn main() {
    let ha = haha { data : 20};
    let hahei = ha;
    println!("{}", ha.data);
}

运行结果:

20

当然,并不是所有数据类型都可以实现Copy trait。对于自定义类型而言,只有所有成员都实现了Copy trait,这个类型才能实现Copy trait。

六、析构函数

这个名词是不是很熟悉?嗯?

RAII手法很舒服,懂的都懂。

在Rust中,不存在构造函数的问题,但是有析构函数的概念。析构函数中不仅可以释放申请的内存,还可以编写逻辑用于管理其他的资源,如文件、锁、套接字等。懂的自然懂。

在Rust实现析构函数需要通过Drop trait

trait Drop {
	fn drop(&mut self);
}
use std::ops::Drop;

struct A {
    data : i32
}

impl Drop for A {
    fn drop(&mut self) {
        println!("destruct fn: {}", self.data);
    }
}

fn main() {
    let a = A { data : 100 };
    println!("enter a scope");
    {
        let aa = A { data : 200 };
        println!("exit scope");
    }
    println!("exit main fn");
}

运行结果:

enter a scope
exit scope
destruct fn: 200
exit main fn
destruct fn: 100

Rust中的析构函数的调用时机和C++比较类似。

use std::ops::Drop;

struct A {
    data : i32
}

impl Drop for A {
    fn drop(&mut self) {
        println!("destruct fn: {}", self.data);
    }
}

fn main() {
    println!("enter a scope");
    {
        let aa = A { data : 200 };
        let bb = A { data : 300 };
        println!("exit scope");
    }
}

运行结果:

enter a scope
exit scope
destruct fn: 300
destruct fn: 200

同一作用域下多个局部变量,先声明后析构,因为局部变量存在“栈”中嘛。

当然,Rust也可以实现RAII手法来进行资源管理。

Rust中允许主动析构:

use std::ops::Drop;

struct A {
    data : i32
}

impl Drop for A {
    fn drop(&mut self) {
        println!("destruct fn: {}", self.data);
    }
}



fn main() {
	let a = A{ data : 100 };
	a.drop();
}

编译报错:

error[E0040]: explicit use of destructor method
  --> src\main.rs:17:7
   |
17 |     a.drop();
   |       ^^^^ explicit destructor calls not allowed

报错了,是的,Rust不允许手动调用析构函数。

但是我们自己想一想,怎么才能让他主动调用析构函数呢,之前有一个consume函数还记得吗?

use std::ops::Drop;
use std::mem::drop;

struct A {
    data : i32
}

impl Drop for A {
    fn drop(&mut self) {
        println!("destruct fn: {}", self.data);
    }
}

fn main() {
    let a = A{ data : 100 };
    drop(a);
}

运行结果:

destruct fn: 100

析构函数提前调用了。

Rust提供了标准库中一个函数 std::mem::drop

# [inline]
pub fn drop<T>( _ x: T) { }

实现和我们的consume是一样的,内部为空,参数值传递。

use std::ops::Drop;
use std::mem::drop;

struct A {
    data : i32
}

impl Drop for A {
    fn drop(&mut self) {
        println!("destruct fn: {}", self.data);
    }
}

fn main() {
    let a = A{ data : 100 };
    drop(a);
}

运行结果:

destruct fn: 100

我们只需要保证移动语义就好了。

对Copy语义的变量调用drop是没有意义的。

use std::mem::drop;

fn main() {
    let a = 1;
    drop(a);
    println!("a after droped: {}", a);
}

运行结果:

a after droped: 1

可以看到,drop无效

前面我们知道了变量遮蔽的概念,那么变量遮蔽是否会导致析构呢。

use std::ops::Drop;
use std::mem::drop;

struct A {
    data : i32
}

impl Drop for A {
    fn drop(&mut self) {
        println!("destruct fn: {}", self.data);
    }
}

fn main() {
    let a = A{ data : 100 };
    let a = A{ data : 200 };
}

运行结果:

destruct fn: 200
destruct fn: 100

shadowing并不代表生命周期结束。

七、借用

borrow

所有权的借用。

借用指针(引用):&和&mut,只读借用和可读写借用。

借用指针只能临时地拥有对这个变量读或者写的权限,并没有对这个变量生命周期管理的义务,也因此借用指针的生命周期不能大于它所引用的变量的生命周期,否则会导致空悬指针。
对于不可变变量,不能有&mut借用
同一作用域内,&型借用可以由多个。如果存在&mut型借用指针,那么就只能有一个借用指针

fn main() {
    let mut str = String::from("I love ");
    println!("original string: {}", str);
    println!("original string len: {}", getlength(&str));
    push_to_string(&mut str);
    println!("new string: {}", str);
    println!("new string len: {}", getlength(&str));
}

fn getlength(str : &String) -> usize {
    str.len()
}

fn push_to_string(str : &mut String) {
    str.push_str("Rust!");
}

运行结果:

original string: I love 
original string len: 7
new string: I love Rust!
new string len: 12
八、slices 切片
fn main() {
    let mut str = String::from("Hello World");
    let fist_word = &str[0..5]; //注意这里是左闭右开
    let second_word = &str[6..11];
    println!("fist_word: {}", fist_word);
    println!("second_word: {}", second_word);
}

运行结果:

fist_word: Hello
second_word: World

本期的内容就到这里,是不是感觉意犹未尽?有什么问题可以在文章下评论哟!
我们现在用到了一些结构体,trait。因此后两期将会介绍这个。
欢迎关注我的微信公众号:Rust编程之路,跟我一起探讨Rust之奥义!
在这里插入图片描述

发布了28 篇原创文章 · 获赞 72 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_23905237/article/details/105320386