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

游戏开发中如何避免只读State类返回可写引用并安全暴露可读内部数据

解决方案

一、安全暴露深层只读数据,解决State类臃肿问题

核心思路

通过只读视图接口+分层封装的方式,把原本堆在State类里的查询逻辑下放到对应的子模块封装类中,同时完全避免泄漏内部可写引用。

具体实现步骤

  • 为每一层内部数据定义只读接口,仅暴露允许读取的属性和查询方法,不包含任何修改逻辑。
  • State类仅对外暴露这些只读接口类型的属性,不直接返回内部的可写集合或类实例。
  • 把对应的查询逻辑封装到只读接口的内部实现类中,这些实现类作为State的私有嵌套类,能访问State的私有成员,但对外完全隐藏实现细节。

代码示例

// 定义公开的只读接口,外部仅能看到这些成员
public interface IReadOnlyUnitMap
{
    bool IsUnitAt(Vector2 pos);
    int GetUnitOwnerId(Vector2 pos);
    int GetUnitRemainingMobility(Vector2 pos);
}

public partial class State
{
    public int Turn {get; private set;} = 1;
    private Dictionary<Vector2, FactionsMgr.Faction> _unit_map = new();
    // 对外仅暴露只读接口
    public IReadOnlyUnitMap UnitMap { get; }

    public State()
    {
        // 初始化内部封装的只读实现
        UnitMap = new UnitMapImpl(this);
    }

    // 私有嵌套类实现只读接口,持有State的引用访问私有成员
    private class UnitMapImpl : IReadOnlyUnitMap
    {
        private readonly State _state;
        public UnitMapImpl(State state) => _state = state;

        public bool IsUnitAt(Vector2 pos) => _state._unit_map.ContainsKey(pos);

        public int GetUnitRemainingMobility(Vector2 pos)
        {
            if (!IsUnitAt(pos))
            {
                GD.Print("Warning: GetUnitRemainingMobility asked for unknown unit: ", pos);
                return -1;
            }
            var owner = _state._unit_map[pos];
            var unit = owner.units[pos];
            return UnitsMgr.GetMaxMobility(unit.type) - unit._taken_movement;
        }

        public int GetUnitOwnerId(Vector2 pos)
        {
            return IsUnitAt(pos) ? _state._unit_map[pos].Id : -1;
        }
    }
}

调用方式

外部读取数据时直接通过分层的只读属性访问即可,逻辑归属清晰:

// 外部调用示例
int mobility = state.UnitMap.GetUnitRemainingMobility(pos);
int ownerId = state.UnitMap.GetUnitOwnerId(pos);

二、Command类权限优化,无需嵌套在State内部

核心思路

通过接口隔离权限的方式,拆分只读和可写两个State接口,仅将可写接口开放给Command使用,既保证修改权限可控,又能把Command类从State内部拆分出去。

具体实现步骤

  • 定义公开的IReadOnlyState接口,包含所有对外暴露的只读属性和子视图(比如上面的UnitMap),供UI、逻辑查询等模块使用。
  • 定义内部的IWritableState接口,仅包含允许Command调用的修改方法,不对外公开。
  • State类同时实现这两个接口,修改方法用显式接口实现,避免外部正常调用时看到修改接口。
  • Command接口的Execute方法仅接收IWritableState参数,所有具体Command类直接实现该接口即可,无需嵌套在State内部。

代码示例

// 公开的只读接口,给所有非修改模块使用
public interface IReadOnlyState
{
    int Turn { get; }
    IReadOnlyUnitMap UnitMap { get; }
}

// 内部的可写接口,仅给Command使用
internal interface IWritableState
{
    void ConsumeUnitMobility(Vector2 pos, int cost);
    void SetTurn(int turn);
}

// State实现两个接口
public partial class State : IReadOnlyState, IWritableState
{
    // 显式实现可写接口的方法,外部正常访问State实例时看不到这些方法
    void IWritableState.ConsumeUnitMobility(Vector2 pos, int cost)
    {
        if (_unit_map.TryGetValue(pos, out var owner))
        {
            owner.units[pos]._taken_movement += cost;
        }
    }

    void IWritableState.SetTurn(int turn) => Turn = turn;
}

// Command接口独立定义,不需要放在State内部
public interface ICommand
{
    void Execute(IWritableState state);
}

// 具体命令实现
public class MoveUnitCommand : ICommand
{
    private readonly Vector2 _fromPos;
    private readonly int _cost;

    public MoveUnitCommand(Vector2 fromPos, int cost)
    {
        _fromPos = fromPos;
        _cost = cost;
    }

    public void Execute(IWritableState state)
    {
        state.ConsumeUnitMobility(_fromPos, _cost);
        // 其他移动逻辑
    }
}

额外优化

如果还是希望保持Command的访问权限控制,又不想拆接口,可以利用C#的分部类特性,把所有Command相关的代码放到单独的State.Commands.cs文件中,既保持嵌套类的访问权限,又不会让State的主文件过于臃肿。


注意事项

  • 所有对外返回的引用类型都必须是只读接口或者值类型,禁止直接返回内部的可写类实例、原始集合(List/Dictionary等),如果需要返回集合请用ReadOnlyCollection/ReadOnlyDictionary封装。
  • 深层嵌套的类(比如Faction、Unit)也需要对应做只读接口封装,避免返回内部可写实例导致意外修改。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 19:15:03