使用`let`绑定来增加值的生命周期

24

我编写了以下代码从 stdin 读取一个整数数组:

use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let xs: Vec<i32> = line.unwrap()
            .trim()
            .split(' ')
            .map(|s| s.parse().unwrap())
            .collect();

        println!("{:?}", xs);
    }
}

这个方法很好用,但是我觉得 let xs 这一行有点长,所以我把它分成了两段:

use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let ss = line.unwrap().trim().split(' ');
        let xs: Vec<i32> = ss.map(|s| s.parse().unwrap()).collect();

        println!("{:?}", xs);
    }
}

这行不通!Rust 给出了以下错误:

error[E0597]: borrowed value does not live long enough
  --> src/main.rs:6:18
   |
6  |         let ss = line.unwrap().trim().split(' ');
   |                  ^^^^^^^^^^^^^                  - temporary value dropped here while still borrowed
   |                  |
   |                  temporary value does not live long enough
...
10 |     }
   |     - temporary value needs to live until here
   |
   = note: consider using a `let` binding to increase its lifetime

这让我感到困惑。是line还是ss生命周期不足?我该如何使用let绑定来增加它们的生命周期?我以为我已经在使用let了?

我已经阅读了生命周期指南,但仍然无法完全理解。有人能给我一个提示吗?

2个回答

26
在您的第二个版本中,ss的类型是Split<'a, char>。类型中的生命周期参数告诉我们该对象包含一个引用。为了使赋值有效,引用必须指向该语句之后存在的对象。然而,unwrap()会消耗line,换句话说,它会将Ok变体的数据从Result对象中移出。因此,引用不指向原始的line,而是指向一个临时对象。
在您的第一个版本中,通过对map的调用,在长表达式的末尾消耗了该临时对象。要修复第二个版本,您需要绑定unwrap()的结果以保持值的存活时间足够长:
use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let line = line.unwrap();
        let ss = line.trim().split(' ');
        let xs: Vec<i32> = ss.map(|s| s.parse().unwrap()).collect();

        println!("{:?}", xs);
    }
}

只是为了确保我理解你的意思,CharSplits 引用了 line 中的值。但实际上它引用了从 Ok 中取出的一个副本,该副本在 let ss 行结束时被丢弃。为什么不直接将所有值保持活动状态直到块的末尾呢? - Thomas Ahle
是的。临时值仅在生成该值的表达式所在的语句中有效。 - Francis Gagné
3
注意,这里有两个名为line的变量:第一个是IoResult<String>(等同于Result<String, IoError>),第二个是Stringunwrap()StringIoResult移出,之后IoResult就无法再使用了(这也是我在重复使用名称line的原因:无论如何你都不能再使用第一个 line)。String根本没有被复制。 - Francis Gagné
非常有趣。这样的移动是如何完成的?数据是被复制并且原始数据被销毁了吗?还是字符串在堆上,而原始指针被销毁了? - Thomas Ahle
2
一个 String 结构体包含一个 Vec,其中包含指向数据(存储在堆上)的指针、长度和容量。当发生移动时,结构体的成员被复制,但引用的数据不会被复制,也不会运行析构函数。原始副本随后由编译器变得无法使用(如果尝试使用该值,则会出现错误),因此您不会访问已转移所有权的数据。 - Francis Gagné

7

这篇内容涉及到 unwrap() 函数调用,它能获取容器中的对象,但是这个引用应该比容器对象更长久地存在,因为在下一行中容器对象就会失效(没有局部绑定关联到它)。

如果你想要更简洁的代码实现,一个非常常见的做法是:

use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let xs: Vec<i32> = line.unwrap()
            .trim()
            .split(' ')
            .map(|s| s.parse().unwrap())
            .collect();

        println!("{:?}", xs);
    }
}

如果没有,您可以创建到“未包装”结果的绑定并使用它。

所以你的意思是 linelet xs 行超出了作用域,但在 let ss 行没有超出作用域,尽管它们在同一个块中? - Thomas Ahle
1
line.unwrap() 的结果超出了范围。 - snf
谢谢,我觉得我开始明白了。 - Thomas Ahle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接