在继承 类 中强制继承成员的类型安全

Enforcing type safety of inherited members in inherited classes

我正在努力遵守良好的 OO 设计原则和设计模式等。因此,在开发我的这个 C# 应用程序时,我经常可以找到设计和架构问题的多种解决方案,我一直想找到并实施更“规范”的一个,以期构建高度可维护和灵活的软件,并成为一个更好的 OO 程序员.

所以假设我有这两个抽象 classes:CharacterWeapon

abstract class Weapon
{
    public string Name { get; set; }
}

abstract class Character
{
    public Weapon weapon { get; set; }
}

派生的 classes 可能是 SwordStaff 来自 Weapon,以及 WarriorMage 来自 Character等(这都是假设,与我的实际软件无关!)。

每个 Character 都有一个 Weapon,但是对于 Character 的每个实现,我知道它会有什么 Weapon 的实现。例如,我知道(并且我想强制执行!)在运行时 Warrior 的每个实例都会有一个类型为 SwordWeapon。我当然可以这样做:

class Sword : Weapon
{
    public void Draw() { }
}

class Warrior : Character
{
    public Warrior()
    {
        weapon = new Sword();
    }
}

但是这样一来,每次我想在 Warrior 中正确使用我的 Weapon 对象时,我都必须执行强制转换,我认为这是一种不太好的做法.我也没有办法防止自己搞砸,那就是没有类型安全!

理想的解决方案是能够用 Sword weapon 属性 覆盖 Warrior class 中的 Weapon weapon 属性 ,这样我就有了类型安全,如果用户将我的 Warrior 用作 Character,他仍然可以将我的 Sword 用作 Weapon。遗憾的是,C# 似乎不支持这种构造。

所以这是我的问题:这是某种 classical OO 问题,有名称和有据可查的解决方案吗?在那种情况下,我非常想知道问题的名称和解决方案。一些好的阅读链接 material 会很有帮助! 如果不是,您会提出什么样的 class 设计来以优雅和惯用的方式维护功能并强制类型安全?

感谢阅读!

只需使用generics type constraint!这是一个例子

public abstract class Weapon
{
    public string Name { get; set; }
}

public interface ICharacter
{
    Weapon GetWeapon();
}

public interface ICharacter<out TWeapon> : ICharacter
    where TWeapon : Weapon
{
    TWeapon Weapon { get; } // no set allowed here since TWeapon must be covariant
}

public abstract class Character<TWeapon> : ICharacter<TWeapon>
    where TWeapon : Weapon
{
    public TWeapon Weapon { get; set; }
    public abstract void Fight();
    public Weapon GetWeapon() { return this.Weapon; }
}

public class Sword : Weapon
{
    public void DoSomethingWithSword()
    {
    }
}

public class Warrior : Character<Sword>
{
    public override void Fight()
    {
        this.Weapon.DoSomethingWithSword();
    }
}

更新:这个问题是 a series of articles on my blog 的灵​​感来源。感谢您提出有趣的问题!

is this some kind of classical OO problem, with a name and a well-documented solution?

您 运行 遇到的根本问题是,在 OO 系统中很难向 sub[ 添加 restriction =54=]。说 "a character can use a weapon" 然后 "a warrior is a kind of character that can only use a sword" 很难在 OO 系统中建模。

如果您对此背后的理论感兴趣,您将需要查找 "Liskov Substitution Principle"。 (还有关于为什么 "Square" 不是 "Rectangle" 的子类型的任何讨论,反之亦然。)

但是有很多方法可以做到。这是一个。

interface ICharacter
{
    public IWeapon Weapon { get; }
}
interface IWeapon {  }
class Sword : IWeapon { }
class Warrior : ICharacter 
{
    IWeapon ICharacter.Weapon { get { return this.Weapon; } }
    public Sword Weapon { get; private set; }
}

现在 public 战士的表面区域有一把武器,它始终是一把剑,但是任何使用 ICharacter 界面的人都会看到 Weapon 属性 类型为 IWeapon.

这里的关键是摆脱"is a special kind of"关系,走向"can fulfill the contract of"关系。与其说 "a warrior is a special kind of character",不如说 "a warrior is a thing that can be used when a character is required".

