将本地字符串作为切片(&str)返回

88

有几个问题似乎与我遇到的问题相同。例如,请参见这里这里。基本上,我正在尝试在本地函数中构建一个String,然后将其作为&str返回。切片不起作用,因为生命周期太短。我不能直接在函数中使用str,因为我需要动态构建它。但是,我也不想返回一个String,因为这个对象一旦构建起来就是静态的。有没有办法两全其美?

这里是一个最小化的无法编译复制:

fn return_str<'a>() -> &'a str {
    let mut string = "".to_string();

    for i in 0..10 {
        string.push_str("ACTG");
    }

    &string[..]
}

4
其他问题也有相同的问题,答案仍然是一样的:由于 Rust 的内存模型,无法在函数中构建一个 String 并将其作为 &str 返回。 - Levans
1
你不想返回一个 String 的理由对我来说毫无意义。在这个“静态”对象中存储一个 String 而不是一个 &str。这样更容易,至少与人体工程学相同,从所有权的角度讲更有意义,而且甚至没有任何性能优势。 - user395760
@delnan,你实际上回答了我另一个疑问,那就是使用“String”是否有性能劣势。我应该能够重构以使用“String”。 - anderspitman
2
有些人可能会发现这篇文章很有帮助:Rust中返回引用的策略 - Jonathan Tran
7个回答

106

不可以这样做,至少有两个原因可以解释这个问题。

首先,要记住引用是借用的,它们指向某些数据但不拥有它们,它们归属于其他人。在这种情况下,你想要返回的切片字符串是函数拥有的,因为它存储在一个局部变量中。

当函数退出时,所有局部变量都被销毁;这涉及到调用析构函数,而String的析构函数会释放字符串使用的内存。然而,你想返回一个指向为该字符串分配的数据的借用引用。这意味着返回的引用立即变成了悬空引用——它指向无效的内存!

Rust 的创建目标之一就是防止此类问题发生。因此,在 Rust 中不可能返回指向函数局部变量的引用,而这在 C 等语言中是可能的。

还有另一个稍微正式一点的解释。让我们看看你的函数签名:

fn return_str<'a>() -> &'a str

请记住,生命周期和泛型参数都是函数的参数,由函数调用者设置。例如,另一个函数可能会像这样调用它:

let s: &'static str = return_str();

这要求'a必须是'static,但这当然是不可能的 - 您的函数没有返回对静态内存的引用,而是返回具有严格较短寿命的引用。因此,这样的函数定义是不安全的,并被编译器禁止。

无论如何,在这种情况下,您需要返回一个拥有类型的值,特别是在这种情况下,它将是一个拥有的String

fn return_str() -> String {
    let mut string = String::new();

    for _ in 0..10 {
        string.push_str("ACTG");
    }

    string
}

8
只要你泄漏了String,就可以在技术上将其返回为'static。 (使用std::mem::forget),但它将永远保持分配状态。(我猜这就是'static的意义所在) - Triss Healy
1
Rust非常聪明,那么为什么它看不到当我在本地创建一个变量并尝试返回对它的引用时,我想返回它而不是复制它并且不要放弃它? - dscham
2
@Sens 所有本地变量都分配在当前执行函数的堆栈帧中。当函数返回时,堆栈帧将被释放,以及其所有内容。因此,如果您返回对其中一个本地变量的引用,则该引用将在函数返回后立即失效。唯一的解决方法是将这样的本地变量分配到堆栈之外,并返回对 that 的引用,但这种方法违反了 Rust 的各种机制(例如,现在不清楚谁拥有此值)。顺便说一下,其他具有 GC 的语言正是这样做的。 - Vladimir Matveev
@VladimirMatveev 好的,那很有道理。我倾向于忘记堆栈分离。来自GCed语言。不过,编译器不应该看到非常常见的用例并生成机器代码以将返回值从堆栈复制到堆中吗?或者这对Rustlings来说太神奇了?(以一种爱的方式)编辑:哎呀,我刚意识到这正是您需要手动完成的事情。而且这是低级语言的适当行为。 - dscham
1
@Sens 嗯,那将会是太多的魔法。举个例子,Go语言就是这样做的,但Rust选择不明确地这样做。这也使得类型系统更加正交和有序:比如说&StringString之间存在着强烈的语义差异,为了让它们能够协同工作,要么就应该从&String自动转换为String,或者&String和其他引用类型必须以某种方式被传递到堆上,这将需要对类型系统进行一些复杂的改变。因此,在这里不做任何花哨的事情也是最简单的方法。 - Vladimir Matveev

