有没有办法返回在函数中创建的变量的引用?

130
我想编写一个程序,它将以两个步骤编写文件。在运行程序之前,该文件可能不存在。文件名是固定的。
问题在于 OpenOptions.new().write() 可能会失败。在这种情况下,我想调用自定义函数 trycreate()。这个想法是创建文件而不是打开它并返回一个句柄。由于文件名是固定的,trycreate() 没有参数,因此我无法设置返回值的生命周期。
我该如何解决这个问题?
use std::io::Write;
use std::fs::OpenOptions;
use std::path::Path;

fn trycreate() -> &OpenOptions {
    let f = OpenOptions::new().write(true).open("foo.txt");
    let mut f = match f {
        Ok(file)  => file,
        Err(_)  => panic!("ERR"),
    };
    f
}

fn main() {
    {
        let f = OpenOptions::new().write(true).open(b"foo.txt");
        let mut f = match f {
            Ok(file)  => file,
            Err(_)  => trycreate("foo.txt"),
        };
        let buf = b"test1\n";
        let _ret = f.write(buf).unwrap();
    }
    println!("50%");
    {
        let f = OpenOptions::new().append(true).open("foo.txt");
        let mut f = match f {
            Ok(file)  => file,
            Err(_)  => panic!("append"),
        };
        let buf = b"test2\n";
        let _ret = f.write(buf).unwrap();
    }
    println!("Ok");
}

打开这个页面,按Ctrl-F,搜索“Cow”,没有结果??虽然您无法返回在函数中创建的变量的引用,但是您可以使用std::borrow::Cow来泛化拥有的数据和未拥有的引用--它是一个Deref,让您选择给定实例是否拥有或借用其数据。我发现这是在返回拥有和未拥有的数据之间切换的最可靠方法。 - BallpointBen
5个回答

127

你提出的问题

TL;DR:不,你不能返回一个由函数拥有的变量的引用。无论是你创建的变量还是作为函数参数取得所有权。

解决方案

不要试图返回引用,而是返回一个拥有所有权的对象。例如,返回 String 而不是 &str,返回 Vec<T> 而不是 &[T],返回 T 而不是 &T,等等。

如果你通过参数获取了变量的所有权,请尝试获取一个(可变)引用,然后返回相同生命周期的引用。

在极少数情况下,你可以使用不安全的代码来返回拥有值和对其的引用。这有许多微妙的要求,你必须遵守这些要求,以确保你不会导致未定义的行为或内存不安全。

另请参见:

更深入的答案

fjh 是完全正确的,但我想更深入地评论一下你的代码中的其他错误。

让我们从返回引用的较小示例开始,并查看错误:

fn try_create<'a>() -> &'a String {
    &String::new()
}

Rust 2015

error[E0597]: borrowed value does not live long enough
 --> src/lib.rs:2:6
  |
2 |     &String::new()
  |      ^^^^^^^^^^^^^ temporary value does not live long enough
3 | }
  | - temporary value only lives until here
  |
note: borrowed value must be valid for the lifetime 'a as defined on the function body at 1:15...
 --> src/lib.rs:1:15
  |
