【问题标题】:Return a reference from a callback [duplicate]从回调中返回引用[重复]
【发布时间】:2017-08-12 09:21:34
【问题描述】:

我正在尝试从回调内部返回对数据数组的引用。由于生命周期,下面的 sn-p 是不可能的,但我还是添加了它以提供更好的上下文。

我想实现某种虚拟文件系统。我想使用返回类型&[u8],因为我正在考虑使用mmap 以及看起来很有希望的实现暴露&[u8] 来访问数据。

现在这有点过头了,所以我想专注于回调来读取并返回传递给它的文件的内容。

这样做的惯用方式是什么?

use std::fs::File;
use std::io::prelude::*;

fn main() {
    test(&|path| {
        if false {
            let mut data: Vec<u8> = Vec::new();
            let mut file = File::open(path).unwrap();
            file.read_to_end(&mut data).unwrap();
            return Some(&data);
        }
        None
    });
}

// loads various files. I do not care about them anymore once this function returns
pub fn test<'a>(loader: &Fn(&str) -> Option<&'a [u8]>) {}

【问题讨论】:

  • 在这种情况下,闭包和函数没有区别。
  • 此外,接受泛型 F 其中F: Fn(&amp;str),而不是特征对象&amp;Fn(&amp;str) 更为惯用。
  • 虽然问题表面上相似,但其他问题的答案在这里并不适用。在这种情况下,OP 正在询问如何做一些真正有意义的事情,他们只是错误地接近它。具体来说,OP 需要重构代码,以便 clsoure 不会直接返回引用。相反,闭包应该返回一个管理资源生命周期的对象,并且提供了一个获取引用的方法。为了使 test 可用于不同的资源,闭包应该可能返回一个装箱的 trait 对象。 (详情见我的回答。)
  • @user4815162342 OP 明确表示忽略他们将使用mmap 的事实,所以我做到了。如果您认为这个问题实际上是关于返回一个装箱的 trait 对象,我们可以更改问题以澄清,但这会使 other 现有答案无效。你觉得三个错误中哪一个是较小的罪?
  • @Shepmaster 据我了解,OP 希望首先测试设计的其他方面,而不是立即实现完整的mmap 逻辑。另一方面,interface(闭包的签名)应该足够灵活,以便稍后添加对mmap 的支持——这就是OP 想要返回&amp;[u8] 的原因。但这是我的理解,我自己当然不会改变这个问题 - 如果他们在阅读这些 cmets 后选择这样做,则取决于 OP。保留两个答案都很好,每个答案都在其对问题的理解范围内有效。

标签: rust


【解决方案1】:

返回对堆栈分配数据的引用是不正确的,因为它会立即超过它所引用的对象。唯一可以始终返回且不问任何问题的引用是那些生命周期为 'static 的引用——Rust 会仔细检查。对新分配数据的引用绝对不是'static

幸运的是,有一种方法可以绕过它:当 Rust 可以证明引用比数据更有效时,返回引用是安全的。例如:

// Memory backed by a Vec
struct VecMemory {
    data: Vec<u8>
}

impl VecMemory {
    fn as_slice(&self) -> &[u8] {
        &self.data
    }
}

as_slice() 可能会返回一个引用,因为该引用可证明比它所引用的对象的寿命更长。如果我们撤消生命周期省略,as_slice() 的签名将是:

fn as_slice<'a>(&'a self) -> &'a [u8]

下一个问题是闭包应该返回什么?如果它返回 @E_net4 所建议的 VecVecMemory(同样只包含 Vec),那么将使用向量作为底层存储将被烘焙到接口中。为了支持不同的存储类型,闭包应该返回其他语言所称的接口。最接近的 Rust 等价物是 trait 对象,它在返回上下文中指定为 Box&lt;SomeTrait&gt;

通过这种设计,闭包有效地堆分配了一个资源管理对象,并返回一个两指针大小的框,该框提供所有权和对堆分配值的统一接口。盒子的用户只通过盒子与实现进行通信,盒子使用内部 vtable 与实现对话。 (指向 vtable 的指针是 Box 本身占用两个指针而不是一个的原因。)换句话说,闭包的返回值 擦除返回的具体类型。

use std::fs::File;
use std::io::prelude::*;

trait Memory {
    fn as_slice(&self) -> &[u8];
    // a real-life trait would likely also define
    // as_slice(&mut self) -> &mut [u8]
}

// Memory backed by a Vec
struct VecMemory {
    data: Vec<u8>
}

impl Memory for VecMemory {
    fn as_slice(&self) -> &[u8] {
        &self.data
    }
}

fn main() {
    test(&|path| {
        if false {
            let mut data: Vec<u8> = Vec::new();
            let mut file = File::open(path).unwrap();
            file.read_to_end(&mut data).unwrap();
            return Some(Box::new(VecMemory { data: data }));
        }
        None
    });
}

// loader returns a boxed trait object whose underlying memory
// can be accessed as long as the box is alive.
fn test<'a>(_loader: &Fn(&str) -> Option<Box<Memory>>) {}

要将mmap 用于存储,可以编写不同的Memory 实现,例如Mmap。这将存储原始指针和mmap() 返回的内存大小。它将在new() 中调用mmap(),在Drop::drop 中调用munmap()。最重要的是,Mmap 将使用不安全块实现Memory,以从存储的指针和长度构造切片。同样,这是安全的,因为引用的生命周期将与 Mmap 的生命周期相关联。

【讨论】:

    【解决方案2】:

    您不想在此处返回引用,因为您的 data 仅存在于闭包内。除非您希望将回调 API 更改为通过可变引用修改缓冲区的东西,否则更简单(并且仍然是惯用的)方法将返回向量。

    test(&|path| {
        if false {
            let mut data: Vec<u8> = Vec::new();
            let mut file = File::open(path).unwrap();
            file.read_to_end(&mut data).unwrap();
            return Some(data);
        }
        None
    });
    

    然后,修改消费者函数test 以返回向量,从而保留所有权。如果需要,可以通过调用as_slice获取对vector内数据的引用。

    pub fn test<'a>(loader: &Fn(&str) -> Option<Vec<u8>>) {}
    

    我想使用返回类型 &[u8] 因为我正在考虑使用 mmap 和看起来很有希望的实现暴露 &[u8] 来访问数据

    您可能的意思是希望您的函数返回&amp;[u8]。即使在这种情况下,数据也必须在其他地方拥有,这是您必须自己处理的事情。这可能涉及拥有某种 ResourceHandler 结构,该结构将提供与资源处理程序一样长的切片。

    但是现在这有点过头了,所以我想专注于回调来读取并返回传递给它的文件的内容。

    在这种情况下,您暂时可以返回 Vec。 :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-02-06
      • 2016-09-10
      • 1970-01-01
      • 2018-11-29
      • 1970-01-01
      • 1970-01-01
      • 2020-08-14
      相关资源
      最近更新 更多