Rust 语言中的内存管理、borrow、&,ref,mut
刚学习Rust, 觉得Rust在内存管理上很独特, 但是它的概念和语法让人很容混淆; 这里做一下归纳:
Part I Rust 中的变量声明与内存 :
和其它程序一样, Rust 将数据存储在Stack 或 heap中。
Stack(堆栈): 相当于一个先进后出的队列,最近访问的数据在堆栈的最上面,存在堆栈的数据要求是数据的大小在编译的时候是已知的和必须是固定的,所以像字符串这样的的动态类型,实际上并不存储在堆栈上。 对于类型未知,或者长度不固定的数据,通常存储在heap上,指向它的指针因为大小已知并且固定,存储在stack上。
heap(堆): 对于大小未知,长度不固定的数据,通常通过堆来分配。 放在对上的数据,访问速度慢。
所有的计算机语言都需要管理运行时的内存, 像Java、go使用的是garbage collection, 由GC进行内存的分配和回收;像C,c++则需要手动申请和释放内存。 而Rust使用ownership 系统类管理内存。
“managing heap data is why ownership exists can help explain why it works the way it does.”
Ownership包含一些列的规则, 编译器会在编译的时候检查, 这些规则一定要背过:
- Rust 中,每一个值都有一个变量称为它的拥有者。
- 同一时刻,每一个值都仅能有一个拥有者。
- 如果一个值超出了拥有者的存在范围(scope),将自动被drop.
Stack中的变量、ownership、与值交换
所有的基础类型Scalar Type: integer、float、boolean、char; 以及有这些scala type组成的tuple都是存储在Stack里, 他们之间的赋值操作(=)只产生值拷贝,不会发生引用,所以默认满足ownwership中的1和2; 当这些类型的变量超出其scope后,自动的从stack中弹出,所以也自动满足3.
上面的例子, 作为基础类型的x,y只发生值拷贝,并存入stack中,所以能够正常打印。但是s1放生了ownership的转移(s2=s1),所以会报错。
image.png
上面的例子, 元组类型和数组类型是大小固定的,期包含的值也是scalar type时, 发生的仍旧是值copy, 同基本类型一样,满足ownership的规则。
那么怎么判断哪些值会发生值拷贝, rust 中有一个trait: Copy. Copy 能够允许复制存储在stack上的数据,而且仅复制stack上的数据。 所有的基本类型都实现了Copy。 所以可已发生值拷贝的类型满足下面两个条件:
a. 所有数据存储在stack上。
b. 实现了copy trait.
元组类型和数组类型自身是存储在stack上,如果其内容仅包含存储在stack上的类型,那么他们也满足上面的条件。
再来看看枚举类型和struct类型。
对于struct类型,默认是没有实现copy的, 我们做一个测试:
可以看到, 与数组、元组不同, 即使包含的是基本类型,也无法编译通过。
对于struct类型,参考trait copy的描述 ,遵循以下规则:
** " 如果他的所有组件实现了copy, 那么这个类型可以实现copy."**, 枚举类型,遵循与struct相同的规则。
所以上面的例子可以改写为:
image.png
所以,我们按照以上规则实现的struct类型是存储在stack上的,发生的copy是值copy.
相反,如果我们将上面的例子中的y改为string类型,编译器会提醒我们,不能应用copy trait。
image.png
更多关于Copy的内容可以参考trait copy的描述。
heap中的数据、ownership
对于存在stack中的值,做赋值操作时(=),编译器会发生值拷贝,所以新旧变量各自拥有自己的数据,互相不影响。但是对于数据存储在heap中的变量,在stack中,变量通常是已指针方式表示的,这个指针指向数据在Heap中的地址。 所以赋值操作可以有两种不同的方式。已字符串举例,
方式一:
let s1=String::from("hello");
let s2=s1;
在方式1中, 通过赋值,s1与s2共同拥有heap中的数据,都是ownership,如果s2和s1拥有不同的scope,假设s2先退出,s2退出后,会释放掉自己和heap的数据。这时,s1就会出现指针异常。
消除这种异常有多重做法,在Rust中,根据Ownership的规则,就是要求s1 和 s2 不同同时拥有数据。也就是说,执行赋值语句后(s2=s1), s2将变为onwership, 而s1将放弃权利,不能再访问s1指向的值。 image.png image.png
如上图,编译器报错, 提示s1的值已经发生转移,不在允许访问s1.
方法二:
let s1=String::from("hello");
let s2=s1;
当发生赋值操作后, 也可以如下图这般,s1和s2指向不同的heap中的数据, 也就是说不仅指针发生了copy,heap的数据也发生了copy, 这种copy通常叫做深拷贝。 通过深拷贝,s1,s2具有不同数据的ownership,所以不会发生冲突。
image.png
但是,这种深拷贝需要更多的资源消耗,在rust中,系统不会自动通过深拷贝数据。 要实现这个方法,需要用户自己实现std::clone trait. String类型实现了clone, 上面的代码需要改写为。
let s = String::new(); // String type implements Clone
let copy = s.clone(); // so we can clone it
Part II 数据引用与租借 :
对于存储在heap的数据, 如果发生赋值操作,将发生ownership的转移。
通常我们在写程序的时候, 将a的值传给b后,通常我们仍旧需要使用a, 特别是使用函数的情况下。我们对上面的代码做个修改:
可以看到,当我们直接将s1付给fn的参数s2后,s1就无法再用了。
这种情况下,紧紧使用rust的赋值操作传递ownership就不够方便了(也可要使用完后再传回去)。所以,rust 引进了引用(&)的操作类型。&允许你引用某个值,但是不具有这个值的ownership.
image.png
如上图,s 通过指向s1的指针引用字符串(s1的值), 但是,s因为不具有ownership,所以推出scope时,并不释放s1的值。现在对上面的代码做一下修改,是用引用方式,就可以正常工作了。
image.png
在上图的例子中,s获得了s1的值得引用,通常就称为s租借了s1的引用。所以当s使用完租借的引用后,就自动归还这个引用。
在Rust中,引用通常有2中, 只读引用和可读写引用。
只读引用只能读取数据,不能修改数据,如上面的string的例子, 用&操作符表示。
可读写引用,可以修改值, 但是可读写引用要求可声明的变量也是可读写的;rust中默认只是用Let声明的变量都是只读变量,要声明一个可读写变量需要增加mut (表示mutable的意思); 对于mut变量的引用使用&mut。
image.png
除了&和&mut 写法的区别, rust 同时增加了对只读引用(&)和读写引用(&mut)的限制:
每一个变量只能有一个可读写变量;但可以有多个只读变量
每一个变量只能同一时间拥有一个可读写变量或者多个只读变量 (同一scope &和&mut 不同同时出现在一个变量身上)
一个变量的引用必须总是有效的
最后一个规则隐含的意思,reference的生命周期总应该短于变量自身(owner)的声明周期。编译器将负责检查。
引用(&) 和 解引用(*)
引用和解引用是一对互反操作。引用代表的是某个被别的变量所有的值得借用; 而解引用是访问被引用的值得操作。
虽然通过引用借用的值需要解引用来操作,但是对大多数的操作场景,解引用操作是隐含被执行的,在rust中成为 Deref` coercion. 所以我们可以像使用原始值一样使用引用。
可以看到,在表达式中使用five_refer(&T类型)和*five_refer(Integer类型)得到的值是一样的。 同样,引用可以被直接用来属性访问、方法调用、index等多数场景,编译器会自动进行解引用。
但是要注意的是,值得引用(&T)和值得拥有者(T)是两种不同的类型, 不能直接用于类型比较。
image.png
& 和 ref
引用表示的是针对某个被其它变量拥有的值的一次借用。通常有两种方式获得:
a. 使用&,&mut 运算符。
b. 使用ref, ref mut 模式。
个人的理解, &首先是类型的一种(reference type ),然后是运算符(获取引用 operation ),所以可以声明变量类型,也可以用于表达式。而ref 则是作为关键字存在,用于模式说明,但不能用于表达式。
他们在声明变量引用时,&和ref起相同的作用,但是由于上述的区别,写法是不一样:
如上图,在声明时,合在用于表达式时,两者的相同处和区别。
除此之外, 由于ref不能用于表达式,他们在匹配绑定(pattern binding)的时候也表现的不同:
a, ref 用于指明在模式绑定中使用引用的方式,并不影响模式绑定的结果,ref不能算为绑定的一部分。例如, Foo(ref foo) 和 Foo(foo) 会匹配相同的对象.
b, & 用于指明你需要一个引用类型而不是引用的对象本身,作为运算符,它属于表达式的一部分。所以,&Foo和Foo则匹配的是不同的对象。
下图是rust网站的ref说明文档给出的一个例子, 可以看到,增加ref的写法不会影响匹配结果。
现在如果我们使用&引用,就会报错,会影响到匹配模式本身,如下图:
image.png
由于ref 不影响表达式, 在结构解析的时候( destructuring),可以用于获取引用。
如上图, 使用ref 表示ref_to_x获取的是x的引用, 不会造成解析表达式错误。 但是如果替换成 &, 将会编译报错。
image.png
ref 和 mut 联合使用已可以获得mut类型的引用,这里也不能替换成&。编译器会报类型不匹配错误。
包含ref的模式匹配的判断方法: 去掉ef看是否表达式匹配
mut a: &T和a: &mut T
另外,经常让人困扰的是还有这两个写法:
mut a: &T : 这个表示声明一个对T的引用a, 且这个a的值(指向T的指针)是可变的。 可以拆分写:
let mut a; //声明一个可变变量
a=&T; //这个变量是对T的引用,且不能修改引用的值
a:&mut T : 这个表示, 声明一个对T的可变引用, 且a本身不能变。可以拆分写为:
let a; //声明一个不可变变量(指针指向的地址不可变)
a=&mut T;//这个变量是对T的借用,且可以修改T的值。
用网上的一个例子:
总结起来好多, 这部分就到这儿吧。
& 和&mut
简单的说,&是一个不可变引用,及不能修改借用的值,&mut是可变引用,及可以修改引用的值。除此之外,还有一些需要注意的事情。
在借用中我们有提到借用的规则:
每一个变量只能有一个可读写变量;但可以有多个只读变量
每一个变量只能同一时间拥有一个可读写变量或者多个只读变量
前面提到过,对于直接存储在stack中的值,变量间直接复制,只发生值拷贝,付之后,新旧变量各自拥有自己的数据,互不影响。
而对于存储在heap中的数据,变量间赋值时会发生ownership的转移。所以上图中的w在赋值后就不可用了。所以要可用应该使用引用。
当两个变量都是引用, 两个变量间赋值是,遵守上面提到的借用规则。由于只读引用(&)可以同时存在多个,所以赋值后,新旧只读引用都可以用(引用自身长度固定,所以是存储在stack中的,赋值后发生值拷贝);但是对于可读写引用(&mut),根据规则只能存在一个,所以赋值后,不会发生值拷贝,而是直接发生move,旧引用将被作废掉,如上图c赋给d后,c不能再被使用。
但是当函数传参的时候,使用&mut 参数并不发生move, 而是会触发隐式重借用:implicitly reborrowed, 这个以后再总结。