首页 > 其他分享 >《Beginning Rust From Novice to Professional》---读书随记(借用与生命周期)

《Beginning Rust From Novice to Professional》---读书随记(借用与生命周期)

时间:2022-11-28 20:35:11浏览次数:40  
标签:mut 对象 i32 Novice --- let result 借用 随记

Beginning Rust From Novice to Professional

Author: Carlo Milanesi

如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的

Chapter 22 Borrowing and Lifetimes

Ownership and Borrowing

之前已经说过所有权的问题,在赋值的过程或者传递参数的过程中,会出现两种语义:移动语义和复制语义,也即是,要么移交(移动)所有权到新的变量手中,要么创建一个与旧的一模一样数据的交给新变量手中,但始终所有权都只属于一个变量。接下来要讲讲引用(借用)的问题

let n = 12;
let ref_to_n = &n;

在第二个语句之后,“ref_to_n”变量拥有一个引用,该引用引用的是由“n”引用的相同数字。

它不能是一种所有权,因为这个数字已经被“n”所拥有,如果它也被这个引用所拥有,它将被销毁两次。所以,像这样的引用永远不拥有一个对象。

“n”和“*ref_to_n”表达式指同一个对象,但只有“n”变量拥有它。“ref_to_n”变量可以访问该对象,但它并不拥有它。这种概念被称为“借用”。我们说“ref_to_n”借用了“n”拥有的相同数字。

Object Lifetimes

需要注意的是scope的概念是在编译时使用的,而不是在运行时。相对的,在运行时中叫做lifetime。在Rust中,对象的生命周期是在创建它的指令执行和破坏它的指令执行之间的指令执行序列。在此时间间隔内,该对象被称为“活着”。

当然,在范围和生命周期之间存在着一种关系,但它们并不是同一个概念。例如:

let a;
a = 12;
print!("{}", a);

一般来说,一个变量的作用域在声明该变量时开始,而一个对象的生命周期在该对象收到一个值时开始。变量范围的结束也不总是与其拥有对象的生命周期结束的点一致。

let mut a = "Hello".to_string();
let mut b = a;
print!("{}, ", b);
a = "world".to_string();
print!("{}!", a);
b = a;

第一句:变量a声明并初始化,a的作用域和生命周期开始
第二句:变量b声明并初始化,a移动到b,b的作用域开始,但是a的作用域暂停了,因为它变得不可访问了,但是b所拥有的对象并没有创建,因为它拥有的是a创建的
第四句:a被赋值了一个新的对象,然后a恢复了作用域,然后一个新的对象创建,所以它的生命周期开始,这里更像是a的初始化,因为它之前的对象移动了
第六句:a再次移动到b,所以a的作用域再次暂停,b接收了来自a的新对象,然后旧对象会在这里被销毁,因为没有任何变量拥有它,所以它的生命周期结束了
随着程序结束,先是b再到a,它们的作用域先后结束,然后是b拥有的那个对象,生命周期结束了,而a没有拥有任何对象,所以没有任何事情发生

从上面的分析可以看出来,scope作用域是指的变量(标识符)的范围,而lifetime指的是真正拥有值的那个对象的范围。因为对象是只存在与运行时,而变量是编译时已知的,所以才有了之前提及的,scope是在编译时的概念,lifetime是运行时的概念

Errors Regarding Borrowing

let ref_to_n;
{
    let n = 12;
    ref_to_n = &n;
    print!("{} ", *ref_to_n);
}
print!("{}", *ref_to_n);

ref_to_n声明,但是没有初始化,然后下一个范围内,声明并初始化n,然后是ref_to_n借用n拥有的对象,当超过这个范围之后,n的作用域结束,它拥有的对象生命周期结束,然后在范围外,ref_to_n再次使用了这个借用,所以编译器会报错"n does not live long enough",这个叫做use after drop错误

let mut v = vec![12];
let ref_to_first = &v[0];
v.push(13);
print!("{}", ref_to_first);

上面程序编译器报错"cannot borrow v as mutable because it is also borrowed as immutable",错误叫做use after change by an alias

