【问题标题】:Why and when use polymorphism?为什么以及何时使用多态性?
【发布时间】:2012-09-27 04:30:53
【问题描述】:

我是 OOP 新手,多态性让我很难过:

class Animal
{
    public virtual void eat()
    {
        Console.Write("Animal eating");
    }
}
class Dog : Animal
{
    public override void eat()
    {
        Console.Write("Dog eating");
    }
}
class Program
{
    public void Main()
    {
        Animal dog = new Dog();
        Animal generic = new Animal();
        dog.eat();
        generic.eat();
    }
}

这样打印出来

Dog eating
Animal eating

但为什么不只使用 Dog 类型而不是动物类型,例如 Dog dog = new Dog()?当您知道该物体是动物但不知道它是哪种动物时,我认为这很方便。请给我解释一下。

谢谢

【问题讨论】:

  • 我现在能说的最好的就是你会知道什么时候需要它。我希望他们使用更好的例子。
  • 我想你已经了解了如何使用以及多态的作用。现在我想提一下,如果只是为了告诉对象代表哪种动物,您只需拥有一个属性Kind 并使用它。事实上,你不应该为你拥有的每一种东西都创建一个类...例如Car类,以及RedCarPurpleCarVioletCarBlueCar等派生类对于每种颜色,几乎没有什么用处。只需添加一个 Color 属性并使用它。

标签: c# oop polymorphism


【解决方案1】:

您可以通过超类引用子类。

Animal dog = new Dog();
Animal cat = new Cat();
Animal frog = new Frog();

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

animals.add(dog);
animals.add(cat);
animals.add(frog);

foreach(Animal animal in animals)
{   
    Console.WriteLine(animal.eat());
}

【讨论】:

  • 哦,我明白了,所以我们使用 virtual 和 override 来避免调用超类吃或子类吃之间的歧义,对吧?
  • @lombardo2 我们使用 virtual 和 override 使其明确。其他语言使其隐含并假设您始终调用派生类的实现。现在,通过使虚拟方法显式(并选择加入),我们避免了允许其他代码以意想不到的方式改变行为的风险。需要覆盖的事实(并且当基类中有匹配的虚拟方法时不使用它是一个警告)可以避免意外覆盖该方法。
【解决方案2】:

因为您应该在基类中保留通用行为,并覆盖特定子类的不同行为。从而减少代码重复。

在您的示例中,您仍然可以像 Dog dog = new Dog() 一样实例化您的对象。这与多态性关系不大。虽然当你试图传递给任何期望任何动物的方法时,最好使用基类,无论是人还是狗等。

