【问题标题】:We use inheritance when A (derived class) "is a" B (base class). What do we do when A "can be" B or C?当 A(派生类)“是”B(基类)时,我们使用继承。当A“可以”是B或C时,我们该怎么办?
【发布时间】:2026-02-02 17:00:01
【问题描述】:

抱歉这个丑陋的问题,但我不知道如何措辞。我举个例子来说明我的意思:

人类可以是法师或战士,因此法师和战士可以从人类继承。但是,如果兽人也可以兼得呢?我们不能说“人就是战士”或“战士就是人”。 Orc 和 Human(或父类 Humanoid)是否继承了所有技能,然后选择使用什么?

我不知道是否应该标记特定语言,因为这是关于 oop 的一般问题,但由于不同的语言可以有不同的方法来解决相同的问题,我更喜欢从 C++ 的角度来回答。

【问题讨论】:

  • 这是一个有点争议的话题,但是 c++ 确实支持多重继承:cprogramming.com/tutorial/multiple_inheritance.html - 使用多重继承,人类可以是战士或法师,而兽人可以也可以是战士或法师。不一定推荐这样做。
  • @neminem 但是,如果您只为每个子对象使用其中一个类,可以从一堆类继承吗?
  • “好的”由您决定。这当然是允许的 - 在这种情况下,你的“Humanoid”类可能更干净拥有“Class”和“Race”属性,然后拥有这些类和种族从它们各自的基类继承,而不是让你的 Humanoid 直接从类和种族继承。但你可以这样做。 (有时我真的希望 C# 确实支持多重继承......)
  • 这里多重继承的问题是你需要有 N^2 个 final 类,所以每个种族和类的组合都会有一个类(例如@ 987654322@)。这很快就会失控。

标签: c++ oop inheritance


【解决方案1】:

改进您的建模

  1. 抽象类种族,具体类人类、兽人等...
  2. 抽象类,具体类法师、战士等……

例如,在计算法师的统计数据时,向 Race(不是人类或兽人)询问 int get_intelligence_bonus()Race 中的抽象函数)之类的东西。如果可能的话,应该在 RaceClass 之间进行交互,而不是具体的对应对象。

使用构图来实际构建你的角色:

Character::Character( std::unique_ptr<Race> r, std::unique_ptr<Class> c ) 
{
  // calculate stats, etc...
}

您可以使用纯 OOP 进行动态绑定,或者如果您愿意,也可以使用模板进行静态时间绑定,但这是一个正交的实现问题。无论哪种方式,您都需要正确设置类层次结构。

如果您是 C++ 新手,或者您只需要在构造函数中使用种族和类,您可以使用 const&amp; 而不是 std::unique_ptr&lt;&gt;。但如果你认真学习 C++,请阅读所有权语义变量生命周期,以便更好地理解std::unique_ptr&lt;&gt;

http://en.cppreference.com/w/cpp/memory/unique_ptr

【讨论】:

  • 所以角色会在创建的那一刻得到它的统计数据,而不是实际上是RaceClass?我喜欢它,但模板的东西对我来说太高级了:) 另外,我应该阅读unique_ptr,但我认为它指向一个不打算创建对象的类?
  • 这是正确的:更喜欢组合而不是继承。使用std::unique_ptr&lt;&gt; 的替代方法是使用const&amp;,但您可能需要克隆您的Race 或Class 实例或持有对它的引用。相反,std::unique_ptr&lt;&gt; 允许您的 Character 实例拥有传入的内存。这是更好的封装并且更有意义:当您的 Character 实例消失时,Race 和 Class 实例也会消失。我假设您稍后将需要使用这些实例。如果你只在构造函数中需要它们,那么传入一个const&amp; 就可以了。
  • en.cppreference.com/w/cpp/memory/unique_ptr 回想一下自动(局部)变量会发生什么:当它们超出范围时,会调用析构函数并回收内存。 std::unique_ptr&lt;&gt; 的行为是这样的 - 当它超出范围时,将调用析构函数并回收内存。
【解决方案2】:

那时我爆发了policy-based design。然后我可以拥有Human&lt;Warrior&gt;Orc&lt;Warrior&gt;Orc&lt;Mage&gt;。 (或者如果它更有意义,可以转过来:Warrior&lt;Human&gt; 等)但那是我知道编译时发生了什么。

如果我直到运行时才知道,我会使用一个类 Humanoid,它有一个 Race(子类是OrcHuman 等)并且有一个RPGClass(子类MageWarrior等)

在其他语言中,有诸如protocols之类的东西可以定义对象的接口,这样你就不必定义一个充满抽象虚方法之类的基类。所以RPGClassRace 将是分别用于与MageHuman 等类接口的协议。策略只是在编译时解析的协议。

