在涉及线和地图的情况下对所有权感到困惑

Confused about ownership in situations involving lines and map

fn problem() -> Vec<&'static str> {
    let my_string = String::from("First Line\nSecond Line");
    my_string.lines().collect()
}

编译错误失败:

  |
7 |     my_string.lines().collect()
  |     ---------^^^^^^^^^^^^^^^^^^
  |     |
  |     returns a value referencing data owned by the current function
  |     `my_string` is borrowed here

我明白这个错误的意思 - 它是为了阻止您返回对超出范围的值的引用。查看了所涉及函数的类型签名后,问题似乎出在 lines 方法上,该方法借用了调用它的字符串。但为什么这很重要?我正在迭代字符串的行以获得部分的向量,我返回的是这个“新”向量,而不是任何会(非法)直接引用 my_string.

(我知道我可以通过仅使用字符串文字而不是使用 String::from 转换为“拥有的”字符串来非常轻松地修复这个特定示例。这是一个重现问题的玩具示例 -在我的“真实”代码中,字符串变量是从文件中读取的,所以我显然不能使用文字。)

对我来说更神秘的是,函数的以下变体,对我来说应该遇到同样的问题,却工作正常:

fn this_is_ok() -> Vec<i32> {
    let my_string = String::from("1\n2\n3\n4");
    my_string.lines().map(|n| n.parse().unwrap()).collect()
}

原因不可能是map做一些魔术,因为这也失败了:

fn also_fails() -> Vec<&'static str> {
    let my_string = String::from("First Line\nSecond Line");
    my_string.lines().map(|s| s).collect()
}

我已经玩了很长时间,在 map 中尝试了各种不同的功能 - 有些成功了,有些失败了,老实说,我不知道有什么区别。所有这一切让我意识到我对 Rust 的 ownership/borrowing 规则在非平凡情况下的工作方式知之甚少,尽管我认为我至少了解基础知识。因此,如果有人能给我一个相对清晰和全面的指南,说明所有这些示例中发生了什么,以及如何以某种直接的方式修复失败的示例,我将非常感激!

键是 lines 产生的值的类型:&str。为了避免不必要的克隆,lines 实际上 returns 引用了它所调用的字符串的切片,当你将它收集到 Vec 时,Vec 的元素只是对字符串切片的引用。因此,当然,当您的函数退出并删除字符串时, Vec 内的引用将被删除并无效。请记住,&str 是借用的字符串,而 String 是拥有的字符串。

解析有效,因为您获取了那些 &str,然后将它们读入 i32,因此数据被传输到一个新值,您不再需要对原始字符串的引用.

要解决您的问题,只需使用 str::to_owned 将每个元素转换为 String:

fn problem() -> Vec<String> {
    let my_string = String::from("First Line\nSecond Line");
    my_string.lines().map(|v| v.to_owned()).collect()
}

需要注意的是 to_string 也可以,而且 to_owned 实际上是 ToOwned trait 的一部分,因此它对其他借用的类型也很有用。

对于大小值的引用(str 未调整大小,因此这不适用),例如 Iterator<Item = &i32>,您可以简单地使用 Iterator::cloned 克隆每个元素,以便它们不再参考。

另一种解决方案是将 String 作为参数,因此它以及对它的引用可以超出函数的范围:

fn problem(my_string: &str) -> Vec<&str> {
    my_string.lines().collect()
}

这里的问题是这一行:

let my_string = String::from("First Line\nSecond Line");

将字符串数据复制到堆上分配的缓冲区(因此不再 'static)。然后 lines returns 引用该堆分配的缓冲区。

注意&str also implements a lines method,这样就不用把字符串数据复制到堆中了,直接用你的字符串就可以了:

fn problem() -> Vec<&'static str> {
    let my_string = "First Line\nSecond Line";
    my_string.lines().collect()
}

Playground

这避免了所有不必要的分配和复制。