rust:move和borrow

author author     2022-08-10     285

关键词:

感觉Rust官方的学习文档里关于ownship,borrow和lifetime介绍的太简略了,无法真正理解这些语法设计的原因以及如何使用(特别是lifetime)。所以找了一些相关的blog来看,总结一下,以备以后参考。

起因

Rust想要解决的问题是在无GC的情况下安全地管理资源。这点并不容易实现,但不是一点思路都没有。比如,有一个Java程序:

public void foo() {
    byte[] a = new byte[10000000]; 
   a = null;
  byte[] c = new byte[10000];

}

上边的代码有两处位置是我们可以明确告诉编译器可以释放之前分配的两个数组所占的内存的:

  • a = null  此处,之前a指向的数组不能再被访问,所以它的内存可以被回收了
  • foo方法的结尾 }. 此时,数组c的内存可以被释放。

但是,实际情况会比这更复杂。比如,我们可能在foo方法中把a数组传递给foo的本地作用域之外的数组结构,这样, a就不应该在foo的结尾处被释放了。对于Java,在运行时通过GC的方式回收资源是唯一可行的方式,Java的语法并没有提供足够多的线索使得编译器可以知道内存释放的时机。但这样并不是没有好处,因为如果要添加有利于编译器的标记,就只能由程序员来做,这样无疑会降低程序开发的效率。

在Rust语言里,程序员需要思考资源的使用情况,并提供有关的信息给编译器,以使得编译器在编译时检查资源访问的冲突、以及由编译器来决定资源释放的时机。于是,Rust有了下面三个主流语言没有的语法:

  • ownship
  • borrowing
  • lifetime

下面来概述一下为什么需要这三个语法,它们分别负责解决什么问题。

ownship

首先,如果由编译器来决定什么时候资源应该被销毁,那么编译器依据的规则必须是一个很简单的、不由运行时逻辑决定的规则,比如,Reference Counting这种规则是不能在编译时用来检查的。Rust选择通过scope/stack的方式来决定资源的生命周期。当一个变量离开了它的作用域,它所拥有的资源会被释放。但是如果允许一个资源被多个变量拥有,那么编译器就又得通过非常复杂的方式来决定资源释放的时机、甚至不可能做到这点。所以Rust规定任何资源只能在一个所有者(owner)。这样编译器只用检查资源的owner的作用域,就可以决定资源的释放时机。

move

如果资源在被绑定到它的owner以后,这种“所有权”无法转移,会是非常不灵活的。最重要的情况是我们无法由一个函数来决定其参数绑定的资源的释放。所以,需要“move"语法,来转移资源的所有权。

borrow

如果只能通过move才能使得我们通过函数参数或者非owner的其它变量来访问资源,也会有很多不方便之处:

  • 无法共享一个资源给多个对象
  • 在调用一个函数之后,被move给它的参数的资源之前绑定变量就不能再被使用。很多时候,我们并不想这么做,而只是通用一个函数修改/读取这个资源,在此之后,还想继续使用它之前绑定到的变量。

所以,需要有一个语法允许owner把自己的拥有权“借出”,这个语法就是"borrow"。

但是,这种“借出”比move语法要灵活,比如允许多个变量都能引用到一个资源,但这样就面临着读写冲突的问题。所以borrow分了两种:immutable borrow和mutable borrow,并且编译器对于一个作用域里这两种borrow的数量进行限制,从而避免读写的冲突。

borrow实际上创建了到原始资源的reference,它是一种指针。

比较特殊的是mutable borrow,即&mut,它可以把owner绑定到新的资源。在通过mutable borrow改变owner绑定的目标时,会触发owner最初绑定资源的释放。

 

lifetime

如果资源(a)的生命周期比引用(b)的短,即在b失效之前,a已经不能再访问了,那么,编译器应该禁止让b引用a,否则会产生“use after free error”。有时候,a和b的这种关系比较容易编译器发现,比如