1 | fn try_create<'a>() -> &'a String {
  |               ^^

Rust 2018

error[E0515]: cannot return reference to temporary value
 --> src/lib.rs:2:5
  |
2 |     &String::new()
  |     ^-------------
  |     ||
  |     |temporary value created here
  |     returns a reference to data owned by the current function

有没有办法在没有参数的函数中返回引用?从技术上讲,“是的”,但对于您想要的内容,“不行”。引用指向现有的内存片段。在没有参数的函数中,唯一可以引用的是全局常量(具有生命周期`&'static`)和本地变量。我们先忽略全局变量。在像C或C++这样的语言中,您实际上可以引用本地变量并返回它。但是,一旦函数返回,就不能保证您所引用的内存仍然是您认为的那样。它可能会保持您期望的一段时间,但最终该内存将被重新用于其他用途。一旦您的代码查看内存并尝试将用户名解释为用户银行账户中剩余的金额,问题就会出现!这就是Rust生命周期的作用-您不允许超出所引用值在其当前内存位置有效的时间长度。另请参见:Is it possible to return either a borrowed or owned type in Rust?Why can I return a reference to a local literal but not a variable?

您实际的问题

请查看OpenOptions::open的文档:

fn open<P: AsRef<Path>>(&self, path: P) -> Result<File>

该函数返回一个Result<File>,因此我不知道您会如何返回OpenOptions或其引用。如果您将其重写为以下方式,则该函数将起作用:

fn trycreate() -> File {
    OpenOptions::new()
        .write(true)
        .open("foo.txt")
        .expect("Couldn't open")
}

使用Result::expect来生成一个有用的错误信息并引发panic。当然,在程序的核心部分引发panic并不是非常有用的,因此建议将错误传播回去:

fn trycreate() -> io::Result<File> {
    OpenOptions::new().write(true).open("foo.txt")
}

OptionResult有很多处理链式错误逻辑的好方法。在这里,你可以使用or_else

let f = OpenOptions::new().write(true).open("foo.txt");
let mut f = f.or_else(|_| trycreate()).expect("failed at creating");

我也会返回main中的Result。所有这些,包括fjh的建议:
use std::{
    fs::OpenOptions,
    io::{self, Write},
};

fn main() -> io::Result<()> {
    let mut f = OpenOptions::new()
        .create(true)
        .write(true)
        .append(true)
        .open("foo.txt")?;

    f.write_all(b"test1\n")?;
    f.write_all(b"test2\n")?;

    Ok(())
}

15
注意:在C++中,返回指向栈局部变量的引用是未定义的行为;如果它似乎有效,那只是你运气不好。在常见情况下,编译器应该检测到该问题并发出警告。 - Matthieu M.
@Shepmaster: 你一定在使用旧编译器;我还卡在gcc 4.3.2上,而我有它!但是,只是一个警告。大多数C/C++编译器采取保守的方法:错误由标准规定,其余部分使用警告(更或多或少准确...)。 - Matthieu M.
@MatthieuM。我们确实有一个较旧的编译器,但可能不是那么老。当然,我可能从未尝试过将局部变量作为引用返回,但我不知道我是否能够声称自己那么好。 :-) - Shepmaster
1
@D3181,不存在名为Write的方法。您要找的是write,您需要在作用域中具有该特征。请查看Rust 1.0 /“编写文件”部分 的"What's the de-facto way of reading and writing files in Rust 1.x?"。 - Shepmaster
1
@JohnDoe,这不是在答案中链接的问题中涵盖了吗? - Shepmaster
显示剩余13条评论

23

有没有办法从没有参数的函数中返回一个引用?

没有(除了对静态值的引用,但这在这里并没有帮助)。

但是,您可能需要查看OpenOptions::create。 如果您将main中的第一行更改为:

let  f = OpenOptions::new().write(true).create(true).open(b"foo.txt");

如果文件尚不存在,它将被创建,这应该解决您最初的问题。


20

你不能返回指向局部变量的引用。你有两个选择,要么返回值,要么使用静态变量。

原因是:

引用是指向内存位置的指针。一旦函数执行完毕,局部变量就会从执行堆栈中弹出,并且资源也被释放了。在那之后,任何对局部变量的引用都将指向一些无用的数据。由于它已经被释放,它不再属于我们程序的所有权,操作系统可能已经把它分配给另一个进程,我们的数据可能已经被覆盖了。

对于下面这个例子,x 是在函数运行时创建的,并在函数完成执行后被删除。它是局部的,并存在于该特定函数的堆栈上。函数的堆栈保存局部变量。

run 从执行堆栈中弹出时,对 x 的任何引用,&x,都将指向一些垃圾数据。这就是人们所说的悬空指针。Rust 编译器不允许使用悬空指针,因为这是不安全的。

fn run() -> &u32 {
    let x: u32 = 42;

    return &x;
} // x is dropped here

fn main() {
    let x = run();
}

因此,我们不能返回对局部变量的引用。我们有两个选择:要么返回该值,要么使用静态变量。