此错误是由于向集合中插入项或删除项会“无效”对集合的所有引用造成的。一般来说,此错误属于更广泛的错误类别,其中数据结构可以通过多个路径或别名访问,当使用一个别名更改数据结构时,它不能被另一个别名正确使用。

How to Prevent “Use After Drop” Errors

struct X(char);
impl Drop for X {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}
let _a = X('a');
let _b;
let _c = X('c');
_b = X('b');

在Rust中,变量释放的顺序是声明的反方向,而不是初始化顺序的反方向

所以,为了避免使用被删除的对象,所有需要借用另一个变量拥有的对象的变量都必须在该变量之后声明

How to Prevent “Use After Change by an Alias” Errors

首先,它需要考虑任何读取对象而不写它的语句,就像对该对象的临时不可变借用,以及任何更改对象的语句,比如对该对象的临时可变借用。

然后,需要记住,任何时候对一个对象的引用并分配给一个变量,借用就开始了;借用在这个变量范围的末端结束

let a = 12;
let mut b = 13;
print!("{} ", a);

{
    let c = &a;
    let d = &mut b;
    print!("{} {} ", c, d);
}

b += 1;
print!("{}", b);

那么,规则很简单,任何对象,在代码的任何一点上,都不能同时有一个可变的借用和其他一些借用。

具体就是:

  • 没有借用
  • 只有一个可变借用
  • 只有一个不可变借用
  • 多个不可变借用

Listing the Possible Cases of Multiple Borrowings

六种允许的情况:

// 多个不可变借用
let a = 12;
let _b = &a;
let _c = &a;
// 一个可变借用
let mut a = 12;
let _b = &a;
print!("{}", a);
// 在一个可变结束后,是一个不可变借用
let mut a = 12;
a = 13;
let _b = &a;
// 在一个可变结束后,是一个可变的借用
let mut a = 12;
a = 13;
let _b = &mut a;
// 在一个不可变借用结束,是一个不可变借用
let mut a = 12;
print!("{}", a);
let _b = &a;
// 在一个不可变借用结束后,是一个可变借用
let mut a = 12;
print!("{}", a);
let _b = &mut a;

然后是六种错误的用法:

// 在一个可变借用还没结束的时候,又借用了一个不可变借用
let mut a = 12;
let _b = &mut a;
let _c = &a;
// 在一个不可变借用没结束的时候,又借用了一个可变借用
let mut a = 12;
let _b = &a;
let _c = &mut a;
// 在一个可变借用没结束的时候,又借用了一个可变借用
let mut a = 12;
let _b = &mut a;
let _c = &mut a;
// 在一个不可变借用没结束的时候,又临时借用了一个可变借用
let mut a = 12;
let _b = &a;
a = 13;
// 在一个可变借用没结束的时候,又临时借用了一个可变借用
let mut a = 12;
let _b = &mut a;
a = 13;
// 在一个可变借用没结束的时候,又临时借用了一个不可变借用
let mut a = 12;
let _b = &mut a;
print!("{}", a);

上面总结一下,就是在同一时刻,只能存在无数个不可变借用,或者一个可变借用

Using a Block to Restrict Borrowing Scope

let mut a = 12;
{
    let b = &mut a;
    *b += 1;
}
let c = &mut a;
*c += 2;

b变量被限制在一个块里面,所以在超出这个块之后,b的作用域就结束了,所以c可以继续借用,其实这个块和一个函数是等价的

let mut a = 12;
fn f(b: &mut i32) {
    *b += 1;
}
f(&mut a);
let c = &mut a;
*c += 2;

The Need of Lifetime Specifiers for Returned References

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    result = {
        let _x1: &Vec<u8> = &v1;
        let _x2: &Vec<u8> = &v2;
        _x1
    }
}
print!("{:?}", *result);

再次强调,保证不出现use after drop错误,借用变量的声明应该在拥有者变量的后面

所以上面的代码是正确的,因为result在v1的后面声明,但如果代码改成了下面的样子

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    result = {
        let _x1: &Vec<u8> = &v1;
        let _x2: &Vec<u8> = &v2;
        _x2
    }
}
print!("{:?}", *result);