let a;
{
   let b = 1;
   a = &b
}
println!("{}",a);

但有时候, 这种关系是编译器发现不了的。比如,a是一个函数的返回值,它的生命周期可能比引用b的要短,也可能是一个常量。编译器不去执行函数的逻辑,就无法确定a的生命周期,因此它就无法判断是否使用b来引用a是安全的。所以,Rust需要一些额外的标记,来告诉编译器什么情况下“reference”是可安全访问的。

实际上,Rust中每个reference的类型可以认为是一个“复合类型”,lifetime是其中的一部分。不过,程序员无法具体地描述一个reference的lifetime,比如,你无法说"a的生命周期是从第5行到第8行”。"lifetime"的值,最初肯定是由编译器写入的。程序员只能通过‘a这种标记来引用已有的lifetime值,来在程序员告诉编译器一些跟lifetime有关的逻辑。

 

ownship

 

Variable bindings have a property in Rust: they ‘have ownership’ of what they’re bound to. This means that when a binding goes out of scope, Rust will free the bound resources. For example:

fn foo() {
    let v = vec![1, 2, 3];
}

重点是当一个变量离开它的作用域时,Rust会释放它所绑定的资源。而这个决定了资源生命周期的变量就是这个资源的owner. 

 

 

Rust会确保一个资源只有一个owner。这个看起来跟读写冲突有关,可以看下The details。而且只有一个owner,编译器显然也更容易确定资源释放的时机。

 

但是,如果这种资源“所有权"不能转移,就会存在很多问题。比如,我们很多情况下想要将资源的所有权交由一个函数处理。

这种逻辑,由Rust的"move"语法来搞定。

Move semantics

 

move的特点就是在move之后,原来的变量就不可用了。因为函数就像是一个黑盒,如果把所有权转交给函数,那么无法确保函数返回后之前的变量还能够使用。

move的这个特点,有两种典型的例子可以展示:

let v = vec![1, 2, 3];

let v2 = v;

println!("v[0] is: {}", v[0]);

这种情况下,把vector的所有权move给v2之后,就不可以再访问v了。所以编译时会报错

error: use of moved value: `v`
println!("v[0] is: {}", v[0]);

第二种是把资源move给函数

fn take(v: Vec<i32>) {
    // what happens here isn’t important.
}

let v = vec![1, 2, 3];

take(v);

println!("v[0] is: {}", v[0]);

也会报跟上面一样的错误。

 

Borrowing

如果只能通过资源所绑定到的变量来访问它,会有很多不方便的地方,比如并行地去读取一个变量的值。而且,如果只想“借用”一下某个变量绑定的资源,在借用完成以后,不想释放这个资源,而是把所有权“交还”给原来的变量,那么用move语法就有些不方便。比如Rust文档里的这个例子:

fn foo(v1: Vec<i32>, v2: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
    // do stuff with v1 and v2

    // hand back ownership, and the result of our function
    (v1, v2, 42)
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let (v1, v2, answer) = foo(v1, v2);

这里,foo函数结束时并不想释放v1、v2变量绑定的资源,而是想继续使用他们。如果只有move语法,就只能用函数返回值的方式返回所有权。

borrow语法,可以使这种情况更简单。但是,它本身也会带来新的复杂性。

上面的例子,用borrow语法,可以这么做:

fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
    // do stuff with v1 and v2

    // return the answer
    42
}

let v1 = vec![1, 2, 3];
let v2 = vec![1, 2, 3];

let answer = foo(&v1, &v2);

// we can use v1 and v2 here!

Instead of taking Vec<i32>s as our arguments, we take a reference: &Vec<i32>. And instead of passing v1 and v2 directly, we pass &v1 and &v2. We call the &T type a ‘reference’, and rather than owning the resource, it borrows ownership. A binding that borrows something does not deallocate the resource when it goes out of scope. This means that after the call to foo(), we can use our original bindings again.

 所以,borrow实际上就是生成了对资源的引用,这种引用的作用域并不和资源的生命周期挂钩,这点和binding有本质的不同。

