混淆所有权的问题涉及到线路和地图时怎么办?

4
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的东西。
(我知道我可以很容易地通过使用字符串文字而不是将其转换为"所拥有的"字符串来修复此特定示例。这是一个玩具示例,重新生成此问题-在我的"真实"代码中,字符串变量是从文件中读取的,因此我显然不能使用字面意义上的值。)
更神秘的是,以下对该函数的变异,在我看来应该遇到同样的问题,但它却正常工作:
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的所有权/借用规则如何工作几乎没有掌握,尽管我认为我至少了解基础知识。因此,如果有人能给我一个相对清晰和全面的指南,解释所有这些示例中正在发生的事情,并说明如何以一些简单的方式修复失败的示例,我将非常感激!

2个回答

6
关键在于lines返回的值类型: &str。为了避免不必要的复制,lines实际上返回对所调用字符串片段的引用,当您将其收集到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()
}

1
谢谢 - 这很有道理,事实上我之前一直在尝试使用 .to_owned 来修复问题,但是一直无法使其正常工作。现在我已经解决了,虽然我的其他几个函数都必须从接受 &str 切片转换为拥有的 String。我想这是不可避免的(我现在明白了 Rust 书中的 部分,我以为它是说始终优先使用字符串切片,但我现在看到这只适用于借用的 String,而不是拥有的)。再次感谢。 - Robin Zigmond
@RobinZigmond 我在第二个示例中意识到了一个小错误,我已将其从&String更改为&str,因为&str更通用。 - Aplet123

1
这里的问题在于这一行代码:

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

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

请注意,{{link1:&str也实现了lines方法},因此您无需将字符串数据复制到堆上,可以直接使用字符串:

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

游乐场

这个方法避免了所有不必要的内存分配和复制。


1
谢谢,但正如我在问题中已经说过的那样 - 我知道我可以像这样修复示例,但在我的实际情况中,字符串是动态的,而不是字面量,所以我不能这样做。使用另一个答案中建议的 to_owned,并修改使用此输出的其他函数,为我解决了这个问题。 - Robin Zigmond

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