19
在某些情况下,你会得到一个字符串切片,并且可能需要有条件地创建一个新的字符串。在这种情况下,你可以返回一个 Cow。这样可以在可能的情况下使用引用,否则使用拥有所有权的 String
use std::borrow::Cow;

fn return_str<'a>(name: &'a str) -> Cow<'a, str> {
    if name.is_empty() {
        let name = "ACTG".repeat(10);
        name.into()
    } else {
        name.into()
    }
}

3
天哪,我开始认为我的用例(有条件地通过字符串引用或根据需要修改字符串)不可能实现,直到我最终偶然发现了这个。 - ijoseph

9
您可以选择通过内存泄漏的方式将一个 String 转换为 &'static str
fn return_str() -> &'static str {
    let string = "ACTG".repeat(10);

    Box::leak(string.into_boxed_str())
}

在许多情况下,这是一个非常糟糕的想法,因为每次调用此函数时,内存使用量将永久增长。

如果您想要每次调用返回相同的字符串,请参见:


4
问题在于你正在尝试创建一个引用了函数返回后将消失的字符串。
在这种情况下,一个简单的解决方法是将空字符串传递给函数。这将明确地确保所引用的字符串仍然存在于函数返回的范围内。
fn return_str(s: &mut String) -> &str {

    for _ in 0..10 {
        s.push_str("ACTG");
    }

    &s[..]
}

fn main() {
    let mut s = String::new();
    let s = return_str(&mut s);
    assert_eq!("ACTGACTGACTGACTGACTGACTGACTGACTGACTGACTG", s);
}

Rust Playground 中的代码: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2499ded42d3ee92d6023161fe82e9b5f


2
这是一个老问题,但非常常见。有很多答案,但没有一个回答了人们对字符串和字符串切片的明显误解,这源于不知道它们的真实本质。
但在回答隐含的问题之前,让我们从显而易见的问题开始:我们能返回对局部变量的引用吗?
我们要实现的是悬空指针的教科书定义。当函数完成执行时,局部变量将被删除。换句话说,它们将从执行堆栈中弹出,并且对局部变量的任何引用都将指向一些垃圾数据。
最好的做法是返回字符串或其副本。不需要过于关注速度。
然而,我认为问题的本质是是否有一种方法将String转换为str?答案是否定的,这就是误解所在的地方:
你不能通过借用将一个 String 转换为 str。因为一个 String 是在堆上分配的。如果你对它取一个引用,你仍然会使用通过引用访问的在堆上分配的数据。而另一方面,str 存储在可执行文件的数据段中,是静态的。当你对一个字符串取一个引用时,你会得到与常见字符串操作匹配的类型签名,而不是实际的 &str。
现在,如果你绝对需要使用静态文本,可能有一个特定用例的解决方法:
由于你使用四个碱基 A、C、G、T 的组合,每组四个,你可以创建所有可能结果的列表作为 &str 并通过某些数据结构使用它们。这样做可能会有些麻烦,但肯定可行。
你可以查看这篇文章以获取详细说明:Rust 的 `String` 和 `str` 之间有什么区别?

0
如果在编译时以静态方式创建结果字符串是可能的,那么这将是一种解决内存泄漏问题的方法。
#[macro_use]
extern crate lazy_static;
    
fn return_str<'a>() -> &'a str {
    lazy_static! {
        static ref STRING: String = {
            "ACTG".repeat(10)
        };
    }

    &STRING
}

-2

可以的 - 方法 replace_range 提供了一个解决办法 -

let a = "0123456789";
//println!("{}",a[3..5]);  fails - doesn't have a size known at compile-time
let mut b = String::from(a);
b.replace_range(5..,"");
b.replace_range(0..2,"");
println!("{}",b); //succeeds 

为了达成这个目标,我付出了血、汗和泪水!


3
...是的,Rust中的辛勤付出有时候意味着我们应该停下来思考其他的方法 :) - Félix Adriyel Gagnon-Grenier

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