多态性基础

时间:2012-01-03 15:30:49

标签: c# oop inheritance polymorphism

我现在正在研究继承和多态,我遇到了编译器将评估(使用反射?)在基类型引用中存储什么类型的对象以便决定运行什么方法的概念使用覆盖调用方法。

例如:

class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing shape...");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing circle...");
    }
}

static void Main()
{
    Shape theShape = new Circle();
    theShape.Draw();
}

将输出以下内容:

Drawing circle...

我一直认为,在声明任何类型的对象时,它是一种为特定类型的对象指定内存的方式。所以Int32 i = 2l;意味着我现在把内存放在一边作为整数的“占位符”。但是在上面的代码中,我把内存放在了一个Shape上,但它可以实际引用/存储Circle类型的对象!?

12 个答案:

答案 0 :(得分:20)

C#(和Java)中的所有类变量实际上只是引用 - 与所谓的基本类型(例如int,float)和结构相反;当您编写Circle时,new Circle()对象的实际空间是保留的,Shape theShape仅保留参考空间!

任何引用变量都可以包含对所有派生类型的引用;调用哪个方法(如果声明为virtual)的实际解析通过在运行时使用虚拟方法表 (而不是通过反射)来实现。

解释可以使用什么多态(引用wikipedia):

  

[It]允许使用统一接口处理不同数据类型的值。

在您的情况下,Shape对象的公共接口将是Draw()方法。有一个Shapes列表,并在每个Shapes上调用Draw()方法来显示它们是完全合理的。这意味着,为了查看所有形状,您的程序不需要注意在此列表中存储哪些形状 - 将自动调用所有正确的Draw()方法。

每个类变量自动成为引用是C#(和Java)与C ++等语言的重大差异之一,您可以在其中决定变量的生存位置;要使Circle具有值类型(在C ++中),您需要编写:

Circle circle;

如果你想要指向它,你会写

Circle * circle = new Circle();

Java和C#没有明确的符号使变量成为“指针”或“引用” - 只是每个应该保存对象的变量都是指针/引用!

另请注意(例如在C ++中)如果使用指针或引用,则只能使用多态性;那是因为价值类型可以像被宣布的那样被访问,而不是更多;使用引用和指针,当你的实际变量只引用/指向某个东西时,它可以指向许多东西(无论编译器允许它指向什么)。

答案 1 :(得分:17)

  

我现在正在研究继承和多态,我遇到了编译器将评估(使用反射?)在基类型引用中存储什么类型的对象以便决定运行什么方法的概念使用覆盖调用方法。

编译器没有这样的评估;编译器在代码运行之前很久就完成了。 运行时评估引用的对象类型,以决定调用哪个虚拟方法。使用Reflection不会这样做。

编译器评估的是在运行时调用方法时应使用的虚拟方法槽。编译器发出说明“运行时,当此代码运行时,在此插槽上询问此对象的指令” ,并查看该槽中存储的方法,然后执行它。“

如果C#没有内置的虚拟方法,了解如何在C#中实现虚拟方法是很有教育意义的。请参阅my three-part series of articles on that

  

我一直认为,在声明任何类型的对象时,它是一种为特定类型的对象指定内存的方式。

现在是您教育的好时机,开始正确使用“声明”,“对象”等词语。对象未声明。 类型已声明。宣布变量

因此,您的理解是,声明给定类型的局部变量是为特定类型的对象指定内存的一种方式。这几乎是正确的。如果类型是值类型那么这是正确的。如果类型是引用类型,则该类型的本地变量是包含对实际包含该对象的其他存储的引用的存储。

对于C#来说,这是绝对基本的,所以请确保你明白这一点。 string类型的局部变量不包含字符串。它包含一个字符串的引用;字符串完全在其他地方,引用引用该位置。

  

在上面的代码中,我把内存放在了一个Shape上但实际上它可以引用/存储Circle类型的对象!?

它可以将引用存储到Circle中,是的,因为Circle是一种Shape,因此可以在需要引用Shape的地方使用Circle。它不能存储 Circle ,因为Circle不是对Shape 的引用。

如果您的笔记本包含朋友的地址,则可能包含对公寓楼内单元的引用,并且可能包含对房屋的引用。 笔记本不包含公寓楼或房屋。公寓和房屋都是各种住宅;您的笔记本包含对住宅的参考。

假设一位朋友购买了一些土地,建造房屋并向您发送新地址。您不需要在笔记本中为房子分配空间。城市区划部门已经为在其他地方建造的房屋分配了空间。您需要在笔记本中分配空间以获取住所的地址。房子是一种住宅这一事实使得将地址放入笔记本中是合法的。

当您创建作为引用类型实例的对象时,运行时是分区部门 - 它负责为实际对象分配存储。构造函数“构建房屋”。分配局部变量以将引用存储到实际对象的存储中。

值类型没有引用语义;相反,值类型的变量包含实际对象。这就是为什么值类型被称为“值类型”,而引用类型被称为“引用类型”;因为值类型的变量存储实际对象,而引用类型的变量存储对完全位于其他地方的对象的引用。

我不确定这会回答你的问题,因为你似乎没有在你的问题中提出问题。你有什么问题?

答案 2 :(得分:9)

class Contact {
  public string FirstName;
  public string LastName;
}

class Customer : Contact {
  public int OrderNumber;
}

enter image description here

当一个期望引用Contact的方法实际上被赋予对Customer的引用时,它仍然有效,因为Customer引用也是对Contact的引用。

答案 3 :(得分:4)

编译器不理解 reflection ,它使用Virtual Method Table来查找指向正确调用方法的指针,在调用时。

