学习rust之所有权的学习

Table of Contents

1. 所有权(ownership)是什么?

在理解所有权之前,我们稍微接触一点硬件知识。

1.1. stack vs heap?

首先的一个问题是栈和堆的的区别。 栈和堆对编程语言的意义,主要在于如何访问、如何修改存储在栈和堆上的向量。 而对于不同的编程语言,其实还是有些不同的,因此我们先介绍一般风格(称之为C风格),之后给出几个特殊风格的例子。

栈和堆有什么区别?栈相当于一个更简朴、更快速、体积更小、更死板的仓库——就像蜂窝一样,每一个单元都是固定的,因此你不需要记录太多关于栈的信息,仅仅需要记住一个索引就好了。而堆与之相反。堆的空间被认为是很大的,因此访问会慢一些。堆所能存储的东西也是更加灵活的,更加自定义的。

我试图总结一个表格,来说明二者在编程语言上的核心区别。可能会有谬误,仅代表我的理解。

\ stack heap
所存储变量类型 基本类型变量(如float) 复杂变量类型(class)
所存储变量赋值新的变量时 值复制 引用(指针,索引)复制,值不复制
接上行,拷贝类型 赋值为深拷贝 赋值为浅拷贝
进出方式 先进后出 先进先出

当然,世界上也有一些比较纯粹的编程语言,可能你定义一个整数,也会按照堆的风格进行处理,或许smalltalk就是这样一种语言。

1.2. 已有的内存管理风格

在理解所有权之前,先来看看目前的编程语言中已有的内存管理方式:

  1. GC风格(Garbage Collection):即用户不需要关心垃圾的处理过程,解释器会自动地对无用的空间进行回收。
  2. 精细内存管理风格:我需要手动新建一片内存区域,同时手动清楚他们。

可以看出,GC风格和精细的内存管理是互补的:前者使用方便,节省脑力;后者可以在运行时无需花费额外的代价管理内存空间。

而新提出的“所有权”,则有一种“我全都要”的感觉。 当然,世界上不存在我全都要,关于“所有权”的花销在哪里,最后再去考虑,我们先来学习一下什么是所有权。

1.3. 所有权(ownership)是什么?

笔者看来,所有权就是约束了 变量名(也就是指向内存空间的index)变量数值(也就是内存空间所存储的数据) 的一种描述。

我先试一试一句话定义ownership:

对于一个对象而言,其所有权指:对于该对象而言,存在且仅存在一个主人可以访问、修改这个对象。

The ownership for a object is the authority that there exists one and only one owner that can visit this object.

一般而言,所有权规则被定义为下面三条:

  • Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
  • 值在任一时刻有且只有一个所有者。
  • 当所有者(变量)离开作用域,这个值将被丢弃。

我们现在可以停下来,思考一下为什么所有权可以做内存管理了。其实答案比较直白:如果一个值存在堆上,那么每变更一个所有域,我就需要人工交换所有权。如果我忘记了,编译时按照上述规则编译器就会找不到那个变量,从而内存错误会发生在编译时。

当然,这样的一个可能的问题就是:我不能再像过去使用java、python等语言那样设置多个变量名指向一个变量了。比如我有一个初始化函数 object() 可以返回一个在堆上的对象,下面的伪代码语句对rust不适用。

a=object()
b=a
print(a)

我们再看一个类似的伪代码,该代码和变量赋值类似,属于函数赋值:由于我们使用的是一个不知道长度的string(编译时未知)而非一个字面值字符串,所以 s1 会存储在堆上, 由于s1在堆上,所以当其把指针传给函数的参数 a_string 时,s1就不再是owner了,这时如果你试图使用它——就会报错,你只能再对他赋值。

  let s1=String::from("lalalaaaaaaaa");
  let s2=in_and_out(s1);
  println!("{}",s1); // error

fn takes_and_gives_back(a_string: String) -> String { 
    let new_stirng=some_transform(a_string) // some_transform是我瞎写的函数,注意没分号,代表返回之
}

上述情况是较为繁琐的,因为如果我们还想接着使用s1,那么针对以上问题,我们还要接着把函数里的a_string返回回来,即

fn takes_and_gives_back(a_string: String) -> String { 
    let new_stirng=some_transform(a_string);
    (a_string, new_string)
}

这样就太麻烦了。 此处和C++一样,rust给出了引用。

1.4. 不是指向数据,而是指向指针————引用就是一个指向指针的指针

我们来看看引用,一种C++里也用烂了的方式。

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

以上述代码为例,s不再是指向String::from("hello")的指针了。如果我们认为s1指向了堆中的该字符串,那么变量本身则是存储在栈中的一些数据(可以看作是对内存中该数据的meta-data)。如果我们使用&去解引用,那么我们获得的s,并不是和s1一样的一个指向内存中变量空间的指针,而是一个指向了s1的指针。对于这个s,他在函数被使用时建立(即初始化在栈中),在函数结束时销毁(该参数不能被返回,原因后面介绍)。因此,我们可以通过s去操纵内存空间,因为函数自动使用了语法糖,让你觉得你在操纵s1。

rust在此基础上又进了一步,对于上述代码中的s1,你可以使用多个只读的引用,但你不能使用多个具有写权限的引用。——等等,读写权限是什么鬼?

是这样的:rust默认定义的变量不具有写权限(即初始化完,就是最终状态了),如果你想定义一个变量,还可以改变,就要加关键词mut。所以:

// case1: 可以编译通过的版本
let s = String::from("hello");
let r1=& s;
let r2=& s;
println!("{}, {}", r1, r2);

//------编译通过
let mut s = String::from("hello");
let r1 = &mut s;
println!("{}, {}", r1, r1);

//------编译不通过
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

下面给出一个更加变态的代码,看看我学会了没有。

let mut s = String::from("hello");

let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// r1 r2是值传入,而通过作用域println!(),所有权被转到函数里了,所以r1,r2消失。
// 此位置之后 r1 和 r2 不再使用

let r3 = &mut s; // 没问题
println!("{}", r3);

前面我埋了一个坑,即为什么:函数里的引用不能够作为返回值返回。这其实就是编译器为了保证安全设置的一个规则下的一个具体表现。 rust编译器规定:

引用的所有权不能比变量的所有权更早地离开某个作用域.

这是针对悬垂引用问题(Dangling Reference)提出的。该问题是指:堆上的数据已经消失了,栈上存储的指向这些数据的变量名也消失了,但是栈上存储的指向栈上的用户名的指针,也就是我们的引用,还在。—>这就是潜在的危险了。

1.5. 总结

关于ownership的介绍就是这么多。总结下来就是:拥有权独一份,过期就删除;只读的引用拥有权有很多份,过期就删除;可写的引用拥有权有且只有一份。以及:rust不可能会让你同时拥有两个可以执行写操作的指针。


Author: Zi Liang (liangzid@stu.xjtu.edu.cn) Create Date: Wed Nov 17 20:18:15 2021 Last modified: 2024-03-09 Sat 20:56 Creator: Emacs 28.1 (Org mode 9.5.2)