在Rust中,使用结构体的生命周期的正确方式是什么?

59
我想写下这个结构:
struct A {
    b: B,
    c: C,
}

struct B {
    c: &C,
}

struct C;

应该从A.c中借用B.c。
A ->
  b: B ->
    c: &C -- borrow from --+
                           |
  c: C  <------------------+

这是我尝试过的内容:
struct C;

struct B<'b> {
    c: &'b C,
}

struct A<'a> {
    b: B<'a>,
    c: C,
}

impl<'a> A<'a> {
    fn new<'b>() -> A<'b> {
        let c = C;
        A {
            c: c,
            b: B { c: &c },
        }
    }
}

fn main() {}

但它失败了。
error[E0597]: `c` does not live long enough
  --> src/main.rs:17:24
   |
17 |             b: B { c: &c },
   |                        ^ borrowed value does not live long enough
18 |         }
19 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'b as defined on the method body at 13:5...
  --> src/main.rs:13:5
   |
13 |     fn new<'b>() -> A<'b> {
   |     ^^^^^^^^^^^^^^^^^^^^^

error[E0382]: use of moved value: `c`
  --> src/main.rs:17:24
   |
16 |             c: c,
   |                - value moved here
17 |             b: B { c: &c },
   |                        ^ value used here after move
   |
   = note: move occurs because `c` has type `C`, which does not implement the `Copy` trait

我已经阅读了关于Rust所有权的文档,但我仍然不知道如何解决这个问题。

7
在Rust中,不可能使用兄弟结构体引用(例如,引用同一结构体的部分)。 - Matthieu M.
4个回答

88

实际上,上面的代码失败有不止一个原因。让我们稍微分解一下,并探索一些修复它的选项。

首先,让我们移除 new 并在 main 中直接构建一个 A 实例,这样你就会发现问题的第一部分与生命周期无关:

struct C;

struct B<'b> {
    c: &'b C,
}

struct A<'a> {
    b: B<'a>,
    c: C,
}

fn main() {
    // I copied your new directly here
    // and renamed c1 so we know what "c"
    // the errors refer to
    let c1 = C;

    let _ = A {
        c: c1,
        b: B { c: &c1 },
    };
}

这将会失败,错误信息如下:

error[E0382]: use of moved value: `c1`
  --> src/main.rs:20:20
   |
19 |         c: c1,
   |            -- value moved here
20 |         b: B { c: &c1 },
   |                    ^^ value used here after move
   |
   = note: move occurs because `c1` has type `C`, which does not implement the `Copy` trait

它的意思是,如果你将分配给,你将其所有权移交给(即你不能再通过访问它,只能通过访问)。这意味着对的所有引用将不再有效。但是你在作用域中仍然有一个<&c1>(在B中),所以编译器不能让你编译此代码。
编译器在错误消息中提示了一个可能的解决方案,即类型不可复制。如果你可以复制一个,那么你的代码就是有效的,因为将分配给将创建一个值的新副本,而不是移动原始副本的所有权。
我们可以通过改变定义来使可复制:
#[derive(Copy, Clone)]
struct C;

现在上面的代码可以正常工作了。请注意,@matthieu-m评论中提到的仍然是正确的:我们不能在B中同时存储值的引用和值本身(在这里我们存储了一个值的引用和它的复制品)。这不仅适用于结构体,而且适用于所有权。
现在,如果您不想(或无法)使C可复制,您可以分别在A和B中存储引用。
struct C;

struct B<'b> {
    c: &'b C,
}

struct A<'a> {
    b: B<'a>,
    c: &'a C, // now this is a reference too
}

fn main() {
    let c1 = C;
    let _ = A {
        c: &c1,
        b: B { c: &c1 },
    };
}

一切都好了吗?实际上并不是...我们仍然想将A的创建移回到一个new方法中。这就是我们会遇到生命周期问题的地方。让我们将A的创建移回到一个方法中:

