国际象棋项目循环依赖规避及类双向引用同步问题咨询
问题解答
当前设计的缺陷
- 双向引用确实存在引用不同步风险:如果有其他地方直接修改
Piece.Tile或Tile.Piece而不走统一的移动逻辑(比如调试临时代码、其他开发者新增的方法),就会出现一方更新另一方未更新的情况,导致数据不一致,后续查找必然出错。 - 你当前的
Move方法存在潜在顺序隐患:先调用p.Move()更新棋子的Tile引用,再更新Tile的Occupant,虽然现在逻辑成立,但后续若修改p.Move()逻辑或调整步骤顺序,很容易引入bug。
更优方案:单一数据源+封装访问逻辑
推荐采用单一数据源,将棋子位置信息仅存储在一处,同时通过封装方法保证双向查找的O(1)效率,从根源避免不一致问题。以下是两种可行思路:
思路1:Tile存Piece引用,Grid维护棋子到Tile的映射
- 核心设计:
Tile类保留Piece引用,HasOccupant基于此判断。Grid类维护Dictionary<Piece, Tile>映射表,专门存储棋子与对应格子的关联关系。
- 双向查找实现:
- 格子找棋子:直接取
Tile.Piece,O(1)。 - 棋子找格子:从Grid的字典中查询,O(1)。
- 格子找棋子:直接取
- 修正后的移动逻辑:
public void Move(Tile fromTile, Tile toTile, Piece p){ if (toTile.HasOccupant){ throw new Exception("Tile should be empty before moving a piece to it"); } // 移除原映射 _pieceToTileMap.Remove(p); // 更新Tile的引用 fromTile.UnsetOccupant(); toTile.SetOccupant(p); // 添加新映射 _pieceToTileMap.Add(p, toTile); // 若Piece需感知位置,仅做通知而非存储引用 p.UpdatePosition(toTile); } - 优势:所有位置变更通过Grid统一处理,映射表与Tile引用同步更新;Piece无需存储Tile引用,避免自行修改的风险。
思路2:Piece存Tile引用,Grid维护格子到棋子的映射
- 核心设计:
Piece类保留Tile引用,Tile仅存储自身坐标等基础信息,不存Piece。Grid类维护Dictionary<Tile, Piece>映射表,记录格子的占用情况。
- 双向查找实现:
- 格子找棋子:从Grid的字典中查询,O(1)。
- 棋子找格子:直接取
Piece.Tile,O(1)。
- 修正后的移动逻辑:
public void Move(Tile fromTile, Tile toTile, Piece p){ if (_tileToPieceMap.ContainsKey(toTile)){ throw new Exception("Tile should be empty before moving a piece to it"); } // 移除原映射 _tileToPieceMap.Remove(fromTile); // 更新Piece的引用 p.Move(toTile); // 添加新映射 _tileToPieceMap.Add(toTile, p); } - 优势:同样保证单一数据源,所有位置变更由Grid管控;Tile类更简洁,职责更单一。
是否应采用单一数据源?
是的。单一数据源是解决双向引用不一致问题的核心方案,它将状态的唯一权威存储点集中在Grid(棋盘的全局管理者),所有状态变更都通过统一接口处理,从根源杜绝不同步可能。同时只要维护好对应的字典映射,完全可以满足O(1)的双向查找需求。
额外建议:将Move方法放到Grid类中,因为Grid作为棋盘管理者,处理移动逻辑更符合职责单一原则,也能更好地控制状态变更。
内容的提问来源于stack exchange,提问作者zdani
相关产品推荐
相关产品推荐

