在Rust中返回对枚举变体的引用是个好主意吗?[duplicate]

ruarlubt  于 2023-02-16  发布在  其他
关注(0)|答案(1)|浏览(170)

此问题在此处已有答案

Why can I return a reference to a local literal but not a variable?(1个答案)
4天前关闭。
我正在Rust中制作一个井字游戏作为一个初学者项目,当我遇到这个问题时,我正在计算游戏的状态(赢,平局等)。我首先编写了以下代码:

fn status(&self) -> Status {
    if Board::has_won(self.x_board) {
        Status::Won(true)
    } else if Board::has_won(self.o_board) {
        Status::Won(false)
    } else if self.is_full() {
        Status::Draw
    } else {
        Status::None
    }
}

然后我修改它以返回一个引用:

fn status(&self) -> &Status {
    if Board::has_won(self.x_board) {
        &Status::Won(true)
    } else if Board::has_won(self.o_board) {
        &Status::Won(false)
    } else if self.is_full() {
        &Status::Draw
    } else {
        &Status::None
    }
}

我这样做是为了将self的生存期和返回值链接在一起,这样,如果电路板的状态发生了变化,就不能再使用对status的引用。
然后我试了这个代码:

fn status(&self) -> &Status {
    let status;
    if Board::has_won(self.x_board) {
        status = Status::Won(true)
    } else if Board::has_won(self.o_board) {
        status = Status::Won(false)
    } else if self.is_full() {
        status = Status::Draw
    } else {
        status = Status::None
    }

    &status
}

它导致了一个编译器错误,说它不能返回一个引用到拥有的数据。为什么第一个代码片段没有这个问题?

fslejnso

fslejnso1#

我猜第一个代码段受lifetime extension约束,而第二个代码段是对显式局部变量的引用,因此已经有了显式的生存期。生存期扩展的规则很复杂,所以我让其他人来讨论它的细节。相反,我想提出一个稍微不同的设计。
你所做的一切令人钦佩。事实上,这是令人敬畏的。我从来没有想过把这样一个状态变量绑定到一个带有引用的数据结构上。也就是说,你在这里对Rust撒了一点谎,我们可以更直接地得到你想要的行为。
基本上,你有一个Status。它是一个值,它不是借用的,假装它是借用的只是有点尴尬。这没有错,只是尴尬。但是你希望它在语义上表现得像是借用的,即使它不是。我们可以使用PhantomData来做到这一点。
考虑一下这个问题。不要管你的Status枚举。把它做成CopyClone,因为它很好很简单,而且永远不要引用它。它仍然可以作为一个独立的数据结构合理地使用,只要你永远不要假设它被绑定到任何特定的电路板上。
现在,当您 * 希望 * 将其绑定到特定的板时,请使用一个新的结构体,我将其命名为BoardStatus

struct BoardStatus<'a> {
  status: Status,
  _phantom: PhantomData<&'a Status>,
}

一个BoardStatus实际上就是一个Status,它唯一的非零字段是一个Status,所以两者的表示应该是相同的。但是它也有一个PhantomDataPhantomData<T>是一个零大小的类型(意味着它在运行时不占用空间),它 * 假装 * 包含一个T以用于生存期。(一个拥有的状态值,no borrows),它 * 假装 * 在'a期间被借用。那么你的status方法可以有这样的返回类型。

fn<'a> status(&'a self) -> BoardStatus<'a>

或者,使用lifetime elision

fn status(&self) -> BoardStatus<'_>

这样一来,Status枚举和Status枚举之间就有区别了,前者是可独立测试的,没有额外的负担;以及BoardStatus结构,它 * 仍然 * 只是一个状态,但显式地绑定到一个板。
最好的部分是:这一切都有"零"开销。在运行时没有实际的指针,所以BoardStatus在运行时和Status一样高效,没有间接寻址。一个真正的零成本抽象。

相关问题