在Rust中返回一个新字符串的正确方法

102

我刚刚花了一周的时间阅读了 Rust 书籍,现在正在编写我的第一个程序,它返回系统壁纸的文件路径:

pub fn get_wallpaper() -> &str {
    let output = Command::new("gsettings");
    // irrelevant code
    if let Ok(message) = String::from_utf8(output.stdout) {
        return message;
    } else {
        return "";
    }
}

我遇到了错误expected lifetime parameter on &str,我知道Rust需要一个输入&str并将其作为输出返回,因为在函数结束后,我在函数内部创建的任何&str都会立即清除。

我知道我可以通过返回String而不是&str来解决这个问题,很多类似问题的答案也都这么说了。但我看起来还可以这样做:

fn main() {
    println!("message: {}", hello_string(""));
}

fn hello_string(x: &str) -> &str {
    return "hello world";
}

如何从我的函数中获得一个&str。 有人可以解释一下为什么这样做不好,以及我为什么不应该这样做吗?或者在某些情况下它是否可以接受?


3
我不太清楚你如何避免这种情况。你可以尝试通过预分配缓冲区来降低成本,但必须有人拥有这些内存。你无法在从命令输出中读取字符串之前确定其长度。在堆上分配内存对我来说是有意义的。 - bluejekyll
@bluejekyll,我已经澄清了我的问题,展示了一种看似可以通过提供一个虚拟参数来获得函数中的&str的方法。 - eiko
2个回答

122

如果您在函数中分配了String,则无法返回&str。有关此问题的进一步讨论,请参见原因,以及它不仅限于字符串。这使得您的选择更加容易:返回String

String是堆分配的,并且被构建为可变的。

String是堆分配的,因为其长度未知。由于该分配仅由String拥有,因此才能使字符串发生变化。

我的函数只是为了参考目的而返回文件路径,我宁愿让调用者决定是否需要存储在堆中的可变字符串。

这是不可能的。你的函数已经执行了一个分配操作。如果你不将分配返回给调用者,那么必须释放该值以防止内存泄漏。如果在释放后返回它,那将是一个无效的引用,会导致内存安全问题。

But I can also seemingly do this:

fn hello_string(x: &str) -> &str {
    return "hello world";
}

to get a &str out of my function. Can someone explain to me why this is bad and why I should never do it? Or maybe it's not bad and okay in certain situations?

它并不是“糟糕的”,只是在原始情况下不能真正允许您做想要的事情。那个“hello world”是一个“&'static str”,一个字符串切片,已经存储在程序本身的代码中。它具有固定的长度,并且已知比“main”生命周期更长。
签名“fn hello_string(x: &str) -> &str”可以扩展为“fn hello_string<'a>(x: &'a str) -> &'a str”。这表明所得到的字符串切片必须具有与输入字符串相同的生命周期。静态字符串将比任何生命周期都更长,因此可以替换它。
这对于仅基于输入字符串生成结果的函数非常有用:
fn long_string(x: &str) -> &str {
    if x.len() > 10 {
        "too long"
    } else {
        x
    }
}

然而,在您的情况下,该函数拥有该字符串。如果您试图返回一个与输入字符串完全不相关的String引用:
fn hello_string(x: &str) -> &str {
    &String::from("hello world")
}

你可能会遇到常见的错误信息“borrowed value does not live long enough”。这是因为借用值仅存在于方法结束之前,而不是输入字符串切片的整个生命周期。你不能“欺骗”编译器(如果可以,那是一个严重的 bug)。

2
引用不就像指向内存位置的(C/C++)指针吗?String::from("hello world") 不是在堆上分配的吗?为什么 Rust 不能直接返回一个指向那个堆位置的引用(指针)呢?我对这一点真的很困惑。 - JohnTortugo
2
@JohnTortugo 是的,引用在很多方面都像指针(但在其他方面不是)。如果你看到一个 C 函数像 char *foo() 这样,你会释放返回的数据吗?String 实际上是一个带有 (allocated_capacity, used_size, pointer_to_heap_allocation) 的结构体。 - Shepmaster
谢谢,@Shepmaster。至少有了char *foo()选项,如果我需要保留引用更长时间,我可以选择[不]释放。 - JohnTortugo
3
@JohnTortugo并不完全正确,因为仅凭函数签名无法确定在C代码中是否可以释放它。Rust通过将这两个概念拆分为两种类型来消除歧义。 - Shepmaster
很棒的回答@Shepmaster。但是你提到:
静态字符串将超过任何生命周期,因此可以替换。
但如果这是真的,为什么必须至少有一个参数来比较静态变量呢?也就是说,我不清楚为什么不能是fn hello_string() -> &str {。谢谢!
- Serg
@Serg,该函数签名不符合任何生命周期省略规则,因此编译器不知道要使用哪个生命周期。 - Shepmaster

4

如果你想在Rust中返回&str,你需要添加通用生命周期。例如:

fn hello_string<'life>() -> &'life str {
    return "hello world";
}

或者,
fn hello_string<'life>(a: &'life str, b: &'life str) -> &'life str {
    return "hello world";
}

这里有三条规则。

  1. 每个作为引用的参数都会有它自己的生命周期参数。
  2. 如果有恰好一个输入生命周期参数,则该生命周期将分配给所有输出生命周期参数。
  3. 如果有多个输入生命周期参数,但其中之一是 &self&mut self,则 self 的生命周期将分配给所有输出生命周期参数。

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