Reflection是我们的工具,开发人员从给定的对象或类型实例获取运行时信息而不是更多。

编译器比那更进一步。

答案 4 :(得分:4)

I've put memory aside for a Shape but it can infact reference/store an object of type Circle!?

不,你在这里不正确。执行Circle()时,将在此处分配的内存将基于Circle类而不是Shape。

你在这里做的是创建一个Shape Class指针,并使用该指针指向circle类的对象/内存。通过多态,你可以通过基类指针(形状)指向子类(Circle)的对象,这就是你能写的原因

Shape shape = new Circle();

答案 5 :(得分:3)

当您声明Shape时,您只是将创建引用后将指向该对象的内存放在一边(您正在创建引用)。

当您实例化Circle时,内存将被消耗,您在声明中保留的空间现在指向Circle

至于调用适当的方法,运行时根本不使用Reflection。所有信息都存储在Virtual Method Table中,并在进行呼叫时解决。

答案 6 :(得分:3)

您已通过在Circle类上调用new来为Circle分配内存。您只需将其存储到Shape对象中即可。这是可能的,因为Circle类包含一个Shape对象作为其基础。

当您执行以下行时:

Shape theShape = new Circle();

您说创建一个Circle对象并使用Shape对象指向它。由于您指向Circle类型的对象,因此调用Draw()的覆盖函数。

查看切片问题,看看当你不使用引用类型时会发生什么。

答案 7 :(得分:3)

在C#或更常见的.NET中,有两种类型的对象:值类型和引用类型。类始终是引用类型。因此,您指定的Shape只是一个引用或指针,而不是形状的完整内存块。

答案 8 :(得分:3)

  

我一直认为,在声明任何类型的对象时,它是一种为特定类型的对象指定内存的方式。

在引用的情况下,您只是为引用指定内存,它对于任何类型的对象具有相同的大小(它是内存地址的大小)。

new表达式将为实际对象进行内存分配。

当你调用theShape.Draw()时,它是.NET运行时决定调用哪个实际方法;在这种情况下,Circle的那个。 (编译器一般不能做出这个决定。)

答案 9 :(得分:3)

在宣布某事时

Shape theShape;

你告诉编译器“theShape”将包含一个Shape的对象,或者可以假装为一个对象(即因为它是一个孩子)。通过这种方式,您可以调用theShape对象上存在的Shape上的任何方法,属性等。

当你说:

Shape theShape = new Circle();

然后你可以被认为是在说上面的内容并且另外说theShape实际上是这个新的Circle对象。显然我们知道Circle会愉快地执行Shape的任何方法,属性等,所以这是完全可以的。

如果我们那么说:

theShape.CircleMethod();

然后事情就会出错。虽然我们知道theShape是Circle,但编译器并不知道这一点。我们所说的只是一个Shape并且在任何地方都没有CircleMethod方法,因此上述调用无效。

如果您想知道为什么会这样,那么请考虑以下代码:

public void doSomething()
{
    Shape theShape = getShape();
    theShape.CircleMethod();
}

public Shape getShape()
{
    return new Circle();
}

getShape方法将返回一个圆圈但是在此你可以清楚地看到不允许调用CircleMethod。 doSomething()方法可能甚至不知道存在圆(例如因为这些方法在不同的程序集中)所以它只能通过将theShape的内容视为Shape来工作,无论实际内部是什么。

我注意到你在评论的某个地方说你希望Circle circle = new Shape();成为它的工作方式。

希望上述内容可以解释为什么不这样做。如果没有,那么希望另一个类比会有所帮助。

其他人说圈子只是一个参考。想象一下它是一个遥控器,声明告诉你遥控器有哪些按钮。在我的原始示例中,名为theShape的远程控件上有Shape上所有方法的按钮,因为这是它的声明方式。当您按遥控器上的按钮时,它会调用它指向的真实对象上的方法。您可能会认为遥控器不完整,因为Circle上有许多我们没有按钮的东西,但关键是控件上的所有按钮都可以工作,因为Circle支持他们。

在示例Circle circle = new Shape();中,我们的遥控器具有Circle的所有按钮,但显然当我们按下CircleMethod的按钮时,我们指向遥控器的Shape对象没有想法该怎么做。这就是为什么这不起作用。

至于你为什么要这样做。我的第二个例子可能是一个很好的例子。您可能从另一个方法(例如,可能读取用户输入选择圆形或矩形的方法)获取Shape,您要做的就是为用户绘制选定的形状。你不需要知道他们选择了哪一个,因为你知道它们都是形状,Shapes有一个Draw方法所以你可以调用它。

P.S。我知道其中一些与其他地方的内容非常相似,但我觉得他们都专注于技术方面,我试图通过类比它的工作方式而不是堆栈和堆栈之类的技术方面,我不这样做尽管我认为自己是一个非常优秀的程序员,但大部分时间都在关心。 ; - )

答案 10 :(得分:3)

首先,值类型和引用类型之间存在差异。 当您声明引用类型(例如,Shape类的一个实例)时,将在堆栈上分配内存,该内存将包含对堆上内存插槽的引用。

此外,将在堆上创建Type对象;一旦用Shape实例初始化Circle变量,就会在堆上创建一个Circle对象,并且它还有一个指向Circle Type对象的“类型指针”。

当调用[overriden]虚拟Draw方法时,CLR知道哪个实现必须执行,因为它可以通过类型指针找到正确的类型。

有关更深入的信息,请参阅this文章。

答案 11 :(得分:2)

执行new Circle()时会分配内存,因此内存中有Circle