7

我是 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()?我认为当您知道该物体是动物但不知道它是哪种动物时,这很方便。请给我解释一下。

谢谢

4

7 回答 7

9

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

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());
}
于 2012-10-06T02:05:12.340 回答
8

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

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类型,而无需更改接受它们的方法。

(与此相关的是策略模式。)

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

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

实际上,这以两种方式使用多态性。首先,它返回的 s 的枚举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的,因为这就是所有调用代码将要使用的。

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

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

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


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

一般来说,正如《四人组》一书所推荐的,更喜欢组合而不是继承。(如果您没有副本,请获取一份。)不要过度使用继承,但要在适当的地方使用它。如果将通用功能分组为组件并且每个组件都由这些组件构建,那么应用程序可能会更有意义吗?当然,更常用的例子可以从中吸取教训。AnimalAnimalCar

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

于 2012-10-06T02:26:36.847 回答
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 特定行为。狗可能会在消火栓上撒尿,而鬣蜥可能会对路人发出嘶嘶声,但走路的行为已经与他们走路时的行为脱钩了。

于 2012-10-06T02:21:07.253 回答
1

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

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

于 2012-10-06T02:06:51.367 回答
1

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

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

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

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

于 2012-10-06T02:20:52.737 回答
0

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

我想讲述这个故事,因为搜索多态的真实生活示例会呈现许多与真实生活的同构(如狗 -> 动物或汽车 -> 车辆),并且在“我确实编写了这段代码”中不够真实。

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


故事

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

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

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

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

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

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


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

于 2012-10-06T02:29:31.310 回答
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"
于 2012-10-06T02:07:06.603 回答