【问题标题】:Polymorphism fundamentals多态性基础
【发布时间】:2012-02-01 15:04:24
【问题描述】:

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

例如:

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 类型的对象!?

【问题讨论】:

    标签: c# oop inheritance polymorphism


    【解决方案1】:

    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++ 中),如果您使用指针或引用,则只能使用多态;那是因为值类型只能按照声明的方式访问,而不是更多;使用引用和指针,当您的实际变量仅引用/指向某个东西时,它可以指向许多东西(无论编译器允许它指向什么)。

    【讨论】:

    • 但是我引用了一个不同类型的对象!?我知道由于继承,它们显然是相关的,但是尽管 Circle 对象可能比 Shape 具有更多的属性和方法,所以可能会有很大的不同。我会认为这在语法上是不正确的。为什么你需要这样做?你只需做 Circle circle = new Circle();确定??
    • 你正在引用一个派生类型的对象,这是完全可能的,这就是多态的用途。
    • > 你为什么需要这样做?这样您就可以针对 Shape 接口编写代码,而不必担心所使用的特定类型的 Shape。如果你想做的只是 Draw () 一些你不需要担心 Shape 是圆形、方形、三角形等的东西。
    • new关键字与存储位置无关,也与类型是引用类型还是值类型无关。考虑堆和堆栈也没有什么价值。见blogs.msdn.com/b/ericlippert/archive/2009/04/27/…
    【解决方案2】:

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

    编译器不做这样的评估;编译器在代码运行之前很久就完成了执行。 runtime 评估引用的对象类型,以确定调用哪个虚拟方法。它不使用反射这样做。

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

    如果 C# 没有内置虚拟方法,那么看看如何在 C# 中实现虚拟方法是很有教育意义的。见my three-part series of articles on that

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

    现在是您开始正确使用“声明”、“对象”等词语的好时机。未声明对象。 类型 被声明。 变量被声明了。

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

    这对 C# 来说是绝对基础,因此请确保您理解这一点。字符串类型的局部变量不包含字符串。它包含一个字符串的reference;该字符串完全位于其他位置,并且引用指向该位置。

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

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

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

    假设一个朋友买了一些土地,盖了一所房子,然后给你发了他们的新地址。您不需要在笔记本中为房子分配空间。城市分区部门已经为在其他地方建造房屋分配了空间。您需要在笔记本中分配空间用于住宅地址。房子是一种住所这一事实使得将地址放在笔记本上是合法的。

    当您创建作为引用类型实例的对象时,运行时是分区部门——它负责为实际对象分配存储空间。建造者“建造房子”。分配局部变量以将 reference 存储到实际对象的存储中。

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

    我不确定这是否能回答您的问题,因为您似乎实际上并没有在您的问题中提出问题。你的问题是什么?

    【讨论】:

      【解决方案3】:
      class Contact {
        public string FirstName;
        public string LastName;
      }
      
      class Customer : Contact {
        public int OrderNumber;
      }
      

      当期望引用联系人的方法实际上被赋予了对客户的引用时,它仍然有效,因为客户引用也是对联系人的引用。

      【讨论】:

      • 好吧,但是假设我有以下声明: Contact contact = new Customer();那么写contact.OrderNumber会不会不正确??
      • 您无法访问 OrderNumber 这样做,因为指向客户对象的变量是“联系人”,即使内存中有客户对象,它也不知道客户的实现。但如果你投它会起作用: ((Customer) contact).OrderNumber = 5;
      【解决方案4】:

      编译器无法通过 reflection 理解它,它使用Virtual Method Table 以便在调用时找到指向要调用的正确方法的指针。

      Reflection 是一个工具,供我们开发人员从给定的对象或类型实例获取运行时信息,仅此而已。

      编译器走得更远。

      【讨论】:

        【解决方案5】:

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

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

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

        Shape shape = new Circle();

        【讨论】:

        • 那么引用 shape.CircleProperty;??在 Circle 类中哪里定义了 CircleProperty?
        • 你将无法做到这一点,因为 Shape 引用只会知道那些在 Shape 类中公开的属性。您可以在 Shape 指针中保存 Circle 类的引用,但这并不意味着它允许您访问 Circle 类的公共属性
        【解决方案6】:

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

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

        至于调用适当的方法,运行时根本不使用反射。所有信息都存储在Virtual Method Table 中,并在拨打电话时解析。

        【讨论】:

          【解决方案7】:

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

          当您执行以下行时:

          Shape theShape = new Circle();
          

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

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

          【讨论】:

            【解决方案8】:

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

            【讨论】:

            • 我明白你在说什么,但基于此,我认为只能从孩子引用父母,因为孩子可能具有“额外”的属性和方法。所以我假设 Circle circle = new Shape();是否正确,因为 circle 那么具有基本 Shape 类所拥有的属性和方法以及更多的能力?似乎,尽管圆是派生类,但我有点无视通过使用不同类型的引用来引用和对象的正确方法?
            【解决方案9】:

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

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

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

            当您调用theShape.Draw() 时,.NET 运行时决定将调用哪个实际方法;在这种情况下,Circle 的那个。 (编译器通常无法做出此决定。)

            【讨论】:

              【解决方案10】:

              当声明某事为

              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 的内容视为一个形状来工作,无论里面实际是什么。

              我注意到在 cmets 的某个地方,您说您希望 Circle circle = new Shape(); 是它的工作方式。

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

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

              在示例Circle circle = new Shape(); 中,我们的遥控器具有Circle 的所有按钮,但很明显,当我们按下CircleMethod 的按钮时,我们将遥控器指向的Shape 对象不知道该做什么。这就是为什么那行不通的原因。

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

              附:我知道其中一些与其他地方说的非常相似,但我觉得他们都专注于技术方面尽管我认为自己是一个非常优秀的程序员,但大部分时间都在乎。 ;-)

              【讨论】:

                【解决方案11】:

                首先,值类型和引用类型是有区别的。 当您声明一个引用类型(例如 Shape 类的一个实例)时,会在堆栈上分配内存,其中将包含对堆上内存槽的引用。

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

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

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

                【讨论】:

                  【解决方案12】:

                  在执行new Circle() 时分配内存,因此您在内存中有一个Circle

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-04-21
                    • 1970-01-01
                    • 2017-05-24
                    • 2015-07-09
                    • 2018-01-09
                    • 1970-01-01
                    相关资源
                    最近更新 更多