Skip to main content
rust 学习

1.1 rust所有权机制

•2 min•237 words
rust认为所有内存都应该有一个所有权概念,因为这个概念我们知道了如何访问内存

所有内存都被所有权机制管理。

Cite

如有疑问参阅 运行时内存结构

变量离开作用域自动释放

变量的作用域,从声明开始到代码块结束。

Todo

随着rust 2018的版本后自动启用 NLL 非词法生命周期,现在在变量最后一次使用之后就会被释放

Rust的设计思路是,当某块内存离开了我们的作用域,rust就会自动释放。

但是什么叫做内存离开了作用域?
内存一般由变量或者说是对象来代表。这些对象代表了这个内存的地址以及它的大小。

所有权转移

内存如果需要多处使用,rust如何做?

rust的所有权限定了内存只有被所有权变量所代表,但是如果有多处栈帧都需要使用这个内存,但是只允许有一个栈帧拥有所有权变量,如何让他们都能操作呢?

变量的本质:固定栈帧中的固定偏移量

变量名只是栈帧中固定偏移量的别名。所以变量不能所谓的转移,只能赋值。

Summary
  1. 变量不能转移,因为变量的完全限定是唯一的
    (比如函数内的变量与调用函数后的接收变量一定不一样)。
    .
  2. 但变量所代表的内存空间可以转移,转移之后原来的变量将不可用。

Rust 直接避免内存拥有多个变量代表,如果你把这个内存代表赋予给其他的变量,或者说你想要创建一个这个内存的新的变量代表,那么原来的我就直接让你作废(语义上应该清空原变量ptr但实际没有,因为编译器已阻隔风险),无法使用。从而避免出现一个内存多个应用变量的场景。

Rust创建了一个术语叫做移动,来指代咱们原来引用变量失效的这种赋值和创建新引用的行为。

常用于拷贝的基本数据交换类型

有些数据类型,常被用于进行数据交换,例如基本数据类型。通常这种类型进行拷贝消耗非常小,并且不需要做控制中心,而是做数据交换。
所以rust为这些类型的变量做了Copy trait的实现,默认赋值操作触发的是Copy而非移动。
.
实现Copy来标记这个类型不需要移动,直接复制。

Copy trait与Drop trait互斥

Copy 的语义:这个类型的值极简,复制就是简单的按位拷贝
Drop 的语义:这个类型拥有特殊资源(如堆内存、文件句柄、锁)。在值离开作用域时,必须执行额外的清理逻辑(释放内存、关闭文件)。
.
如果允许同时实现,大概率因为Copy导致简单拷贝,drop两次直接重复释放。

普通变量想要进行拷贝,可以调用Clone 方法

几乎所有类型都提供了clone()方法,或者提供了Clone函数。可以通过这个方法来获得一个相同数据的全新变量。

Todo
let heap_num: Box<u32> = Box::new(123);
变量的本质:变量只在栈上,堆中没有变量

变量名只是栈帧中固定偏移量的别名。之所以说堆上没有变量,是因为堆上的数据都没有偏移量操作句柄,我们只能通过栈帧上的变量才能寻到堆中的数据位置。

标量类型都实现了Copy特征,复合类型只要所有元素都实现了Copy特征那么自身类型默认实现Copy特征。

Warning

结构体都默认没有copy实现。

Summary
  1. 如果没有实现Drop trait
    元组、结构体中的部分字段,所有权转移到外部。不能再整体使用,但其他未移动的字段依然可以单独使用。
    .
  2. 实现了 Drop
    禁止对任何字段进行部分移动。
    底层逻辑:Drop 要求该结构体在生命周期结束时(离开作用域),必须作为一个整体调用 drop 方法
mut只针对变量,不针对内存,转移所有权之后mut并不转移

所以我们可以将let变量转移所有权给let mut变量。

访问不同于获取,获取即所有权转移

需要严格区分获取与访问,比如 xxx.name.xxx 这个name就被是获取(到局部变量),所有权被转移。
如果只是想要访问,这个时候需要在前面补充&或者在后面补充.as_ref

作用域与赋值

离开作用域通过判断无引用

Drop 的调用是因为所有权持有消失,常见的是离开代码块的变量,另外就是重新赋值让变量的原有所有权消失。

当给“所有权”变量重新赋值的时候,变量中所指向的内存,无人使用,即被认为是离开了作用域。所以会立即调用Drop。

借用变量

借用从而避免所有权转移

所有权转移会导致原变量失效,如果不想频繁失去变量,我们应该借用。

如果某一段内存在一个作用域的多处都有用到。那么在每一处咱们把变量转移进去,又让它转移出来,转移进去又转移出来,过于麻烦。

Rust设计了一种没有所有权的变量。这种变量它不会被认为是需要drop的变量,从而在离开作用域的时候,不调用这个变量的drop。

let mut x = 1;
let x1 = &mut x;
借用类型的变量的本质