那就会报错了,因为result的声明在v2的前面,所以result使用时不能保证v2有效。然后再次修改代码

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
        _x1
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);
-----------------------------------------
let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
        _x2
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);

如果根据这个关系来检查函数的有效性,那编译器的工作时非常巨大的,因此,与泛型函数类似,返回引用的函数也必须在函数签名处隔离借用检查。如果需要借用-检查任何函数,只考虑函数的签名、主体和内部中任何被调用函数的签名,而不需要考虑这个被调用函数的主体

所以,上面两个代码的编译器错误:"missing lifetime specifier",“lifetime specifier”是函数签名的装饰,它允许借用检查器分别检查该函数的主体以及对该函数的任何调用。

Usage and Meaning of Lifetime Specifiers

fn func(v1: Vec<u32>, v2: &Vec<bool>) {
    let s = "Hello".to_string();
}

分析这个函数的关于的变量的情况

  1. 被函数的参数拥有的对象,例如v1
  2. 被本地变量拥有的对象,例如s
  3. 临时对象"Hello".to_string()表达式
  4. 静态对象,"Hello"
  5. 函数参数拥有的借用,这个对象存在于这个函数执行前

当一个函数如果需要返回引用的时候,首先不能引用函数参数的对象,本地变量的对象或者临时对象,因为当函数结束,所有这些都会销毁,会造成悬空引用

那么剩下可以返回的,就是静态对象,或者是函数参数借用的对象

fn func() -> &str {
    "Hello"
}
fn func(v: &Vec<u8>) -> &u8 {
    &v[3]
}

所以,借用检查器只对返回值中包含的引用感兴趣,这种引用可以有两种:引用静态对象,或者借用作为参数接收的一个对象。

为了在不分析函数主体的情况下完成工作,借用检查器需要知道哪些返回的引用引用静态对象,哪些引用一个对象作为参数;在第二种情况下,如果有几个对象作为参数接收,其中哪一个被任何非静态返回引用借用。

trait Tr {
    fn f(flag: bool, b: &i32, c: (char, &i32)) -> (&i32, f64, &i32);
}

这个函数的签名是非法的,这个函数的入参有两个引用,同样的,在返回值中有两个引用返回,返回值的引用,可以引用静态对象,也可以引用b,也可以是c,所以返回值的引用对象不确定,需要手动标注,所以就有了lifetime specifier

trait Tr {
    fn f<'a>(flag: bool, b: &'a i32, c: (char, &'a i32)) -> (&'a i32, f64, &'static i32);
}

首先,a只是一种声明,与泛型参数类似,为了与泛型参数区分,前面加了',然后泛型参数一般是大写的字母,而生命周期说明符是小写的字母

签名中,出现了三个使用了'a的地方,b,c和第一个返回值,第三个返回值是使用了'static

然后此处'a的含义是,返回值的第一个字段借用了b参数和c参数的第二个字段已经借用的那个对象,因此它的寿命必须少于该对象

然后'staic的含义是,返回值的第三个字段是一个静态对象,因此它可以在任何时间运行

trait Tr {
    fn f<'a>(flag: bool, b: &'a i32, c: (char, &i32)) -> (&'static i32, f64, &'a i32);
}

这样标注也是可以的,不同的是,第三个返回值的只与b参数借用的对象的生命周期有关,而不是b和c,因为c没有标注

trait Tr {
    fn f<'a, 'b, T1, T2>(flag: bool, b: &'a T1, c: (char, &'b i32)) -> (&'b i32, f64, &'a T2);
}

这样也是可以的,这次有两个生命周期声明,第一个返回值与c关联,第三个返回值与b关联。

但是其实含义都没有改变,都是表明,返回值的生命周期小于入参的借用的某个对象

Checking the Validity of Lifetime Specifiers

编译时借用检查器需要做的

  • 检查该函数的签名是否有效,对其本身并对其主体有效。
  • 检查该函数的主体是否有效,并考虑到在主体中被调用的任何函数的签名。

如果函数返回值没有引用,那么借用检查器什么都不做

static FOUR: u8 = 4;
fn f() -> (bool, &'static u8, &'static str, &'static f64) {
    (true, &FOUR, "Hello", &3.14)
}

