生命周期和包含引用的对象的引用

8

假设我有一个包含引用的结构体,还有另一个引用该结构体的结构体,就像这样:

struct Image<'a> {
    pixel_data: &'a mut Vec<u8>,
    size: (i32, i32),
}

struct SubImage<'a> {
    image: &'a mut Image<'a>,
    offset: (i32, i32),
    size: (i32, i32),
}

这些结构体有着几乎一模一样的接口,唯一的区别在于SubImage会根据其偏移量来调整位置参数,然后将其转发到包含的Image引用相应函数中。我希望这些结构体可以基本上互换使用,但我似乎无法搞清楚如何正确处理生命周期。最初,我只是使用了Image,并且可以简单地传递对象,从未对生命周期说明涉及到:

fn main() {
    let mut pixel_data: Vec<u8> = Vec::new();
    let mut image = Image::new(&mut pixel_data, (1280, 720));
    render(&mut image);
}

fn render(image: &mut Image) {
    image.rect_fill(0, 0, 10, 10);
}

然后我创建了SubImage,并想要进行以下操作:
fn render2(image: &mut Image) {
    let mut sub = SubImage {
        image: image,           // line 62
        offset: (100, 100),
        size: (600, 400),
    };

    sub.rect_fill(0, 0, 10, 10);
}

然而,这会导致编译器错误:

main.rs:62:16: 62:21 error: cannot infer an appropriate lifetime for automatic coercion due to conflicting requirements

编译器的建议是将签名更改为以下内容:
fn render2<'a>(image: &'a mut Image<'a>)

然而,这只是把问题推到了调用 render2 并拥有一个 &mut Image 的函数上。这很恼人,因为函数调用层数较深,当我只使用 Image 类(也有一个引用)并内联调整偏移量时,我不需要这样做。
首先,我甚至不理解为什么需要这样做(尽管我对 Rust 生命周期的理解有限)。其次(我的主要问题),是否有什么我可以做到 SubImage ,使得这些显式生命周期不必要?
2个回答

8
是的,这个错误可能会让人困惑,但它有合理的原因。
struct SubImage<'a> {
    image: &'a mut Image<'a>,
    offset: (i32, i32),
    size: (i32, i32),
}

在这里,您声明对Image的引用必须与图像本身内部借用的数据完全相同的寿命 - 引用和Image中都使用相同的生命周期参数'a: &'a mut Image<'a>
然而,render2()违反了这个要求。实际上,render2()的签名如下:
fn render2<'b, 'a>(image: &'b mut Image<'a>)

因此,它试图使用 &'b mut Image<'a> 创建 SubImage,其中'b不一定等于'a(在这种特定情况下,它肯定不是),所以编译器退出。

此外,这种签名是你可以在 main() 中提供 &mut image 调用此函数的唯一原因,因为 &mut image 具有变量 image 的生命周期,但该变量中包含的 Image 生命周期稍微长一些的 pixel_data。以下代码不是有效的 Rust 代码,但它接近编译器理解事物的方式,并展示了问题:

fn main() {
    'a: {
        let mut pixel_data: Vec<u8> = Vec::new();
        'b: {
            let mut image: Image<'a> = Image::new(&'a mut pixel_data, (1280, 720));
            render2::<'b, 'a>(&'b mut image);
        }
    }
}

当您将render2()声明为:
fn render2<'a>(image: &'a mut Image<'a>)

你确实将问题“推”到了上游 - 现在无法使用&mut image调用该函数,你现在可以看到为什么了 - 这需要统一'a'b的生命周期,但这是不可能的,因为'a'b更长。

正确的解决方案是在SubImage定义中使用对Image引用和Image本身分别使用不同的生命周期:

struct SubImage<'b, 'a:'b> {
    image: &'b mut Image<'a>,
    offset: (i32, i32),
    size: (i32, i32),
}

现在,'b'a 可能是不同的生命周期,但为了使其工作,您必须将 'a 生命周期与 'b 绑定,也就是说,'a 的寿命必须至少与 'b 一样长。这正是您的代码所需要的语义。如果不强制执行此约束,则可能会出现引用的图像在引用超出范围之前“死亡”的情况,这违反了 Rust 的安全规则。

有关生命周期边界的官方信息吗?对我来说似乎是非常重要的信息,但我在官方书籍或规范中都没有看到过。 - Malcolm
确实,这是一件重要的事情,我认为在文档中也没有看到过。可能值得在 Rust 问题跟踪器中创建一个问题。 - Vladimir Matveev

3

我有什么办法可以让SubImage不需要这些明确的生命周期吗?

Vladimir的回答很准确,但我建议您稍微改变一下代码。我的原始代码中有很多类似的引用和引用的东西。如果您需要这个,那么有单独的生命周期可以帮助很多。但是,我会将Image嵌入到SubImage中:

struct Image<'a> {
    pixel_data: &'a mut Vec<u8>,
    size: (i32, i32),
}

struct SubImage<'a> {
    image: Image<'a>,
    offset: (i32, i32),
    size: (i32, i32),
}

在我的情况下,使用嵌套引用并没有带来什么好处。直接嵌入结构体会使其稍微变大一些,但是可以使访问速度更快(少了一个指针跟踪)。在这种情况下,重要的是它消除了第二个生命周期的需求。


在我的实际代码中,pixel_data 实际上是由不同的结构体拥有的,来自于一个不同的模块,这个模块因平台而异。这个结构体可能包含其他数据(例如,在使用 Windows GDI 时,它包含一个 BITMAPINFO 对象,用于调用 StretchDIBits)。这个特定于平台的代码调用我的代码,给我一个包含其像素数据引用的 Image 对象。我可能可以消除中间人(Image),让特定于平台的代码直接传递给我一个 SubImage,它实际上代表整个图像。但这是我的代码目前正在发展的方式。 - Benjamin Lindley
@BenjaminLindley 如果 pixel_data 是由不同的结构体拥有,那也没关系。请注意,最终代码仍然引用它,并且不会尝试拥有它。我的观点是你很少需要一个引用的引用。请注意,您可以轻松地复制一个 Image,这表明您可以将其嵌入其中。听起来你可以做我建议的事情,所以我鼓励你试一试! - Shepmaster
1
哇,我从来没有在阅读答案时如此不注意。可能只是累了吧。由于某种原因,我以为你是说要将像素数据嵌入“Image”中。我一定是只读了“我的很多原始代码都有与引用相关的类似引用”,然后假设你的意思就是这样,然后我的大脑填补了你答案的其余部分。无论如何,是的,那应该可以行得通。 - Benjamin Lindley

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