综上所述,您正在着手解决一个充满迷人问题的世界,您会发现 OO 语言在表达这些事物方面的真正表现有多么糟糕。一旦您开始提出更复杂的问题,例如 "what happens when a warrior uses a sharp sword to attack an orc wearing leather armor?" 并发现您有 four class 层次结构——角色、武器、怪物、盔甲——所有这些都有影响结果的子classes。虚拟方法只允许您在 一个 运行时类型上进行分派,但您会发现您需要两层、三层、四层或更多级别的运行时类型分派。

它变得一团糟。考虑不要 尝试在类型系统中捕获太多内容。

为了文档和将来的参考,我发布了我最终可能会使用的解决方案:

abstract class Weapon {}

abstract class Character
{
    public abstract Weapon GetWeapon();
}

class Sword : Weapon {}

class Warrior : Character
{
    public Sword Sword { get; private set; }

    public override Weapon GetWeapon()
    {
        return Sword;
    }
}

在我看来,唯一的缺点是 Warrior 类型的对象有一个 Sword Sword 属性 一个 Weapon GetWeapon() 方法。在我的例子中,这只是一个表面问题,因为 属性 和方法 return 在调用时都是对同一对象的引用。

欢迎大家提出意见!

问题是 "warriors can only use swords" 限制不应该在类型系统中建模。

你有"characters are things that have weapons"

abstract class Character {
   public Weapon weapon { get; set; }
}

但您真正想要的是说 "characters are things that have weapons, but with some restriction" 之类的话。我建议根本不要有像 "warrior" 和 "sword" 这样的具体类型,而是那些具体类型应该是 instances:

sealed class Weapon {
   public string Name { get; set; }
}

sealed class Character {
   const int sSkillToUseWeapon = 50;
   readonly Dictionary<string, int> mWeaponSkills = new Dictionary<string, int>();
   Weapon mCurrentWeapon;

   public Weapon CurrentWeapon {
      get {
         return mCurrentWeapon;
      } set {
         if (!mWeaponSkills.ContainsKey(value.Name))
            return;

         if (mWeaponSkills[value.Name] < mSkillToUseWeapon)
            return;

         mCurrentWeapon = value;
      }
   }

   public void SetWeaponSkill(string pWeaponName, int pSkill) {
      if (mWeaponSkills.ContainsKey(pWeaponName))
         mWeaponSkills[pWeaponName] = pSkill;
      else
         mWeaponSkill.Add(pWeaponName, pSkill);
   }
}

然后您可以声明它们的实例以供使用和重用:

var warrior = new Character();
warrior.SetWeaponSkill("Sword", 100);
warrior.SetWeaponSkill("Bow", 25);

var archer = new Character();
archer.SetWeaponSkill("Bow", 125);
archer.SetWeaponSkill("Sword", 25);
archer.SetWeaponSkill("Dagger", 55);
//etc...

A Warrior 是具有特定武器技能集的角色实例。

当您以这种方式处理事情时,您必然会遇到的另一个问题是 "Rusty Sword" 和 "A Rusty Sword" 之间存在差异。换句话说,即使"Rusty Sword"是"Weapon"的实例,但"Rusty Sword"的一般概念与实际躺在地上的生锈剑还是有区别的。 "Goblin" 或 "Warrior".

也一样

我自己使用的术语是 "Stock____" 和 "Placed____",因此定义 "class" 武器的 StockWeapon 和 PlacedWeapon 之间存在差异,它是游戏世界中 StockWeapon 的实际 "instance"。

Placed objects 然后 "have" Stock objects 的实例,这些对象定义了它们的共同属性(基础伤害、基础耐久性等),并具有其他属性来定义它们的当前状态,独立于其他类似项目(位置) 、耐久度损坏等)

(Here 是一个关于使用 OOP 模式进行游戏编程的好网站。(哎呀我想我在写这篇文章的一半时忘记了游戏编程部分只是一个类比。不过我确实打算希望解释将保持类似。))

我认为您在 C# 中缺少对 Covariant return type 的支持。请注意,您可以将 属性 的 get 视为一种特殊的方法。