【问题标题】:Why is it legal to store a derived type within a base class reference?为什么在基类引用中存储派生类型是合法的?
【发布时间】:2011-04-23 09:36:24
【问题描述】:

假设我们已经声明了这两个类:

public class Animal
{
    //.....
}

public class Dog : Animal
{
    //.....
}

好吧,我的问题是:为什么下面的代码行是有效的?

Animal animal = new Dog();

编辑:

在电子书《Professional C# 2008》中有一段说:

在基类引用中存储派生类型总是安全的。

【问题讨论】:

    标签: c# .net inheritance polymorphism


    【解决方案1】:

    它起作用的原因是狗is an动物(因为狗inherits来自Animal)所以将狗对象分配给Animal类型的变量是合法的。

    向另一个方向分配是不合法的。以下行将给出错误,因为 Animal 不继承自 dog:

    // Error: Cannot implicitly convert type 'Animal' to 'Dog'.
    // An explicit conversion exists (are you missing a cast?)
    Dog dog = new Animal();
    

    在某些情况下,您可能有一个 Animal 类型的变量,但您知道它实际上必须是 Dog。在这种情况下,您可以使用强制转换来进行分配,但如果对象实际上不是 Dog,则此强制转换可能会在运行时失败。

    Animal animal = new Cat();
    
    // Unable to cast object of type 'Cat' to type 'Dog'.
    Dog dog = (Dog)animal;
    

    【讨论】:

      【解决方案2】:

      因为Dog 继承自Animal - 所以Dog 可以被视为Animal(事实上,它Animal,它支持相同的' interface' - 即Animal 上的所有属性/方法都保证由Dog 实现。

      【讨论】:

        【解决方案3】:

        您正在创建一个新的Dog,但随后您将其视为(使用)Animal

        当您希望拥有由Animal 拥有或公开的某些行为时,这特别有用,但Dog 可能会用不同的东西覆盖它。 Dog 可以用作DogAnimal,因为两者兼有。

        编辑:这里是一个简单的例子,我将使用一个抽象基类,这样你就可以看到这种安排的真正价值:

        public abstract class Animal
        {
            public abstract void Move();
        
            public virtual void MakeSignatureSound() 
            {
                Console.WriteLine("Ugggg");
            }
        }
        
        public class Dog : Animal 
        {
            public override void Move() 
            {
                RunLikeAPuppy();
            }
        
            public override void MakeSignatureSound()
            {
                Console.WriteLine("Woof");
            }
        }
        
        public class CaveMan : Animal
        {
            public override void Move() 
            {
                RunLikeANeanderthal();
            }
        }
        
        public class Cat : Animal
        {
            public override void Move() 
            {
                RunLikeAKitteh();
            }
        
            public override void MakeSignatureSound()
            {
                Console.WriteLine("Meioww");
            }
        }
        

        注意两点:

        • Animal 类的所有三个派生类必须覆盖 Move() 函数,因为在我的基类中我决定所有动物都应该有一个 Move(),但我没有'不指定如何它们应该移动 - 这取决于个体动物来指定
        • CaveMan 类没有覆盖 MakeSignatureSound() 函数,因为它已经在基类中定义并且对他来说已经足够了

        现在如果我这样做:

        Animal caveman = new CaveMan();
        Animal dog = new Dog();
        caveman.MakeSignatureSound();
        dog.MakeSignatureSound();
        

        我会在控制台上得到这个:

        Ugggg
        Woof
        

        但是因为我使用了抽象基类,我不能直接实例化它的实例,我不能这样做:

        Animal animal = new Animal();
        

        作为开发人员,我想确保当其他人(或我自己)创建新的Animal 时,它必须是特定类型,而不仅仅是没有行为或特征的纯 Animal

        【讨论】:

        • 不错的评论。我的意思是在子类中覆盖方法。你能在这里写一个简单的例子吗?
        【解决方案4】:

        是因为你在这里声明的关系

        public class Dog : Animal
        

        这句话的基本意思是每只狗都是动物

        现在,如果每只狗都是动物,那么当你这样做时

         Animal animal = new Dog()
        

        这意味着我带了一只狗,但把它称为动物。

        一个类似的例子是将你称为哺乳动物 - 你是人类,但也是哺乳动物,所以如果我称你为哺乳动物,那仍然是正确的。

        它不一定能详细说明您的资格,但本身仍然正确。

        【讨论】:

        • 正如你所说:“.....把它称为动物”是不是意味着我们正在做一个隐含的向上转换?
        • 引用基类并不意味着向上转型?
        • @odiseh - 我的错!你说的对!!技术术语是“upcasting”..我错了!!
        【解决方案5】:

        在 C#(和其他一些语言)中,每个对象都有两种类型:静态类型动态类型。对象的静态类型(在您的示例中:Animal)在编译时确定,动态类型(在您的示例中:Dog)在运行时确定。考虑以下几点:

        Animal animal;
        if (userInput) 
            animal = new Wolf();
        else 
            animal = new Dog();
        

        编译器无法确定动物将具有哪种动态类型。仅在运行时确定
        动态类型应始终至少是对象的静态类型。这些是不允许的,会导致编译错误:

        Dog d = new Animal(); // X
        Animal a = new Car(); // X (assuming car does not inherit from animal)
        

        为什么有用?与动态类型语言(只有动态类型而没有静态类型)不同,编译器可以检查类型错误。这很好,因为我们希望尽早发现错误。
        在我的示例中,编译器不知道animalWolf 还是Dog,但两者都派生自Animal,因此可以确定任何Aminal 操作都可以在animal 上执行。尝试执行其他操作会导致编译错误。
        另一方面,我们可以实现没有“双类型”系统无法实现的强大功能。让我们假设Aminal 有一个操作eatDogWolf 都实现了吃,每个都有自己的方式。我们看下面的例子:

        Animal a = new Animal();
        Animal d = new Dog();
        
        a.eat(); // Animal's eat
        d.eat(); // Dog's eat
        

        这里我们可以看到虽然d的静态类型是Animal,但是调用的函数的实际版本是由动态类型决定的。这允许我们做一个叫做多态性的事情。我给你举个例子:

        Animal zoo[100]; // each animal in the zoo array is a static type Animal
        zoo[0] = new Dog(); // first element of the array is of dynamic type Dog
        zoo[1] = new Cat();
        zoo[2] = new Rabbit();
        ...
        // Now the array holds different types of animals. We want to feed them all, but each one in it's own way.
        foreach(Animal a in zoo)
            a.eat();
        

        总结一下:

        • 编译时已知的静态类型。用于确定对对象执行的操作是否合法。
        • 仅在运行时知道的动态类型。用于选择要执行的操作。这还有另一个术语:动态调度

        【讨论】:

          【解决方案6】:

          只是添加一个额外的例子说明什么时候有用......

          你可以有一个强类型的动物列表...

          List<Animal> animals = new List<Animal>();
          

          然后您可以将动物实例添加到该集合中。

          animals.Add(new Dog());
          animals.Add(new Cat());
          

          如果你有像下面这样的动物。

          class Animal
          {
             public abstract string Run();
          }
          
          class Dog : Animal
          {
             public override string Run()
             {
                return "Running into a wall.";
             }
          
          }
          
          class Cat : Animal
          {
             public override string Run()
             {
               return "Running up a tree";
             }
          
          }
          

          然后您就可以安全地遍历该集合了。

          foreach(var animal in animals)
             Console.WriteLine(animal.Run());
          

          这引入了其他概念,例如抽象方法和重写,您也应该研究一下......

          希望对你有用。

          祝你好运!

          【讨论】:

            【解决方案7】:

            假设这条线不合法Animal animal = new Dog();

            public class Animal
            {
                //.....
            }
            
            
            public class Dog : Animal
            {
                //.....
            }
            

            现在在我的主程序中,我有一个名为 Feed 的函数。

            void Feed(Dog d)
            {
            
              //....
            }
            

            然后我添加了另一个类 Cat

            public class Cat : Animal
            {
                //.....
            }
            

            然后在我的主程序中,我将不得不为猫类专门为 Feed 编写另一个函数

            void Feed(Dog d)
            {
            
              //....
            }
            
            void Feed(Cat d)
            {
            
              //....
            }
            

            为了避免编码重复的函数(在派生类的情况下),将派生类型存储在基类引用中是合法且安全的。

            现在我可以编写一个 Feed 函数来代替上面的两个函数,并接受一个基类的参数。

            Animal animal = new Dog();
            
            Animal animal2 = new Cat();
            
            Feed(animal);
            Feed(animal2);
            
            void Feed(Animal A)
            {
            
              //....
            }
            

            【讨论】:

              猜你喜欢
              • 2016-05-21
              • 2018-04-02
              • 1970-01-01
              • 2022-01-21
              • 2023-03-28
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多