下面,要明确的就是“borrow"的范围,就是从什么时候开始borrow,到什么时候borrow结束。

看下面的例子:

let mut x = 5;
{
    let y = &mut x; //borrow开始
    *y += 1;
} //borrow结束
println!("{}", x);

之所以要确定borrow的范围,是因为borrow语法有一些跟作用域要关的要求:

  • First, any borrow must last for a scope no greater than that of the owner.
  • Second, you may have one or the other of these two kinds of borrows, but not both at the same time:
    •   one or more references (&T) to a resource,
    •   exactly one mutable reference (&mut T).

 第一,当owner无法访问了,那么borrow一定不能再访问。

 第二,下面两种情况只能存在一种:

  • 对资源的一个或多个不可变的引用(&T)
  • 对资源的唯一一个可变的引用(&mut T), 也就是说不能同时有多个可变引用。

第二个限制,是为了防止读写冲突。特别是一个mutable borrowing,可能会使得对同一个资源的immutable borrowing访问错误的地址,当然也可能会使得其它的mutable borrowing访问错误的地址。

比如:

fn main() {
    let mut x = 5;

    let y = &mut x;    // -+ &mut borrow of x starts here
                       //  |
    *y += 1;           //  |
                       //  |
    println!("{}", x); // -+ - try to borrow x here
}                      // -+ &mut borrow of x ends here
      

上边的这段代码,编译时就会报错:“cannot borrow `x` as immutable because it is also borrowed as mutable"

而第一个限制是很容易理解的,毕竟如果owner都访问不了了,那么reference当然就不能用了。下面是一个例子:

let y: &i32;
{
    let x = 5;
    y = &x;
}

println!("{}", y);

在x的作用域结束后,对它的borrow y还可以访问,所以,以上的代码不会通过编译。

 

参考文档

The Rust Programming Language

Explore the ownership system in Rust

Rust Borrow and Lifetimes

Lifetime Parameters in Rust

Rust Lifetimes

uva230borrowers(stl行读入的处理重载小于号)

...名字升序排列,再按标题升序排列,然后会有3种指令,BORROW,RETURN,SHELVE。BORROW和RETURN都会带有一个书名在后面,如:BORROW"TheCanterburyTales"RETURN"TheCanterburyTales" 当遇到SHELVE指令,输出已还但没上架的书排序后(规则同上)依次插入... 查看详情

习题5_8图书管理系统(borrowers,acm/icpcworldfinals1994,uva230)(代码片段)

...不相同,以END结束),然后是若干指令:BORROW指令表示借书,RETURN指令表示还书,SHELVE指令表示把所有已归还但还未上架的图书排序后一次插入书架并输出图书标题和插入位置࿰ 查看详情

正则表达式re

#正则正则式针对字符串的操作importres=‘"mobilephone":"$borrow_user","pwd":"$borrow_pwd"‘d="mobilephone":"18511295864","pwd":"123456"p=‘$(.*?)‘#查找一个m=re.search(p,s)print(m)#<re.Matchobject;span=(17,31),match=‘$borrow_user‘>g=m[1]print(g)#borrow_user#查找所有m... 查看详情

uva230borrowers(代码片段)

思路:用结构体book存作者的名字和状态,vector存所有书的名字,利用map<string,book>books(string是书名)联系书名和作者、状态;存下来后按要求排序,输入命令进行操作,比如SHELVE操作:从前往后找还了没有上架的书,然后... 查看详情

问号在类型参数绑定中是啥意思?

...发布时间】:2015-05-1918:40:41【问题描述】:我找到了std::borrow::BorrowMut的定义:pubtraitBorrowMut<Borrowed>:Borrow<Borrowed>whereBorrowed: 查看详情

除了查询遇到问题

