【问题标题】:Creating variable of type <base class> to store <derived class> object in C# [closed]在 C# 中创建 <base class> 类型的变量以存储 <derived class> 对象 [关闭]
【发布时间】:2014-04-19 16:14:36
【问题描述】:

我对编程有点陌生,我有一个关于 C# 中的类、继承和多态性的问题。在学习这些主题时,我偶尔会遇到如下代码:

Animal fluffy = new Cat();  // where Animal is a superclass of Cat*

这让我很困惑,因为我不明白为什么有人会创建一个 Animal 类型的变量来存储 Cat 类型的对象。为什么一个人不简单地写这个:

Cat fluffy = new Cat();

我明白为什么将子对象存储在父类型变量中是合法的,但不明白为什么它有用。是否有充分的理由将Cat 对象存储在Animal 变量与Cat 变量中?有人能给我举个例子吗?我确信它与多态性和方法覆盖(和/或方法隐藏)有关,但我似乎无法理解它。提前致谢!

【问题讨论】:

  • Animal fluffy = new Cat(); 将创建一个具有 static 类型 Animaldynamic 类型 Cat 的对象。您应该自己试验采用Animal 的方法和采用Cat 并通过它们传递的方法。例如,给两个类一个方法.makeNoise(),然后创建一个方法MakeNoise(Cat c)MakeNoise(Animal a)。差异是微妙的,但了解它们会让你成为更好的程序员。
  • @ChristopheDeTroyer - 不。该语句创建了 1 个 Cat 类型的实例和 1 个 Animal 类型的引用。两者都是静态类型的,这里没有动态。对于这样一个基本的陈述,行话很难说对。
  • 是的,我的意思是运行时类型。我很抱歉。 :)
  • 如果您来自像 C++ 这样的语言,这可能会令人困惑:在 Java 或 C# 等 OO 语言中,您的变量本身并不包含值,而是您的变量是指向对象的指针它持有(代表)价值。所以你的变量是指针,允许我们以多态方式指向派生类的实例。即使很多人认为 Java 会说“没有指针,万岁!” 在这门语言中你首先要明白的是,一切都是指针

标签: c# class object inheritance polymorphism


【解决方案1】:

我能给你的最短的例子是如果你想要一份所有动物的清单

 List<Animal> Animals = new List<Animal>();
 Animals.Add(new Cat());
 Animals.Add(new Dog());

如果您曾经使用 Winforms 创建过项目,那么您将已经使用过类似的东西,因为所有控件都派生自 Control。然后您会注意到一个窗口有一个控件列表 (this.Controls),它允许您一次访问窗口上的所有子控件。 IE 隐藏所有控件。

 foreach(var control in this.Controls)
      control.Hide();

【讨论】:

  • +1 给新手的漂亮例子
  • 这解释了协方差,但这并不是 OP 真正要求的。
  • Op 询问“是否有充分的理由将 Cat 对象存储在 Animal 变量和 aCat 变量中?有人可以举个例子吗?”
  • +1 用于显示现有 .NET API 中的示例。
  • @Christophe De Troyer:这里不涉及协方差,这正是 OP 所要求的。
【解决方案2】:

但不是为什么它有用。

看看一些更好的例子:

Cat myCat = new Cat();
Dog myDog = new Dog();

List<Animal> zoo = ...;  // A list of Animal references
zoo.Add(myCat);          // implicit conversion of Cat reference to Animal reference
zoo.Add(myDog);

void CareFor(Animal animal) { ... }
CareFor(myCat);         // implicit conversion of Cat reference to Animal reference
CareFor(myDog);

Animal fluffy = new Cat(); 模式在实际代码中并不常见(但确实会出现)。

考虑到非常简化的代码如何显示某些功能的工作原理并不总是善于展示该功能的原因