上面代码是有效的,因为所有的生命周期都是对的

fn f(n: &u8) -> &'static u8 {
    n
}

这个就是错的,因为返回的引用不是引用的静态对象

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'b i32, bool, &'a i32) {
    (y, true, x)
}
let i = 12;
let j = 13;
let r = f(&i, &j);
print!("{} {} {}", *r.0, r.1, *r.2);

这是正确的,第一个返回值与y关联,第三个返回值与x关联,它们代表两种生命周期,没有长短之分,注意的是,x和y的返回位置,不可以互换,因为它们的生命周期不一样

fn f<'a>(n: i32, x: &'a Vec<u8>, y: &Vec<u8>) -> &'a u8 {
    if n == 0 { return &x[0]; }
    if n < 0 { &x[1] } else { &y[2] }
}

y没有标注生命周期,所以是非法的,编译器会报错

Using the Lifetime Specifiers of Invoked Functions

正如我们在前一节开始时所说的,借用检查器的两个任务之一是,在编译一个函数时,检查该函数的主体是否有效,并考虑到在主体中调用的任何函数的签名。

let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func<'a>(_x1: &'a Vec<u8>, _x2: &Vec<u8>) -> &'a Vec<u8> {
        _x1
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);
let v1 = vec![11u8, 22];
let result;
{
    let v2 = vec![33u8];
    fn func<'a>(_x1: &Vec<u8>, _x2: &'a Vec<u8>) -> &'a Vec<u8> {
        _x2
    }
    result = func(&v1, &v2);
}
print!("{:?}", *result);

第一个是合法的,不过第二个是非法的,"v2 does not live long enough"

为什么第二个还是不通过?从函数签名和函数主体去分析(上一节),两个函数都是正确的标注了生命周期的,是合法的

这时候先从main函数开始看第一个函数,当func执行的时候,有v1,result 和v2三个已经声明的变量,然后是v1和v2已经初始化了,然后func的签名说明了,result的值有着和第一个参数一样的生命周期,那就意味着,赋值给result的对象的生命周期必须比v1小,这也是成立的,因为result的声明在v1之后,所以它会比v1早销毁

接着看第二个函数,也是从main开始,func的签名说明,result的值与第二个参数有着一样的生命周期,这就意味着,result的生命周期必须比v2小,但是这很明显不对,因为result的声明在v2之前,也就说,result的销毁比v2要晚

现在让我们解释一下,为什么在上一节的最后一个示例中,只使用一个生命周期说明符不如对f函数使用两个生命周期说明符好。

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'a i32, bool, &'b i32) {
    (x, true, y)
}

let i1 = 12;
let i2;
let j1 = 13;
let j2;
let r = f(&i1, &j1);
i2 = r.0;
j2 = r.2;
print!("{} {} {}", *i2, r.1, *j2);

这是使用两个生命周期说明符的正确函数

fn f<'a>(x: &'a i32, y: &'a i32) -> (&'a i32, bool, &'a i32) {
    (x, true, y)
}

let i1 = 12;
let i2;
let j1 = 13;
let j2;
let r = f(&i1, &j1);
i2 = r.0;
j2 = r.2;
print!("{} {} {}", *i2, r.1, *j2);

接着是只使用了一个说明符的函数,这时候就出错了"j1 does not live long enough"

两个函数的内部和签名都是对的,这时候从函数被调用处分析,也就是外部环境与函数的签名的角度进行分析

首先第一个正确版本,第一个返回值与第一个参数关联,衍生下去,也就是i2与i1关联,i2必须比i1的生命周期小,然后显然是对的,i2的声明在i1之后;以此类推,j2必须比j1的生命周期小,显然也是对的,j2的声明在j1之后

接着是第二个错误版本,由于只有一个说明符,也就是说,i2和j2必须要比i1和j1小,然后实际是,i2的声明在j1的前面,所以,j1不符合条件,所以出现了上面的错误,j1的生命周期不够长

标签:mut,对象,i32,Novice,---,let,result,借用,随记
From: https://www.cnblogs.com/huangwenhao1024/p/16933520.html

相关文章