为何以及何时使用多态?

时间:2012-10-06 02:01:48

标签: c# oop polymorphism

我是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()?当你知道这个物体是一种动物,但我不知道它是什么样的动物时,我认为这很方便。请向我解释一下。

由于

7 个答案:

答案 0 :(得分:10)

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

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());
}

答案 1 :(得分: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();
}

实际上,它以两种方式使用多态。首先,它返回的Animal枚举可以是任何类型。在这种情况下,任何调用代码都不关心哪一个,它将以更一般的方式使用它们(例如在前面的例子中)。此外,可以返回任何实现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,它指出类型的任何子类型都可以用作父类型,而无需更改程序。也就是说,如果Dog是[{1}}的子类型,那么您应始终能够使用Animal作为Dog,而无需知道它是Animal或对其做出任何特殊考虑。

您也可以从查看Dependency Inversion Principle中受益,上面的存储库实现可以作为示例。正在运行的应用程序不关心哪种类型(Dog)实现接口。它关心的唯一类型是界面本身。实现类型可以具有附加的公共或至少内部方法,实现组件使用这些方法来实现如何实现特定依赖性,但这些方法对于消费代码没有任何影响。每个实现都可以随意交换,调用代码只需要提供更通用接口的任何实例。


旁注:我个人发现继承经常被过度使用,特别是在AnimalRepositoryImplementation示例中的普通继承,其中Animal本身不应该是可实例化的类。如果应用程序的逻辑需要通用形式,它可以是接口或抽象类。但不要仅仅是为了做到这一点。

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

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

答案 2 :(得分:2)

如果你看到的只是层次结构,那么多态性的'动物'例子是相当有缺陷的,因为它似乎暗示每种“种类”关系都应该以多态方式建模。这反过来会导致许多真正复杂的代码;即使没有令人信服的理由让他们在那里,基类仍然比比皆是。

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

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所涉及的一些共享功能。我们所做的只是孤立虚拟方法背后的Pet特定行为。一只狗可能会在消防栓上小便,而鬣蜥可能会在路人身上发出嘶嘶声,但走路的行为却与走路时的行为脱钩。

答案 3 :(得分:1)

因为您应该在基类中保留常见行为,并覆盖特定子类的不同行为。因此,您减少了代码重复。

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

答案 4 :(得分:1)

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

class GroomService
{
    public void Groom(Animal animal)
    {
        animal.Groom();
    }
}

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

最终结果是您最终得到的代码更少,维护更容易。

答案 5 :(得分:0)

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

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"

答案 6 :(得分:0)

我将提供一个真实而实际的案例,其中我实际使用了多态。这是MikeB describes

的特例

我想告诉这个故事,因为搜索真实生活中的多态性例子会产生很多与现实生活同构的东西(比如狗 - >动物或汽车 - &gt;车辆)并且不够真实,因为“我实际上做了”写下这段代码“。

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


故事

大约六年前,我创建了一个在地图上查找路线的应用程序。这是一个桌面应用程序,它必须保持所有道路的连接,并能够通过街道和数字找到方向,并找到从一个位置到另一个位置的路线。我想要注意的是,我从未添加过真实的地图,我使用的都是低调的,专为演示目的而设计。

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

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

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

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

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


最后一点:根据我的经验,创建复杂的继承树根本无助于维护。特别是如果派生类没有添加任何关于基类的内容,或者派生类不重用基类的代码。对于常见的例子来说,这是一个很大的问题,因为在你的Animal&amp;狗的例子,你通过让Dog继承Animal来获得任何东西。