impl<'a> A<'a> {
    fn new() -> A<'a> {
        let c1 = C;
        A {
            c: &c1,
            b: B { c: &c1 },
        }
    }
}

正如预期的那样,这是我们的终身错误:

error[E0597]: `c1` does not live long enough
  --> src/main.rs:17:17
   |
17 |             c: &c1,
   |                 ^^ borrowed value does not live long enough
...
20 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the impl at 13:1...
  --> src/main.rs:13:1
   |
13 | impl<'a> A<'a> {
   | ^^^^^^^^^^^^^^

error[E0597]: `c1` does not live long enough
  --> src/main.rs:18:24
   |
18 |             b: B { c: &c1 },
   |                        ^^ borrowed value does not live long enough
19 |         }
20 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the impl at 13:1...
  --> src/main.rs:13:1
   |
13 | impl<'a> A<'a> {
   | ^^^^^^^^^^^^^^

这是因为c1new方法结束时被销毁了,所以我们无法返回对它的引用。
fn new() -> A<'a> {
    let c1 = C; // we create c1 here
    A {
        c: &c1,          // ...take a reference to it
        b: B { c: &c1 }, // ...and another
    }
} // and destroy c1 here (so we can't return A with a reference to c1)

一个可能的解决方案是在new之外创建C并将其作为参数传递:

struct C;

struct B<'b> {
    c: &'b C,
}

struct A<'a> {
    b: B<'a>,
    c: &'a C
}

fn main() {
    let c1 = C;
    let _ = A::new(&c1);
}

impl<'a> A<'a> {
    fn new(c: &'a C) -> A<'a> {
        A {c: c, b: B{c: c}}
    }
}

playground


你(或其他人)知道是否有一种方法可以在新的函数内部创建“C”,同时让编译器满意吗? - Sushisource
@Sushisource 技术上你可以使用具有静态生命周期的引用(&'static C)返回,但在实践中很少有用。 - Paolo Falabella
有人知道现在是否可以使用 Pin 来解决这个问题吗?文档中的示例展示了如何通过将 &'a T 替换为 *const T 来实现类似的功能。然而,我没有看到任何方法可以用原始指针来替换 B<'a>。有吗? - zbrojny120
2
在类似的情况下,我最终使用了Arc<>来处理这种情况,我认为这有点过度设计,但我还没有看到更好的方式。 - YvesQuemener
将结构体C分配到堆上,而不是栈上,并使用RC。 - towry

8

在#rust IRC上与Manishearth和eddyb沟通后,我认为结构体无法存储自身或部分自身的引用。所以在Rust类型系统内,您试图做的是不可能的。


嗨,Rufflewind,如果无法存储结构体本身的一部分的引用,你知道有什么替代方法吗? - nybon
1
@nybon 最典型的解决方案之一是直接引用该值(即在上面的示例中使用 self.b.c,完全省略 self.c),或者如果不希望这样做,则提供一个方法按需生成对 C 的引用(并且这些引用可以正确地注释为结构体的生命周期)。 - GrandOpener
这是一个问题。我正在使用libgit2(git2),一切都指向仓库。所以,如果我想创建自己的包装器,存储一些像头部、分支、图表的最后一次提交等东西...一切都需要与仓库相关联。 - undefined

2

虽然来晚了(从未来回复),但我对Rust还是很新的,但我正在努力学习(有点儿)。在此基础上构建答案,至少在编译方面对我有效。

impl<'a> A<'a> {
fn new() -> A<'a> {
    let c1:&'a C = &C;
    A {
        c: c1,
        b: B { c: c1 },
    }
}

}


2
这个...在95%的情况下是行不通的。它只能工作是因为C很特殊 - Chayim Friedman

0
看看ouroboros的木箱:
use ouroboros::self_referencing;

struct C;

struct B<'b> {
    c: &'b C,
}

#[self_referencing]
struct A {
    c: C,

    #[borrows(c)]
    #[covariant]
    b: B<'this>,
}

fn main() {
    let c = C;

    let a = A::new(c, |c| B { c: &c });
}

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