实例与继承

Instance vs. Inheritance

我正在学习 C# 并且难以理解继承与实例相比有哪些用例?

具体来说,我正在制作一个控制台角色扮演游戏,我目前的理解是我应该制作一个 superclass/baseclass npc,我从中继承了 3 个子类 mage , paladinthief, 其中我每个实例 High level mage, low level mage.

有道理还是我的逻辑有问题?如果交换了 inheritance/instance,这也会反过来吗?

将实例与继承进行比较类似于比较苹果和橙子。它们或多或少是无关的。继承用于定义 class 定义的继承权,而实例是当您创建其中一个 class 的实际实例时。

在考虑继承是否是正确的选择时,你需要问自己的一个(但到目前为止不是唯一的)问题是 subclass 和 superclass 共享一个 关系?如果答案是肯定的,您可能想要使用继承。如果答案是可能或否,那么您几乎可以肯定不会。

举几个例子。

q: class Mercedes 是否与 class Car.[=41 共享 is a 关系=] 答:也许吧!您更有可能只是有一个 class Car,它有一个 属性,它是 Make(和 Model 等)。

q: MyWebPage 是否与 class BaseWebPage
共享 is a 关系 答:可能,是的。这是使用继承的常见方式。 MyWebPage 覆盖基础 class 的某些功能以控制渲染(作为一个示例)

回到你的例子。 class Mage 是否与 Npc 共享 关系......答案是另一个“可能”。为了使使用继承有意义,您的 Mage 必须覆盖 Npc 的一些基本功能 - 您可能有类似于上面 Car 示例的内容。

全部 classes 继承,默认从Object

为了使用非抽象class(或任何继承的),您需要创建一个实例。

实例保存字段并定义函数,这些函数可以作为 wrappers/accessors 存储在该实例中的其他实例(称为组合)。


对于给定的例子,假设 NPC 有等级。所有 subclasses 然后从它扩展,因此从它继承属性。法师(和其他类型)有自己的属性

class NPC {
  int Level { get; set; }
  
  public NPC(int level) {
    Level = level;
  }
}

class Mage : NPC {
  String PrimaryMagic {get; set; }
 
  public Mage(int level, string magic) : base(level) {
    PrimaryMagic = magic;
  } 
}
var lowMage = new Mage(1, "fire");
var highMage = new Mage(50, "ultima");

但你也可以制作一个没有特定属性的通用水平角色

var nobody = new NPC(0);

你也可以取消法师 class 并给所有 NPC 一些 Spells 列表属性,例如默认为 empty/null.

var magicStudent = new NPC(1); // starts with no magic
// .. Game progresses
if (magicStudent.Level >= 5) {
  magicStudent.Spells.Add("cure"); // Based on some condition, update a list of spells. 
}

i do not understand use cases of inheritance. Thats what bugged me

您在另一个答案上发表了此评论;该答案讨论了何时确定继承候选人,而 OneCricketeer 的答案讨论了一个示例继承层次结构,但您可能仍然想知道“有什么意义?”

从本质上讲,关键是当您不关心事物是什么的细节时,它允许您以通用的方式对待事物。你不需要知道你手里的东西是刀、抹刀、信用卡等,就可以用它在三明治上涂蛋黄酱;我提到的所有这些东西都可以合理地用来舀起蛋黄酱并将其摊开。它们可以根据一些基本特性以通用方式处理,这些特性使它们成为很好的吊具(直边、相当刚硬的主体、防水和防油)

假设您有自己的角色类型,并且它们都来自 NPC。假设每一种玩家都有 Health,当它达到 0 时他们就死了。因为所有角色都有生命值,所以将它放在 NPC 上可能是有意义的。也许不同种类的角色有不同的初始生命值:

class Npc{
  public int Health{get;set;}
}

class Medic:Npc{
  Medic(){
    Health = 30;
  }
}

class Soldier:Npc{
  Soldier(){
    Health = 80;
  }
}

假设您的播放器组织了一个clan/posse/team,那么您的程序有例如:

var team = new Npc[10];

你的队伍最多可以有10种角色,可以任意组合。继承这个东西就是Soldier是-Npc,Medic是-Npc,所以现在你做了一个Npc数组,里面可以塞士兵和medc:

team[0] = new Soldier();
team[1] = new Medic();

假设有人向团队投掷炸弹,对每个人的健康造成 55 点伤害。它会直接杀死一个新的医生。我们不需要知道他们是什么样的玩家;所有 Npc 都有生命值,在这种通用的“带走一些生命值”场景中,医务人员和士兵都可以被视为 Npc:

