Unity中Dice与DiceSet类设计:单元素集合等价于单个对象的实现
我想在Unity里实现以下需求,但不确定可行性:
- 创建
Dice类和DiceSet类,DiceSet包含List<Dice>,希望仅含单个骰子的DiceSet能和Dice隐式等价。有没有办法在列表只有一个元素时返回该元素,否则返回列表(比如用Linq)? - 当前
Dice和DiceSet没有继承关系,我考虑让Dice继承自DiceSet并限制集合内骰子数量为1,但担心单个Dice会在不该被当作DiceSet的场景中被误判,是不是我想多了? - 另外希望两个
Dice相加能生成DiceSet,是不是应该让DiceSet包含List<DiceSet>? - 补充:我希望能从字符串解析生成骰子(比如"3d6+1"会创建包含3个6面骰子且带+1修正值的
DiceSet)。
后来我考虑放弃DiceSet的概念,让Dice自身包含集合,哪怕只有一个骰子也还是Dice类。下面是我的设计示例,请问这个思路对吗?
public class Dice : IDice, IRollable, IModifiable { // 存储每种骰子的数量 public Dictionary<Dice, int> Set { get; set; } // 实现IDice接口 public int NumSides => SideValues.Count; public List<int> SideValues { get; private set; } // 实现IModifiable接口 public int Modifier { get; set; } public int ModifiedValue => RolledValue + Modifier; // 实现IRollable接口 public int RolledValue { get; protected set; } public void Roll() { RolledValue = SideValues[Random.Range(0, NumSides + 1)]; } // Dice构造函数 public Dice(int sides = 6, int quantity = 1, int modifier = 0) { SideValues = new List<int>(Math.Max(sides, 1)); for(int i = 1; i < NumSides + 1; i++) { SideValues.Add(i); } Modifier = modifier; AddDice(this, quantity); } // 向集合中添加骰子 public void AddDice(Dice dice, int quantity){ if(Set.ContainsKey(dice)) Set[dice] += quantity; else Set.Add(dice, quantity); } // 字符串转换 public override string ToString() { // 待实现 } public void FromString(string diceString){ // 待实现 } }
思路点评与优化建议
你的思路方向是对的,用单一Dice类同时承载单个骰子和骰子集合的逻辑,能简化类型转换的问题,但当前代码存在几个关键问题需要修正:
循环依赖问题:
Dictionary<Dice, int> Set用Dice作为键,会导致构造函数调用AddDice(this, quantity)时,尝试把当前未完全初始化的Dice实例作为键存入集合,引发逻辑混乱;而且Dice默认的相等性判断是引用相等,后续添加相同规格的骰子会被当成不同键存储。优化方案:单独定义
DiceSpec类存储骰子核心规格(面数、面值),用它作为Dictionary的键,避免循环依赖:public class DiceSpec { public int NumSides { get; } public List<int> SideValues { get; } public DiceSpec(int sides) { NumSides = Math.Max(sides, 1); SideValues = Enumerable.Range(1, NumSides).ToList(); } // 重写相等判断和哈希值,确保相同规格的骰子被视为同一键 public override bool Equals(object obj) => obj is DiceSpec spec && NumSides == spec.NumSides; public override int GetHashCode() => NumSides.GetHashCode(); }随后将
Dice类里的Set改为Dictionary<DiceSpec, int>。Roll方法逻辑错误:
Random.Range(0, NumSides + 1)的上限是开区间,会导致索引越界(SideValues的最大索引是NumSides-1),应改为Random.Range(0, NumSides)。构造函数初始化问题:
Set字段未在构造函数中初始化,调用AddDice时会抛出空引用异常,需在构造函数开头添加Set = new Dictionary<DiceSpec, int>();。单个骰子与集合的逻辑统一:当前
RolledValue只记录单个骰子的结果,当Dice作为集合时,需要修改Roll方法遍历所有骰子规格计算总和:public void Roll() { RolledValue = 0; foreach(var pair in Set) { var spec = pair.Key; var count = pair.Value; for(int i = 0; i < count; i++) { RolledValue += spec.SideValues[Random.Range(0, spec.NumSides)]; } } }加法逻辑实现:重载
+运算符,让两个Dice实例相加生成新的Dice集合:public static Dice operator +(Dice a, Dice b) { var result = new Dice(); // 复制a的骰子集合 foreach(var pair in a.Set) { result.AddDice(pair.Key, pair.Value); } // 复制b的骰子集合 foreach(var pair in b.Set) { result.AddDice(pair.Key, pair.Value); } // 合并修正值 result.Modifier = a.Modifier + b.Modifier; return result; }字符串解析逻辑:
FromString方法可以用正则表达式匹配(\d*)d(\d+)([+-]\d+)?格式的字符串,拆分出骰子数量、面数、修正值,再生成对应的Dice实例。
另外,关于你最初考虑的Dice继承DiceSet的方案,确实可能带来类型混淆——比如在需要单个骰子的场景中传入了集合类型,反而增加逻辑复杂度,所以用单一Dice类承载集合逻辑的方案更简洁。
内容的提问来源于stack exchange,提问作者Martin Bocanegra