【讨论】:

    【解决方案3】:

    当您有多个类继承同一个父类或实现同一个接口时,这会派上用场。例如:

    class Cat : Animal {
        public override void eat()
        {
            Console.Write("Cat eating");
        }
    }
    
    class Program {
    
        public void Main() {
            Animal cat = new Cat();
            Animal dog = new Dog();
    
            cat.eat();
            dog.eat();
        }
    }
    

    这将输出:

    "Cat eating"
    "Dog eating"
    

    【讨论】:

      【解决方案4】:

      它还允许您将超类传递给方法而不是子类,例如:

      class GroomService
      {
          public void Groom(Animal animal)
          {
              animal.Groom();
          }
      }
      
      public void Main()
          {
              GroomService groomService = new GroomService();
              groomService.Groom(dog);
              groomService.Groom(generic);
          }
      

      最终的结果是代码更少,可维护性更容易。

      【讨论】:

        【解决方案5】:

        如果您所看到的只是层次结构,那么多态的“动物”示例是相当有缺陷的,因为它似乎暗示每个“种类”关系都应该以多态方式建模。这反过来又导致了很多非常复杂的代码。即使没有令人信服的理由让它们存在,基类也比比皆是。

        当您引入使用层次结构的对象时,该示例更有意义。例如:

        public abstract class Pet
        {
            public abstract void Walk();
        }
        
        public sealed class Dog : Pet
        {
            public override void Walk()
            {
                //Do dog things on the walk
            }
        }
        
        public sealed class Iguana : Pet
        {
            public override void Walk()
            {
                //Do iguana things on the walk
            }
        }
        
        public sealed class PetWalker
        {
            public void Walk(Pet pet)
            {
                //Do things you'd use to get ready for walking any pet...
                pet.Walk(); //Walk the pet
                //Recover from the ordeal...
            }
        }
        

        请注意,您的 PetWalker 封装了一些与行走任何类型的宠物有关的共享功能。我们所做的只是隔离了虚拟方法背后的 Pet 特定行为。狗可能会在消火栓上撒尿,而鬣蜥可能会对路人发出嘶嘶声,但遛狗的行为已经与它们走路时的行为脱钩了。

        【讨论】:

        • 现在理解多了,但是抽象方法和虚方法有什么区别呢?
        • @lombardo2 抽象方法在基类中没有实现,任何派生类(非抽象)必须实现它们。另一方面,派生类中的虚方法不需要替换,它们可以保留基类提供的实现。
        • 抽象方法是在基类中没有实现的虚方法。 Abstract 用于当您想说一个行为由所有后代共享,但您不想为其工作方式提供任何实现时。细节完全留给后代。当后代可能想要修改“默认”行为但在许多后代中可能保持不变时,非抽象虚拟方法很有用。
        【解决方案6】:

        当你想要一个不关心具体实现而只关心总体类型的通用方法时,多态真的派上用场了。以你的动物为例:

        public static void Main()
        {
            var animals = new List<Animal>();
            animals.Add(new Dog());
            animals.Add(new Cat());
        
            foreach (var animal in animals)
                Feed(animal);
        }
        
        public static void Feed(Animal animal)
        {
            animal.Eat();
        }
        

        请注意,该方法并不关心它得到什么样的动物,它只是试图喂它。也许Dog 实现了Eat() 以至于它吞噬了所有可见的东西。也许Cat() 实现了它,让它咬一口然后走开。也许Fish() 实现它以至于它吃得太多而死。该方法本身并不关心它得到哪个Animal,您可以轻松添加更多Animal 类型,而无需更改接受它们的方法。

        (与此相关的是Strategy Pattern。)

        相反,有时您希望方法返回通用类型,而不管实现了什么。我使用的一个常见示例是:

        public interface AnimalRepository
        {
            IEnumerable<Animal> GetAnimals();
        }
        

        这实际上以两种方式使用多态性。首先,它返回的Animals 的枚举可以是任何类型。在这种情况下,任何调用代码都不会关心哪个是哪个,它将以更一般的方式使用它们(例如在前面的示例中)。此外,任何实现IEnumerable 的东西都可以返回。

        例如,我有一个使用 LINQ to SQL 的接口实现:

        public class AnimalRepositoryImplementation : AnimalRepository
        {
            public IEnumerable<Animal> GetAnimals()
            {
                return new DBContext().Animals;
            }
        }
        

        这将返回一个IQueryable。然而,无论调用该方法,都不关心它是一个IQueryable。它只会使用IEnumerable 上的功能。

        或者,我有另一个模拟测试的实现:

        public class AnimalRepositoryImplementation : AnimalRepository
        {
            private IList<Animal> animals = new List<Animal>();
        
            public IEnumerable<Animal> GetAnimals()
            {
                return animals;
            }
        }
        

        这将返回一个IList,它再次被变形为更通用的IEnumerable,因为这就是调用代码将要使用的全部内容。

        这些也称为covariance and contravariance。在返回上面的IEnumerable 的情况下,类型从更具体的(IQueryableIList)移动到更通用的(IEnumerable)。他们无需转换就能做到这一点,因为更具体的类型也是类型层次结构中更通用类型的实例。

        与此相关的还有Liskov Substitution Principle,它指出类型的任何子类型都可以用作该父类型,而无需更改程序。也就是说,如果DogAnimal 的子类型,那么您应该始终能够将Dog 用作Animal,而不必知道它是Dog 或做任何特殊的考虑。

        您还可以从查看Dependency Inversion Principle 中受益,上面的存储库实现可以作为示例。正在运行的应用程序不关心哪种类型 (AnimalRepositoryImplementation) 实现了该接口。它关心的唯一类型是接口本身。实现类型可能具有额外的公共或至少内部方法,实现程序集使用这些方法来确定特定依赖项是如何实现的,但这对使用代码没有影响。每个实现都可以随意换出,调用代码只需提供更通用接口的任何实例。


        旁注: 我个人发现继承经常被过度使用,尤其是像Animal 示例中的普通继承,其中Animal 本身不应该是可实例化的类。如果应用程序的逻辑需要通用形式,它可能是接口或抽象类。但不要只是为了做而做。

        一般而言,更喜欢Gang Of Four book 推荐的composition over inheritance。 (如果您没有副本,请获取一份。)不要过度使用继承,但要在适当的地方使用它。如果将常见的Animal 功能分组到组件中并且每个Animal 都是由这些组件构建的,那么应用程序可能会更有意义?当然,更常用的Car 示例可以从中吸取教训。

        保持类型的逻辑定义。你应该可以写new Animal()吗?拥有一个不再具体的Animal 的通用实例是否有意义?当然不是。但是拥有应该能够在任何Animal 上运行的通用功能(馈送、复制、死亡等)可能是有意义的。

        【讨论】:

        • +1 用于:1) 示例的改进,2) Liskov 替换原则和 3) 组合优于继承。
        • 很好解释,谢谢。现在我要阅读你留给我的所有文章。
        【解决方案7】:

        我将提供一个实际使用多态性的真实案例。这是MikeB describes 的一个特例。

        我想讲述这个故事,因为搜索 Real Life 的多态示例会呈现很多与现实生活的同构(例如 Dog -> Animal 或 Car -> Vehicle),并且没有足够的 REAL,如“我确实写过这个代码”。

        现在,在故事开始之前,我想提一下,在大多数情况下,我使用多态性来允许第三方开发人员扩展代码。假设我创建了一个接口供其他人实现。


        故事

        大约六年前,我创建了一个在地图上查找路线的应用程序。这是一个桌面应用程序,它必须保持所有道路的连接,并且能够通过街道和号码定位方向,并找到从一个位置到另一个位置的路线。我想指出的是,我从未添加过真实的地图,我使用的只是假设性的,并且是为演示目的而设计的。

        所以我有一个图表,其中节点所在的地图位置。每个节点都有坐标以将其定位在地图上,但有些节点特别适合穿越街道、建筑物和其他一些节点。

        为此,我为图形的节点使用了一个接口,该接口允许存储和检索节点的坐标和连接。我有这个接口的各种实现。

        一种建筑物和特殊地点的实现,应该可以通过它们的名称找到它们。有些用于指定道路交叉口(因此在搜索特定方向时标记起始位置以计算门牌号码*)。还有一些只是为了展示,允许描述道路的形状(因为道路并不总是直线)。

        *:是的,我确实编写了代码来计算它们,而不是存储每个房子的数量。我天真地认为存储每个房子的号码是天真的。 (或者可能是今天我天真地认为......没关系)。

        至于绘图,重要的是节点在哪里以及是否必须突出显示它(啊,是的,当用户将指针悬停在它们上方时,我确实突出显示了它们),但用于搜索其他类型的数据在相关的地方。


        最后一点:根据我的经验,创建复杂的继承树根本无助于维护。特别是如果派生类没有添加任何对基类的尊重,或者派生类不重用基类的代码。这是常见示例的一个大问题,因为在您的 Animal 和 Dog 示例中很明显,通过使 Dog 继承自 Animal,您将一无所获。

        【讨论】:

        • 有趣,我是从 OOP 开始的,所以需要一些练习和阅读才能达到我需要多态性的地步
        猜你喜欢
        • 2015-01-02
        • 2013-06-13
        • 2021-01-24
        • 2013-01-05
        • 2017-03-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-12
        相关资源
        最近更新 更多