void DropBombOn(Npc[] team){
  foreach(var npc in team) {
    npc.Health -= 25;
    if(npc.Health <= 0)
      Console.WriteLine("Character is dead");
  }
}

但是哪个角色死了?让我们修改 classes:

class Npc{
  public int Health{get;set;}
  public string Name{get;set;}
}

设置它们:

team[0] = new Soldier("john");
team[1] = new Medic("fred");


class Medic:Npc{
  Medic(string name){
    Health = 30;
    Name = name;
  }
}

class Soldier:Npc{
  Soldier(string name){
    Health = 80;
    Name = name;
  }
}


void DropBombOn(Npc[] team){
  foreach(var npc in team) {
    npc.Health -= 55;
    if(npc.Health <= 0)
      Console.WriteLine("Character called " + npc.Name + " is dead");
  }
}

你可以添加其他类型的角色,他们仍然是Npc,所以他们可以像其他人一样被轰炸..

继承允许我们将非常具体的类型视为更一般的东西,如果有可以在一般意义上发生的操作,在所有这些类型的东西具有的一般属性上。

也许所有玩家的 class 中都有代表他们的位图图像。你可以通过访问每个团队并说“给我你的像素”来在屏幕上绘制团队;你不需要访问每个人并说“如果这个 npc 是军医,就画一个军医。如果这个 npc 是士兵,就画一个士兵......” - 你只需绘制他们给你的任何像素,每个不同的玩家都会给出不同的像素像素。那,本质上就是我们对名字所做的——所有角色都有一个名字,我们不关心我们得到的是哪个名字;我们只是询问并打印出了我们得到的结果

另一个有用的东西是 classes 可以有覆盖(替换)基础 classes 的方法。这里我们创建一个描述角色的方法。我们把它放在 Npc 上,这意味着它可以用于从 Npc 继承的任何东西。我们标记为abstract,意思是“任何继承自Npc的东西必须提供一个returns字符串的方法,并称为DescribeYourself”:

public class Npc{
  public int Health{get;set;}
  public string Name{get;set;}
  public abstract void DescribeYourself();
}


public class Medic:Npc{
  public Medic(string name){
    Health = 30;
    Name = name;
  }

  public override string DescribeYourself(){
    return "I'm a medic called " + Name;
  }

}

public class Soldier:Npc{

  public string ServiceNumber {get; set;}

  public Soldier(string name,string serviceNumber){
    Health = 80;
    Name = name;
    ServiceNumber = serviceNumber;
  }

  public override string DescribeYourself(){
    return "I'm a soldier with service number " + ServiceNumber + " and I ain't telling you anything else";
  }
}


void DropBombOn(Npc[] team){
  foreach(var npc in team) {
    npc.Health -= 55;
    if(npc.Health <= 0)
      Console.WriteLine(npc.DescribeYourself() + " and I'm dead");
  }
}

这次我们没有拉出 name/print 一条常见的消息 - 我们要求 npc 描述自己。如果是士兵,代码的士兵版本会运行,我们会看到 我是一名士兵,服务编号为 12345,我不会告诉你任何其他事情,我已经死了安慰。如果是医生,他们会更愿意提供个人信息,说 我是一名医生,名叫约翰,我已经死了 :)

我们在制作士兵时还必须提供服务编号。这概述了不同种类的classes在设置和使用过程中可以有不同的数据requirements/more或更少的数据,但我们仍然可以以通用的方式对待它们:

team[0] = new Soldier("john", "12345");
team[1] = new Medic("fred");

这种“以普通方式对待”的术语是多态性,它可能是 C# 最强大的功能之一。在实际意义上,例如,Microsoft可以提供一个 Stream class,它基本上将字节写入某个地方,然后大量继承的 classes 可以将字节写入不同的地方。我们现在有可以将字节写入.. 控制台、文件、一些保管箱存储、内存、字符串的流......它们都以相同的方式使用,调用 write 的人不会关心代码必须跳过多少圈才能写入控制台、文件和 FTP 服务器。他们只是调用 Write 并期望字节最终到达承诺的位置。

您可以编写自己的流,并将它们交给 Microsoft 的 classes - Microsoft 的 classes 对您的 class 一无所知 es,但您仍然可以要求位图将其自身保存到您的流中,也许您的流 class 接受它并将字节添加到电子邮件中并发送它..

突然之间,您刚刚让位图将自己写入电子邮件成为可能,并且位图可以完全不知道电子邮件是什么,但它仍然可以写入。

你可以完全不知道什么是塞尺,但你可以用它来涂抹蛋黄酱..