学习rust之所有权的学习
Table of Contents
1. 所有权(ownership)是什么?
在理解所有权之前,我们稍微接触一点硬件知识。
1.1. stack vs heap?
首先的一个问题是栈和堆的的区别。 栈和堆对编程语言的意义,主要在于如何访问、如何修改存储在栈和堆上的向量。 而对于不同的编程语言,其实还是有些不同的,因此我们先介绍一般风格(称之为C风格),之后给出几个特殊风格的例子。
栈和堆有什么区别?栈相当于一个更简朴、更快速、体积更小、更死板的仓库——就像蜂窝一样,每一个单元都是固定的,因此你不需要记录太多关于栈的信息,仅仅需要记住一个索引就好了。而堆与之相反。堆的空间被认为是很大的,因此访问会慢一些。堆所能存储的东西也是更加灵活的,更加自定义的。
我试图总结一个表格,来说明二者在编程语言上的核心区别。可能会有谬误,仅代表我的理解。
\ | stack | heap |
---|---|---|
所存储变量类型 | 基本类型变量(如float) | 复杂变量类型(class) |
所存储变量赋值新的变量时 | 值复制 | 引用(指针,索引)复制,值不复制 |
接上行,拷贝类型 | 赋值为深拷贝 | 赋值为浅拷贝 |
进出方式 | 先进后出 | 先进先出 |
当然,世界上也有一些比较纯粹的编程语言,可能你定义一个整数,也会按照堆的风格进行处理,或许smalltalk就是这样一种语言。
1.2. 已有的内存管理风格
在理解所有权之前,先来看看目前的编程语言中已有的内存管理方式:
- GC风格(Garbage Collection):即用户不需要关心垃圾的处理过程,解释器会自动地对无用的空间进行回收。
- 精细内存管理风格:我需要手动新建一片内存区域,同时手动清楚他们。
可以看出,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不可能会让你同时拥有两个可以执行写操作的指针。