【讨论】:

    【解决方案3】:

    让我们看一个实际但极端的例子。

    class Animal { }
    class Bird : Animal { }
    class Cat : Animal { }
    class Dog : Animal { }
    class Elephant : Animal { }
    class Fennec : Animal { }
    

    假设我们有一个 Person 类。我们如何存储对他唯一的宠物的引用?


    方法一:疯狂的方式

    class Person
    {
        public Bird myBird;
        public Cat myCat;
        public Dog myDog;
        public Elephant myElephant;
        public Fennec myFennec;
    }
    

    在那一团糟中,我们如何找回宠物?

       if (myBird != null)
        {
            return myBird;
        }
        else if (myCat != null)
        {
            return myCat;
        }
        else if (myDog != null)
        {
            return myDog;
        }
        else if (myElephant != null)
        {
            return myElephant;
        }
        else if (myFennec != null)
        {
            return myFennec;
        }
        else
        {
            return null;
        }
    

    我在这里很好,只有 5 种动物。假设我们有超过 1000 种动物。您是否会在 Person 类中编写所有这些变量,并在您的应用程序中的每个位置添加所有这些“else if ()”?


    方法 2:更好的方法

    class Person
    {
        public Animal myPet;
    }
    

    这样,由于多态性,我们对人的宠物有唯一且唯一的引用,为了得到宠物,我们只需写:

    return myPet;
    

    那么,最好的做事方式是什么?方法一还是二?

    【讨论】:

    • 您可以毫不费力地让这些示例运行。
    • @IlmoEuro 如果您谈论的是由 if-else-return 组成的代码块,它们最初都在“Animal GetPet()”函数中,但我觉得它不会增加太多显示方法定义。此外,最初的问题是关于将 Y 类型对象的引用存储在 X 类型的变量或字段中的有用性,其中 Y 派生自 X。所以,我放弃了方法定义,只关注一个示例与变量或字段相关。
    【解决方案4】:

    包含初始化的声明,例如Animal joesPet = new Cat(),可以有两个目的:

    • 创建一个标识符,该标识符在其范围内始终代表相同的事物。

    • 创建一个变量,它最初会保存一个东西,但以后可能会保存其他东西。

    父类型变量被初始化为引用子类型实例的声明通常用于第二个目的,在变量最初分配给特定子类型的实例但以后可能需要保存的情况下对不属于该子类型的事物的引用。如果声明是Cat joesPet = new Cat();var joesPet = new Cat();,那么(无论好坏)都不能说joesPet = new Dog();。如果代码不应该能够说出joesPet = new Dog();,那么声明为Catvar 的事实会阻止这将是一件好事。另一方面,如果代码可能需要让joesPet 不是Cat,那么它应该以允许的方式声明变量。

    【讨论】:

      【解决方案5】:

      由于还没有回答,我会尽量给出一个好的答案。

      看看下面的程序:

      class Program
      {
          static void Main(string[] args)
          {
              Animal a = new Animal();
              Cat c = new Cat();
              Animal ac = new Cat();
      
              a.Noise(a);
              a.Noise(c);
              a.Noise(ac);
      
              c.Noise(a);
              c.Noise(c);
              c.Noise(ac);
      
              a.Poop();
              c.Poop();
              ac.Poop();
      
              Console.Read();
      
          }
      }
      
      public class Animal
      {
          public void Noise(Animal a)
          {
              Console.WriteLine("Animal making noise!");
          }
      
          public void Poop()
          {
              Console.WriteLine("Animal pooping!");
          }
      }
      
      public class Cat : Animal
      {
          public void Noise(Cat c)
          {
              Console.WriteLine("Cat making noise!");
          }
      
          public void Noise(Animal c)
          {
              Console.WriteLine("Animal making noise!");
          }
      
          public void Poop()
          {
              Console.WriteLine("Cat pooping in your shoe!");
          }
      }
      

      输出:

      Animal making noise!
      Animal making noise!
      Animal making noise!
      
      Animal making noise!
      Cat making noise!
      Animal making noise!
      
      Animal pooping!
      Cat pooping in your shoe!
      Animal pooping!
      

      您可以看到我们创建了一个Animal 类型的变量a。它指向Animal 类型的对象。它有静态和运行时类型Animal

      接下来我们创建指向Cat 对象的Cat 变量。第三个对象是棘手的部分。我们创建了一个Animal 变量,它的运行时类型为Cat,但静态类型为Animal。为什么这很重要?因为在编译时您的编译器知道变量ac 实际上是Animal 类型。毫无疑问。所以它将能够完成Animal 对象可以做的所有事情。

      但是,在运行时,变量内的对象已知为Cat

      为了演示我创建了 9 个函数调用。

      首先,我们将对象传递给Animal 的实例。这个对象有一个接受Animal 对象的方法。

      这意味着在Noise() 内部,我们可以使用Animal 类拥有的所有方法和字段。没有其他的。所以如果Cat 有一个方法Miauw(),我们就不能在不给我们的动物一个Cat 的情况下调用它。 (类型转换很脏,尽量避免它)。因此,当我们执行这 3 个函数调用时,我们将打印 3 次 Animal making noise!。清楚地。那么我的静态类型有什么关系呢?

      好吧,我们马上就到那里。

      接下来的三个函数调用是Cat 对象内的方法。 Cat 对象有两个方法Noise()。一个接受Animal,另一个接受Cat

      所以首先我们传递一个普通的Animal。运行时将查看所有方法并看到它有一个方法Noise,它采用Animal。正是我们需要的!所以我们执行那个并打印Animal 制造噪音。

      下一个调用传递一个包含Cat 对象的Cat 变量。再一次,运行时会看看。我们是否有一个采用Cat 的方法,因为那是我的变量的类型。是的,是的,我们这样做。所以我们执行方法并打印"Cat making noise".

      第三次调用,我们有我们的变量ac,它的类型是Animal,但指向一个Cat类型的对象。我们将看看是否能找到适合我们需求的方法。我们看一下静态类型(即变量的类型),我们看到它的类型是Animal,所以我们调用以Animal为参数的方法。

      这是两者之间的细微差别。

      接下来是大便。

      所有动物都会拉屎。然而,一个Cat 会在你的鞋子里拉屎。所以我们重写基类的方法并实现它,这样Cat就在你的鞋里。

      您会注意到,当我们在 Animal 上调用 Poop() 时,我们得到了预期的结果。 Cat c 也是如此。然而,当我们在ac 上调用Poop 方法时,我们看到这是一个Animal 便便并且你的鞋子是干净的。这是因为编译器又说我们的变量ac 的类型是Animal,你是这么说的。因此,它会调用Animal类型的方法。

      我希望这对你来说已经足够清楚了。

      编辑:

      我这样想就记住了这一点:Cat x; 是一个类型为Cat 的盒子。盒子里没有猫,但是它的类型是Cat。这意味着盒子有一个类型,不管它的内容是什么。现在,当我在其中存储一只猫时:x = new Cat();,我在其中放入了一个类型为 Cat 的对象。所以我把一只猫放在猫盒里。但是,当我创建一个盒子Animal x; 时,我可以在这个盒子里存放动物。所以当我在这个盒子里放一个Cat 时,没关系,因为它是一种动物。所以x = new Cat() 把一只猫放在一个动物盒子里,这没关系。

      【讨论】:

      • 有几处错误。您使用的是非标准且令人困惑的行话。对象没有“运行时”或“静态”类型,它们只有一种类型。多态性来自于使用不同(但相关)类型的引用指向它们。
      • 不,这是不正确的 :) 对象确实具有运行时类型。静态类型是指变量的类型。运行时类型是指变量指向的对象的类型。
      • 你可以这么说,但这不是标准命名法。你的“盒子”例子是误导性的。多态性存在于引用中,而不是对象中。
      • 你是 100% 正确的!
      【解决方案6】:

      原因是多态性。

      Animal A = new Cat();
      Animal B = new Dog();
      

      如果 Func 采用 AnimalAnimal 实现 MakeNoise()

      Func(A);
      Func(B);
      
      
      ...
      
      void Func(Animal a)
      {
          a.MakeNoise();
      }
      

      【讨论】:

        【解决方案7】:

        简单的答案:如果您使用接口或基类动物,您可以编写可以采用所有类型动物而不是仅一种动物的通用方法。

        Why use an interface when the class can directly implement the functions

        【讨论】:

          【解决方案8】:

          我也使用过几次这种模式,在更高级的环境中,但也许值得一提。在编写执行服务/存储库或任何实现接口的类的单元测试时,我经常使用接口而不是具体类型来键入其变量:

          IRepository repository = new Repository();
          repository.Something();
          Assert.AreEquals(......);
          

          我认为这种特殊情况是将变量作为接口类型的更好选择,因为它有助于额外检查接口实际上是否正确实现。由于很可能在实际代码中我不会直接使用具体类,我发现最好有这个额外的验证。

          【讨论】:

            【解决方案9】:

            如果您正在编写一个模拟动物行为的程序,那么所有动物都有共同点。他们走路、吃饭、呼吸、排泄等等。他们吃什么以及走路的方式等等都是不同的。

            所以你的程序知道所有动物都会做一些事情,所以你编写了一个名为Animal 的基类来完成所有这些事情。所有动物都会做的事情(呼吸、消除)你可以在基类中编程。然后,在子类中,您编写代码来处理它们所做但与其他动物不同的事情,例如它们吃什么和走路方式。

            但控制每只动物行为方式的逻辑并不关心它们如何做任何事情的细节。动物的“大脑”只知道该吃饭、走路、呼吸或排泄。因此,它调用对 Animal 类型的变量执行这些操作的方法,最终根据它所指对象的实际 Animal 类型调用正确的方法。

            【讨论】:

              猜你喜欢
              • 2021-11-28
              • 1970-01-01
              • 1970-01-01
              • 2019-06-24
              • 2020-02-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多