附:我不知道你的例子是多么隐喻(或不是)......

【讨论】:

  • 是的,这是主要问题。在运行时间之前我无法知道谁会是什么。他们甚至可以多次更改。我想组合是要走的路吗?是的,我的问题有点模糊,但我认为更具体的问题不会有太大帮助。
【解决方案3】:

在这个例子中,我会看到组合或多重继承的用途。

也就是说,我会有一个法师和战士职业以及一个人类和兽人职业。如果我想要一个兽人法师,我从法师和兽人类中继承。

也就是说,在实践中,多重继承可能很麻烦(参见此处:What is the exact problem with multiple inheritance?)。因此,我的实现中的兽人法师类可能会有一个兽人和法师私有对象来执行兽人和法师特定的事情。

【讨论】:

  • 我忘了补充一点,直到运行时我才知道我需要什么。我可能需要所有组合。
【解决方案4】:

一个存在有一个职业(阶级)和种族。这里不需要'is-a'。

会有类实例模式的使用,但它们可能与 C++(或任何其他语言)继承规则不完全一致,因此请手动运行它,在用例出现时对其进行限制或增强,并愿意如果碰到缠结,就把它扔掉。

【讨论】:

  • 但是战士可以成为一个兽人或者可以成为一个人类,然后例如在兽人或人类的向量中。我喜欢从两个都有具体实现的基类(种族和职业)继承的想法(我认为 Jonathan 和 kfm 提出了类似的建议)。
【解决方案5】:

如果我遵循,您正在尝试定义一个类 Warrior可以是人类或兽人,但应该继承相关类。

如果这是您想要做的,正确的做法是编写一个通用的Warrior 类:

template<typename Race>
class Warrior : public Race
{
    // Warrior attributes & methods
};

通过这样做,您将能够实例化人类战士 (Warrior&lt;Human&gt;) 以及兽人战士 (Warrior&lt;Orc&gt;) 或任何其他种族的战士。

但是,Warrior&lt;Human&gt;Warrior&lt;Orc&gt; 将被视为完全不同的类,它们不属于同一继承树。


现在,也许这不是您想要做的。也许您希望能够操纵像vector&lt;Warrior*&gt; 这样的容器来无差别地处理人类和兽人战士并利用多态性。在这种情况下,反转继承并拥有一个从 Class 继承的模板类 Human&lt;Class&gt; 可能是有意义的。

但是,如果您还希望能够操作像 vector&lt;Human*&gt; 这样的容器呢?好吧,在这种情况下,您希望根据种族或类别对字符进行多态处理,因此它应该从两者继承!

template<typename Race, typename Class>
class Character : public Race, public Class
{
    // Character attributes & methods
};

然后,您可以安全地拥有一个用于角色类的继承树和一个用于种族的继承树。


再说一次,如果您希望能够操纵诸如vector&lt;Character*&gt; 之类的东西来一次处理所有 个字符,那是行不通的。最简单的方法是创建一个类AbstractCharacter,所有字符都将从该类继承。但是,如果在你做这件事的时候,你希望你的角色能够改变职业呢?然后你必须改变你的设计:也许你的角色不是一个既是战士又是人类的角色,而是一个拥有“人类”种族和“战士”职业的角色。

在这种情况下,为你的角色设置种族和职业属性:

class Character
{
    Race* characterRace;
    Class* characterClass;

    // Character attributes & methods
};

其中RaceClass 分别是种族和类的继承树的根。而且您仍然可以使用多态性。

(注意:虽然我这样写是为了提高可读性,但在实际实现中最好使用智能指针而不是常规指针)

编辑:当我谈到更改角色的类别时,我指的是在运行时。这也意味着你可以在运行时创建你想要的类和种族的角色,而不是简单。

【讨论】:

    【解决方案6】:

    传统的 OOP 术语包括“is a”和“has a”。但没有“可以”。

    您几乎没有提供有关您的特定编码需求的详细信息。但是如果我需要这样的东西,我仍然会使用继承。我会创建一个基类来共享这两个可能项目的共同品质。然后我会为每个可能的特定情况声明专门的派生类。

    然后,我将为每个特定案例创建适当的数据类型,这些数据类型源自我的常见案例。我会根据项目的类型推导出特定的类型。然后我可以创建一个包含不同特定类型的基类集合。

    如果没有通用的基类,我可能会使用接口而不是基类。

    最后,如果您没有提供您想要完成的具体示例,就不可能提供具体示例。

    【讨论】:

      最近更新 更多