...】:2015-10-0513:59:34【问题描述】:我有两张桌子(Books和Borrowed)。我想从表Books中全选。但首先它将检查BookID是否存在于表Borrowed中,如果它确实存在并且它的Status=1,它将不会包含在select*语句中。我试过了,但是不行select*fromBo... 查看详情

2.5references&borrowing(代码片段)

    Hereishowyouwoulddefineandusea calculate_length functionthathasareferencetoanobjectasaparameterinsteadoftakingownershipofthevalue:[[email protected]test]#cargonewreferencesCreatedbinary(application)`references`package[[email protected]test]#cdreferences/[[... 查看详情

如何选择借阅次数最多的书种

...阅次数最多的书种【英文标题】:Howtoselectabookgenrewhichareborrowedthemost【发布时间】:2021-01-1717:56:01【问题描述】:我有一个叫books的表,我要选择一个借阅次数最多的book_genre(times_borrowed):SELECTbook_genreFROMbooksWHEREtimes_borrowed=(SELECTMA... 查看详情

Spring data JPA nativeQuery order by 无效

】SpringdataJPAnativeQueryorderby无效【英文标题】:SpringdataJPAnativeQueryorderbyisinvalid【发布时间】:2019-01-2711:19:02【问题描述】:SpringDataJpa方法如下:@Query("selectpb.id,pp.max_borrow_amt,pp.min_borrow_amtfromproduct_loan_basicpbleftjoinpr 查看详情

uml简单例子

...我举个融投资的用例,有2种角色:投资人investor和借款人borrower。类图主要用来描述实体Entity之间的关系。类图包含3个部分:类名、属性、方 查看详情

java计算时实现数字计算结果高精度

...位c[i]=c[i]-10000;carry=1;returnc;publicstaticint[]sub(int[]a,int[]b)intborrow=0;int[]c=newint[a.length];for(inti=a.length-1;i>=0;i--)c[i]=a[i]-b[i]-borrow;if(c[i]>=0)borrow=0;else//借位c[i]=c[i]+10000;borrow=1;returnc;publicstaticint[]mul(int[]a,intb)//b为乘数intcarry=0;int[]c=newint[a.... 查看详情

oracle的内部连接

...不一致:假设您有两个关系loan(loan_number,branch_name,amount)和borrower(customer_name,loan_number)。loan_number是两个表共有的属性。现在,Oracle为您提供了两种 查看详情

生成一份报告,其中包含当前有逾期图书的借款人的详细信息

...借款人的详细信息【英文标题】:Produceareportwiththedetailsofborrowerswhocurrentlyhavebooksoverdue【发布时间】:2016-03-1509:35:10【问题描述】:我很感激你在这里为我的学习做出贡献。我目前在数据库课程中有一个分配。我真正的问题是理... 查看详情

json嵌套数组-第11讲

...      "name":"reader2",            "borrow_book_id":   [169]        ,            "ID": 198,            "name":"reader1",            "borrow_book_id":   [166,169]       ... 查看详情

json嵌套数组-第11讲

...      "name":"reader2",            "borrow_book_id":   [169]        ,            "ID": 198,            "name":"reader1",            "borrow_book_id":   [166,169]       ... 查看详情

rust语言圣经11-引用与借用(代码片段)

原文链接:https://course.rs/basic/ownership/borrowing.html 欢迎大家加入Rust编程学院,中国最好的Rust学习社区官网:https://college.rsQQ群:1009730433引用与借用上节中提到,如果仅仅是所有权转移,会让程序变得复杂&#... 查看详情

defi借贷协议是什么-收益计算

...以去中心化方式连接借方(lenders)和贷方(borrowers)的平台。一方面,它允许贷方从平台借贷数字加密货币并支付利息,另一方面它允许储户向平台存入数字加密货币以赚取利息。与银行的存款账户相比De... 查看详情

这个例子是不是违反了星型模式?

...型模式,其中包含两个基于业务实体的维度:dim_loan和dim_borrower。还有一些事实表,例如fact_loan_status,每个月有一行显示当时的余额,并有一个FK回dim_loan。 查看详情