在这里,返回该值是最好的选择。通过返回该值,您将把计算结果传递给调用者,在Rust的术语中,x将由调用者拥有。在我们的例子中,它是main。所以没有问题。

由于静态变量的生存周期与进程运行时间一样长,其引用在函数内外都将指向同一内存位置。在这里也没有问题。

注意:@navigaid建议使用box,但这没有意义,因为您正在将现成的数据移到堆上并返回它。这并没有解决问题,您仍然将局部变量返回给调用方,但在访问时使用了指针。这会由于解引用而增加额外的成本,从而增加不必要的间接性。基本上,您只是为了使用&而使用它,没有其他用途。


10
像这样在块末尾使用return不是惯用语。 - Shepmaster
3
第一个答案过于冗长,第二个答案又不够详细。因此我们选择使用“返回”来强调重点。 - snnsnn
6
尽管使用了非惯用的 return,但我认为这个答案的解释最清晰易懂。 - jla
这样做更有意义。编译器错误信息可能需要更明显一些。我曾经遇到过这样的情况,我传递了一个拥有的对象,但是只返回对已移动对象的引用,该对象将在函数结束时被丢弃。我更习惯于阅读更常见的"x.foo()在此借用x,但x在函数末尾被丢弃"。 - Josh Bowden

15
这是对snnsnn的回答的详细阐述,该回答简要解释了问题但没有过于具体。
Rust不允许返回在函数中创建的变量的引用。是否有解决方法?是的,只需将该变量放入Box中,然后返回即可。示例:
fn run() -> Box<u32> {
    let x: u32 = 42;
    return Box::new(x);
} 

fn main() {
    println!("{}", run());
}

在 Rust Playground 中的代码

一般来说,在 Rust 中避免类似问题的方法是返回拥有所有权的对象(Box、Vec、String 等),而不是变量的引用:

  • Box<T> 代替 &T
  • Vec<T> 代替 &[T]
  • String 代替 &str

对于其他类型,请参考Rust 类型周期表以确定应使用哪个拥有所有权的对象。

当然,在此示例中,您可以简单地返回值(T 代替 &TBox<T>

fn run() -> u32 {
    let x: u32 = 42;
    return x;
} 

4
这个答案是错误的且具有误导性。为什么要将一个已经可用的数据进行封装再返回呢?这样只会增加不必要的间接性和成本。 - snnsnn
@snnsnn,u32 变量仅供演示。我已在答案末尾指出。 - btwiuse
2
即使您将适合装箱的变量装箱,该示例仍然是错误的,因为引用是用于将外部作用域中的引用传递到函数中,换句话说,用于借用外部变量,以避免接收和返回的混乱。您的示例完全颠倒了这种逻辑。对于新手来说会很困惑,并且没有真正的用例。 - snnsnn
@snnsnn 如果 Box 的生命周期足够长,通过 Box 在堆上分配值是有意义的。按照 navigaid 的方式,Box 的生命周期太短了,所以就像你说的那样可能没有意义。当作用域结束时,Box 应该被丢弃。因此,我们必须使用 Box::into_raw 或 Box:from_raw。这里也可以看看:https://dev59.com/Pb_qa4cB1Zd3GeqPInyT - lechat

3

问题是:

有没有办法返回在函数中创建的变量的引用?

答案:是的,这是可能的!请参考下面的示例进行验证。

免责声明:通常最好只返回一个值,但并非总是如此...

您必须找到一种方法来延长生命周期。一种方法是在函数外部创建一个虚拟/默认值,并将其作为可变引用(&mut T)提供给函数。现在函数可以填充/替换该值,然后返回对该值的引用(&T)。您还必须指定生命周期,以便返回的引用获得在函数外部创建的值的生命周期('a)。

这样做的原因是函数返回对在调用函数之前分配的内存位置的引用,因此当函数超出范围时,它不会被删除(或移动)。同时,该值也不归函数所有。

以下示例证明了这是可能的:

//&mut T -> &T
fn example2<'a>(life: &'a mut Vec<i32>) -> &'a Vec<i32> {
    *life = vec![1, 2, 3, 4];
    life
}

fn test_example2() {
    //Could also use Vec::new()
    let mut life = Vec::default();
    let res = example2(&mut life);
    println!("{:?}", res)
}

