You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何NLL无法在match分支中间结束可变借用?

Rust NLL未按预期释放可变引用问题

问题代码

核心代码片段:

fn update(&mut self) -> bool {
    let now = std::time::Instant::now();
    let delta_time = now - self.last_update;
    self.last_update = now;

    match self.state {
        ClientState::MainMenu(ref mut menu) => {
            // TODO: handle menu transition and rendering
            true
        }
        ClientState::Playing(ref mut game) => {
            game.update(delta_time);
            self.render(game)
        }
        ClientState::PauseMenu(ref mut menu, ref mut game) => {
            // TODO: do not update/render game in SP mode while in the menu (full pause)
            game.update(delta_time);
            self.render(game)
            // TODO: handle menu transition and rendering
        }
    }
}

fn render(&self, game: &Game) -> bool {
    match self.renderer_to_surface.render(game) {
        Ok(_) => {}
        Err(wgpu::SurfaceError::Lost) => { 
            self.renderer_to_surface.reconfigure();
        }
        Err(_) => return false,
    }

    true
}

完整可复现代码:

use std::time::Duration;

pub struct Game {
    pub delta_time: Duration,
}

pub struct Menu {
    //...
}

impl Game {
    pub fn update(&mut self, delta_time: Duration) -> bool {
        self.delta_time = delta_time;
        false
    }
}

pub enum ClientState {
    MainMenu(Menu),
    Playing(Game),
    PauseMenu(Menu, Game),
}

pub struct Client {
    last_update: std::time::Instant,
    state: ClientState,
}

impl Client {
    /// Return false if there is a fatal error and app should be closed
    fn update(&mut self) -> bool {
        let now = std::time::Instant::now();
        let delta_time = now - self.last_update;
        self.last_update = now;

        match self.state {
            ClientState::MainMenu(ref mut menu) => {
                // TODO: handle menu transition and rendering
                true
            }
            ClientState::Playing(ref mut game) => {
                game.update(delta_time);
                self.render(game)
            }
            ClientState::PauseMenu(ref mut menu, ref mut game) => {
                // TODO: do not update/render game in SP mode while in the menu (full pause)
                game.update(delta_time);
                self.render(game)
                /// TODO: handle menu transition and rendering
            }
        }
    }

    fn render(&self, game: &Game) -> bool {
        // [...]
        true
    }
}

问题说明

game.update是可变方法,因此在Playing和PauseMenu分支中需要以可变引用访问game。调用该方法后,self.render是不可变方法,不再需要可变借用,但编译器在self.render(game)处报错:self无法被不可变借用,因为ref mut game导致self仍处于可变借用状态。

疑问:既然可变引用在调用update后不再被使用,*NLL(非 lexical lifetimes)*应该在发起新的不可变借用前释放可变借用,为何未生效?

原因分析

Rust中,match分支里通过ref mut game创建的可变借用,其生命周期覆盖整个分支块,而非到最后一次使用game的位置。NLL优化的是普通变量的借用生命周期(即变量最后一次使用后就释放借用),但模式绑定产生的借用生命周期由块作用域决定,即使后续不再使用该引用,借用也会持续到分支结束。

这意味着在self.render(game)执行时,self.state的可变借用(通过ref mut game)仍然有效,而self.render需要对self进行不可变借用,违反了“同一时间只能有一个可变借用或多个不可变借用”的核心规则。

解决方案

方案1:显式缩短可变借用的生命周期

将可变引用的使用限制在子作用域内,让借用在子作用域结束时释放:

ClientState::Playing(ref mut game) => {
    {
        let game = game;
        game.update(delta_time);
    } // 可变借用在此处释放
    self.render(game)
}

方案2:显式转换为不可变引用

通过&*game将可变引用转换为不可变引用,让编译器明确知晓可变借用已结束:

ClientState::Playing(ref mut game) => {
    game.update(delta_time);
    self.render(&*game)
}

方案3:重新匹配获取不可变引用

在调用render前,重新对self.state进行不可变匹配(适用于分支逻辑允许的场景):

ClientState::Playing(ref mut game) => {
    game.update(delta_time);
    // 重新匹配获取不可变引用
    if let ClientState::Playing(ref game) = self.state {
        self.render(game)
    } else {
        // 理论上不会走到这里,因为刚匹配过Playing状态
        true
    }
}

方案2是最简洁且常用的解决方式。

内容的提问来源于stack exchange,提问作者endyx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 09:40:53