为什么要使用派生类的引用来创建基类对象?

13

我正在练习C#中的继承,在一个测试程序中,我发现以下语句不会引发错误:

BaseClass baseObj = new DerivedClass();

这个语句为什么被允许,并且有哪些情况下它会对程序员有用呢?

以下是我的测试程序:

class BaseClass
{
    public void show()
    {
        Console.WriteLine("Base Class!");

    }
}

class DerivedClass : BaseClass
{
    public void Display()
    {
        Console.WriteLine("Derived Class!");            
    }
}

class Result
{
    public static void Main()
    {            
        BaseClass baseObj = new DerivedClass();
        baseObj.show();
    }
}

3
为什么这个语句是允许的 - 因为派生类的实例可以被视为基类的实例。这是C#中多态性的整个基础。 - Jon Skeet
那么,它们只有在与虚方法(在基类中)和覆盖(在派生类中)配对使用时才有用? - user3083590
3
不一定。它们在你想表明所有代码都能够仅使用基类成员运行时很有用,因此关于使用哪个派生类的选择局限于实例化它的单个位置。 - Jon Skeet
参见:https://dev59.com/3XRC5IYBdhLWcg3wMd5S - Jon Skeet
3个回答

20

我建议您更详细地了解继承和多态性(这里这里)。

在本答案中,我试图将概念保持足够简单。

为什么允许这个语句?程序员有什么场景可以使用这个语句吗?

但是,为了解释您的问题,让我们看一个需要使用多态性的面向对象编程的简单而经典的例子。

假设您正在编写一个需要存储某些形状并在屏幕上显示它们的程序。为此,您需要将所有形状存储在数组中,对吧?

假设我们的类大致如下:

class BaseShape
{
    public virtual void Display()
    {
        Console.WriteLine("Displaying Base Class!");

    }
}

class Circle : BaseShape
{
    public override void Display()
    {
        Console.WriteLine("Displaying Circle Class!");            
    }
}

class Rectangle : BaseShape
{
    public override void Display()
    {
        Console.WriteLine("Displaying Rectangle Class!");            
    }
}

你的数组可以是一个对象数组。就像这样:

object[] shapes = new object[10];
在您的应用程序中,您需要编写一种方法来显示形状。

一种解决方案是迭代所有形状并调用恰好该形状类型的正确方法。就像这样:

public static void DisplayShapes_BAD(){

    foreach(var item in Shapes)
    {
        if(typeof(Circle) == item.GetType())
        {
            ((Circle)item).Display();
        }
        if(typeof(Rectangle) == item.GetType())
        {
            ((Rectangle)item).Display();
        }
    }
}

但是当应用程序中出现另一种类型的Shape时会发生什么?基本上,您需要修改DisplayShapes_BAD()方法以支持新类型的Shape(在方法主体中添加新的if语句)。

这样做会违反面向对象编程的开放/封闭原则,而且代码维护性不高。

更好的存储形状以避免这种糟糕的方法是使用BaseShape数组。如下所示:

public static List<BaseShape> Shapes = new List<BaseShape>();

以下是将项目添加到这个形状列表的方法:

Shapes.Add(new Circle());
Shapes.Add(new Rectangle());

现在让我们看一下DisplayShapes方法的良好实现。

public static void DisplayShapes_GOOD()
{
    foreach(var item in Shapes)
    {
        item.Display();
    }
}

在上面的方法中,我们使用BaseShape类型的项目调用Display方法。 但是C#如何知道调用正确的方法(例如圆形显示或矩形显示)。这个机制就是多态。

完整代码已分享在Gist中。


2
谢谢。我现在认为我已经理解了这个应用程序。 - user3083590

3
根据我的理解,在Java中,您正在尝试使用BaseClass引用变量baseobj调用DerivedClass对象,这种编码场景是完全有效的,因为它提供了运行时多态性的功能。
在了解运行时多态性之前,让我们先了解Upcasting。当父类的引用变量用于引用子类的对象时,称为Upcasting。
 class A{}

 class B extends A{}

 A obj= new B // Upcasting.

运行时多态性是一种在运行时而非编译时解决对重写方法调用的过程。

由于您没有在派生类中重写show方法,因此您并没有进行运行时多态性,而只是进行了向上转型。当我们想要在运行时解决对重写方法的调用时,向上转型是有用的。


2
首先,为什么允许这样做的问题很简单,因为派生类的实例是基类的实例(子类型多态)。同样地,能够将任何派生类赋值给对象变量:所有.net类最终都派生自object,所以你也可以这样做:object baseObj = new DerivedClass()
用于声明的类型的目标是指示正在使用哪种类型的接口(意图)。如果您将变量声明为对象,则表示只有引用很重要。如果您声明为BaseClass,则表示您正在使用一个对象,其中BaseClass的属性和方法很重要。通过使用BaseClass baseObj = new DerivedClass(),您表明需要BaseClass功能,但是使用DerivedClass实例来定义在BaseClass中描述的映射的工作方式。
这样做的原因之一可能是BaseClass是抽象的(BaseClasses通常是如此),您想要一个BaseClass并且需要一个派生类型来初始化实例,并且选择哪个派生类型应该对实现类型有意义。
更常见的使用原因是,在任何时候,可以将另一个从BaseClass派生的类分配给相同的变量。考虑以下情况:
BaseClass baseObj = SomeCriterium ? (BaseClass)new DerivedClass() : new AlternateDerivedClass(); 

在这个例子中,变量的作用范围仅限于主方法,但如果它在类中任何地方,或者可以通过属性或其他方式进行更改,则使用BaseClass,使用您的类的任何人都可以分配其他BaseClass(派生)实例,而不仅仅是DerivedClass(派生)实例。
最后,一个重新分配的例子,使用接口声明(就多态而言,声明一个实现的接口与声明一个基类一样适用):
IEnumerable<T> values = new List<T>();
if(needfilter)
    values = values.Where(el => filtercriterium);

如果将values声明为List,则无法重复使用值变量进行过滤枚举。基本上,您首先说您仅需要值变量进行枚举。之后,您可以重新分配值并用另一个枚举而不仅仅是列表。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接