fn test2_example2() {
    let life = &mut Vec::default();
    let res = example2(life);
    println!("{:?}", res)
}

//shows real use case
fn get_check_test_slices2<'a>(
    lifetime: &'a mut Vec<usize>,
    limit: usize,
) -> impl Iterator<Item = (&'a [usize], &'a [usize])> + 'a {
    // create a list of primes using a simple primes sieve
    *lifetime = primes1_iter_bitvec(limit).collect::<Vec<_>>();
    // iterate through pairs of sub slices without copying the primes vec
    // slices will be used to check that a complicated sieve is correct
    all_test_check_slices(lifetime)
}

使用名为LateInit的包装类型的示例,带有辅助方法:

fn late_init_example1<'a>(holder: &'a mut LateInit<Vec<i32>>) -> &'a Vec<i32> {
    //create a new vec inside the function
    let v = vec![1, 2, 3];
    //insert it into holder and get reference to value
    let res = holder.init(v);
    //return reference
    res
}

fn late_init_example2<'a>(holder: &'a mut LateInit<Vec<i32>>) -> &'a Vec<i32> {
    //create new vec, insert it into holder, return a reference
    holder.init(vec![1, 2, 3])
}

fn using_late_init_examples() {
    let mut a = LateInit::new();
    println!("example1: {:?}", late_init_example1(&mut a));
    println!("example1: {:?}", late_init_example1(&mut LateInit::new()));
    let b = &mut LateInit::new();
    println!("example2: {:?}", late_init_example2(b));
    //can re-use the same late init
    for i in 0..=4 {
        println!("example2: {:?}", late_init_example2(b));
    }
}

/// A thin wrapper around Option<T>.
/// Enables returning a reference to a value created inside a function.
/// Note: You probably have to add lifetime annotations.
/// 1: This can be achieved by creating a
/// late initialized value on the stack first (Option<T>),
/// 2: then calling a function with a mutable reference
/// to this late initialized value,
/// 3: then initializing the value inside the function,
/// 4: and finally returning a reference to the now initialized value.
/// The returned reference can even be stored in a struct or
/// given to another function, as long as the
/// lifetime annotations are correct and
/// the late init value isn't deleted or moved.
/// TODO: better name?
/// TODO: better documentation.
pub struct LateInit<T> {
    value: Option<T>,
}

impl<T> LateInit<T> {
    /// Creates a new LateInit with None.
    /// Same as default.
    pub fn new() -> Self {
        Self { value: None }
    }

    /// Inserts the value
    /// and returns a mutable reference with 
    /// the same lifetime as the inserted value.
    pub fn init_mut<'a>(&'a mut self, init_value: T) -> &'a mut T {
        self.value.insert(init_value)
    }

    /// Inserts the value
    /// and returns a reference with 
    /// the same lifetime as the inserted value.
    /// Is non mutable init neccesary?
    pub fn init<'a>(&'a mut self, init_value: T) -> &'a T {
        self.value.insert(init_value)
    }
}

impl<T> Default for LateInit<T> {
    /// Creates a new LateInit with None.
    /// Same as new.
    fn default() -> Self {
        Self { value: None }
    }
}

使用LateInit包装类型(与仅使用&mut T相比)可能具有以下优势:
  • 返回的引用始终指向已初始化的值。
  • 您不需要T的默认构造函数,这有时可能会很混乱。
  • 代码的意图可能更清晰。

待办事项:
为LateInit创建一个crate。
向所有可能受益的类似问题传播这一信息。

编辑:
添加了&mut T的示例(感谢Chayim Friedman)。
旧的包装类型被名为“改进版”的LateInit替代。


1
这与将 &mut T 直接传递给函数有何不同? - Chayim Friedman
我已经更新了我的答案,以回答那个问题。通常你可以只是将 &mut T 传递给函数。 - Grand Phason
更新:也许你可以总是使用&mut T... - Grand Phason
答案已使用“&mut T”示例进行了更新。旧的包装类型已被替换为改进版本。已添加一些可能的包装类型优点。 - Grand Phason

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