其实借用类型的变量就是指针,通常借用又被称为引用,引用分为两种,瘦引用与胖引用。瘦引用只包含地址,胖引用:切片引用、trait object引用还有其他数据。

解引用符

*
引用类型和原类型不一样,&i32不能参与i32的运算,但&str却和String有大量相同的特质trait。如果我们想要原变量相同的操作,可以解引用*,表示返回引用背后指向的数据。

Summary
let (a, ref b) = (12 , String::from("12"));

另外一种 let (a, &b) 这种就是解引用了,表示b是原变量

引用的更多可能:很多结构体都实现了AsRef或者AsMut trait表示取引用

这些方法提供了更智能化的取引用可能,比如你可以实现as_ref()方法给String结果返回的是&str而非&String。

可变控制

不可变引用变量 的 保证可重复读承诺

Rust的设计中所有 immutable 引用变量,有着不变承诺。只要immutable引用变量存在,在刚声明时,还是到引用变量快被释放的任何地方,访问 immutable 引用变量都能得到相同的值。
(所以中间不能插入同源可变引用变量)

Rust中不可变这个含义的承诺是,这个不可变引用变量在任何时间、任意位置、任何时候执行,得到的值都是一样的。

所以,不可变引用所存活的作用域,不允许插入一个可变引用的存在。我们可以看作是一片连续的指令序列都是不可变引用的,然后不可便引用死掉。然后我们可以使用可变引用。

最短的作用域

Rust自动计算不可变引用的存活周期,从第一次出现到最后一次出现的作用域都是它的存活作用域。在这中间不可以使用可变引用以及所有权变量本身也不能修改
这种功能来自 NLL(非词法作用域生命周期)的行为

NLL失效场景

match 的所有分支被视为一个整体作用域,编译器保守地假设所有分支都可能执行。
所以match上的表达式变量在整个作用域结束后才会释放。
if let 中的变量也是一样的,再if和else作用域中都不会被提前释放
闭包捕获的引用也是,在闭包变量被drop之前都不会释放内部捕获的引用
方法中有条件返回,某些分支就无法确定到底变量是否会被使用到,所以不释放
…
最好的方法是将所有变量视为作用域结束才会释放,然后通过mem::drop来转移生命周期,来显式结束变量的生命。

同样不允许间接修改!

变动并非完全来自直接修改。还可能来自其他修改的影响。

有一种间接修改,比如说有一个vector,一个可变的一个变长数组。不可变引用了第一个元素。
后面在这个不可变引用的存活周期内,你对它整个vector进行push。
这个虽然说没有直接对这个第一个元素进行一个修改,但是可能导致vector重新扩容分配内存,然后改变原有数据所在内存的地址。

这样的话,我们的不可变引用它就失效了。所以说这种也算是修改。

这些可变方法都必须传入mut才能可变

因此能检测到这些间接修改。

引用变量本身 与 引用变量内部的可变性

let mut a:&String; 这个引用变量是可变变量,可以重新赋值,但是引用内部指向是不可变引用,不能通过这个变量的引用来改变引用指向的内容。
.
所以mut是对引用数据与变量两种形式有作用。

引用承诺

Rust通过生命周期机制,保证了引用生命期间变量的所有权不失效。

引用变量在则原变量在

rust通过这套机制,实现了这样的承诺:
.
1 所有的引用存在,那么内存一定不会被释放,或者说,如果内存已经被释放,那它的引用一定会被编译器检测到错误。
.
2 所有不可变引用,在任何时候,它的值都不会变。

当我们在修改的时候,不会中间插一个别人修改的东西进来。当我们在做修改的时候,从这个修改开始到结束,都只能我们来修改。

当我们在不可变引用存在的时候,作用域中不允许出现可变引用,以及所有权引用的修改操作。

切片引用

Slice 切片:特殊的引用类型

切片(slice)允许你引用集合中一段连续的元素序列,而不用引用整个集合。slice 是一种引用,所以它不拥有所有权。
切片引用类型的标识是&[切片内部类型],中括号是最突出的。

切片只对集合类型有适配。数组,vector等。
字符串实际上是字符集合,其切片引用的类型写作:&str
创建切片引用的语法是这样:

let s = String::from("hello world"); 
let hello = &s[0..5]; 
let world = &s[6..11];

数组切片引用的类型写作:&[i32]

切片类型变量的本质

切片除了地址数据,还需要切片的长度len(代表元素的个数)。rust会自动根据类型判断元素的size,从而通过个数来计算具体的内存偏移。

252

字符串字面量的本质

参阅1 变量与类型 以及运行时内存结构。我们知道字符串字面量存储在运行时内存区的.rodata段,我们使用的只能是它的不可变切片 &'static str

切片不交叉也冲突!

为了统一风格(不允许间接影响),rust并没有直接提供切片不交叉则可以同时修改的设定。但是提供了unsafe 裸指针来实现这个操作,需要自行保证切片不交叉这个条件。