每日一 R「03」Borrow 语义与引用
在上节课中,我们学习了变量的所有权原则。在所有权原则下,值有单一的所有者。所有权原则为 Rust 带来了非常多的好处,例如不需要设计 GC。但是,所有权转移也使得开发者在赋值、函数调用和返回时必须谨慎地处理值的所有权问题,否则将会导致失去所有权而无法访问原来的值。
为了解决所有权的问题,Rust 为所有定长的变量实现了 Copy trait,即当赋值、传参和返回时,使用 Copy 语义自动把值拷贝一份,而不使用 Move 语义;对于非定长类型的变量,没有实现 Copy trait,所以不能使用 Copy 语义复制。Rust 中设计了 Borrow 语义,在不获取变量所有权的情况下,通过“引用”访问变量的值。
01-只读借用/引用
Rust 中借用与引用是同义的,只不过与其他语言中的引用有所不同,所以 Rust 使用了新的概念借用。变量只读借用的语法是&x
,它与 C/C++ 中的取引用语法类似。它对变量的作用我们通过一张图来理解:
图中的变量 y 是 x 的只读引用,s1 是 s 的只读引用。要想访问借用变量的值,可以通过解引用*y
。
在其他语言中,例如 Java,函数调用时传参有传值和传引用的区别。Rust 中没有传引用的概念,传参与赋值一样,遵循变量所有权原则。传参时,对于实现了 Copy trait 的类型来说,Rust 会使用 Copy 语义将数据拷贝一份(浅拷贝)作为函数入参;对于未实现或不能实现 Copy trait 的类型来说,Rust 会使用 Move 语义,将变量所有权转移给函数入参。当函数结束并返回到调用点后,所有权转移到函数内部的变量随着函数的生命周期结束而被回收了,后续便无法再继续访问。
考虑如下的代码:
可以对 sum 方法修改下,将入参改为接收只读引用:
课程中有一张图,很好地解释了这个过程。Rust 中只读引用实现了 Copy trait,所以在调用 sum 方法时,会自动拷贝一份值。所以 sum 方法中 data 同样指向了栈中的胖指针。当 sum 方法结束后,回收的也只是 data 这个引用,而不会影响堆上的数据。
仔细看下上图,如果 data 因为离开作用域,它拥有的堆上数据被回收(所有权原则),那 data1 和 data1’ 这种对 data 的引用岂不是会造成 Rust 极力避免的 use after free 问题吗?
所以,Rust 对变量引用做了严格地限制,即变量引用的生命周期不能超过(outlive)变量本身的生命周期。
02-可变借用/引用
可变借用,顾名思义,是可以改变所引用变量值的引用。变量的可变引用语法是&mut x
。在没有引入可变借用之前,修改值(内存中内容)只能通过拥有所有权的变量进行。引入了可变引用后,相当于同时存在了多个修改窗口,是非常危险的操作。所以,Rust 对可变引用进行了严格的限制:
在一个作用域内,仅允许一个活跃的可变引用。
在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在。
注:什么是活跃的可变引用?我们换个角度来理解,什么是不活跃的可变引用?不活跃是指虽然声明为可变引用,但却未当作可变引用来用。例如下面这个例子:
y 和 z 都是 x 的可变借用,它们的作用域都在 main 方法中,但 Rust 编译器可通过也可正常运行。关于作用域其实还有另外一个说法:作用域从声明的地方开始一直持续到最后一次使用为止。上面的例子中,y 和 x 的作用域并不交叉,所以没问题。如果 1 和 2 两行代码交换位置,编译器就会报错。
对于变量 y 这种引用在作用域 } 之前就不再使用的代码位置,Rust 有一个专门的称呼,叫做 NLL(Non-Lexical Lifetimes)。这是一种编译器优化行为,在比较旧的编译器版本中,可能不可用。
对于引用生命周期限制规则中的第二条,是不是有点熟悉的感觉。仅从描述上看,非常类似于 Java 中的读写锁。其实我们从修改内存的角度看,可变引用和不变引用与读写锁有异曲同工之妙。
今天的课程链接:《08|所有权:值的借用是如何工作的?》
历史文章推荐
版权声明: 本文为 InfoQ 作者【Samson】的原创文章。
原文链接:【http://xie.infoq.cn/article/0a116f9f8043b036c9757fd14】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论