【问题标题】:runtime type vs compile-time type method invocation运行时类型与编译时类型方法调用
【发布时间】:2013-01-21 16:39:10
【问题描述】:

C# 4.0 规范如下:

当一个虚方法被调用时,实例的运行时类型为 该调用发生的位置决定了实际的方法 要调用的实现。在非虚拟方法调用中, 实例的编译时类型是决定因素。

起初,我认为这与初始化有关。例如,给定两个初始化:

BaseClass bcDerived = new Derived();BaseClass bcBase = new BaseClass();

以及辅助类中的重载:

public virtual void Method(Derived d)
{
     Console.WriteLine("Result = derived called");
}

public virtual void Method(BaseClass d)
{
     Console.WriteLine("Result = base called");
}

Method 调用在这种情况下不受virtual 关键字的影响。不管是否标记了virtual,都会调用派生最少的重载。只有在 Derived 类中的override 期间,方法调用才会发生变化。

那么,“运行时类型”和“编译时类型”是什么意思?它们如何影响方法调用?

【问题讨论】:

  • 请说明Method 的声明位置和使用方式。
  • @BrianRasmussen - 在 OP 中,这些方法是辅助类的成员。
  • 谢谢。在那种情况下,我不确定接受的答案如何与您的问题相匹配,因为它不包含帮助程序类。不过,只要你对答案满意,我就很好。
  • @BrianRasmussen - 起初,我以为他已经在callvirt 指令中回答了这个问题(就像我将苹果与橙子进行比较一样)。我假设我的示例中缺少关键字。经过更多的思考,你是对的......它并没有直接解决这个例子。 callvirt 指令应该仍然存在于我的示例中,因为我指定了虚拟。我知道我通过测试重载做了一些奇怪的事情。我的目标是完全理解 C# 规范定义。
  • @P.Brian.Mackey 我只是将我的答案改写为更清楚,并直接谈论您所指的内容。这里的问题是您引用的规范部分与您的问题不匹配;)它指的是对具有虚拟与非虚拟的对象的方法调用,而不是参数和参数列表的重载解决方案。

标签: c# language-specifications


【解决方案1】:

这更多的是虚拟与非虚拟方法的问题,以及调用是如何发生的。您引用的规范部分涉及对变量的方法调用 - 调用 bcDerived.SomeMethod(),而不是调用 foo.SomeMethod(bcDerived)

您引用的规范是指您有非虚拟方法的情况:

public class A
{
    public void Foo() { Console.WriteLine("A.Foo"); }
    public virtual void Bar() { Console.WriteLine("A.Bar"); }
}
public class B : A
{
    public new void Foo() { Console.WriteLine("B.Foo"); }
    public override void Bar() { Console.WriteLine("B.Bar"); }
}

然后调用的方法将由编译器确定,在编译时,这样做:

A someInst = new B();
someInst.Foo();

将导致此调用 A.Foo()无论 someInst 引用 A 的哪个子类,因为这是一个非虚拟方法。

但是,如果您有一个虚方法,则callvirt 指令由编译器指定,这会将决定移至运行时。这意味着:

 someInst.Bar();

将调用B.Bar(),而不是A.Bar()

在您的情况下,您不是在调用虚拟方法(在规范所指的意义上),而是在执行标准方法解析。 C# Spec 的 7.5.3 详细处理了重载解决方案。在您的情况下,编译器会检查参数列表 (bcDerived),并将其定义为类型 BaseClass。对此的“最佳匹配”将是 public virtual void Method(BaseClass d),因为参数列表直接匹配参数列表,因此在编译时使用。

如果您查看规范,方法重载决议不会直接使虚拟方法调用生效 - 它只查看类型之间的隐式转换。

【讨论】:

    【解决方案2】:
    Using these two classes as examples:
    
    public class Parent
    {
        public void NonVirtual()
        {
            Console.WriteLine("Nonvirtual - Parent");
        }
        public virtual void Virtual()
        {
            Console.WriteLine("Virtual - Parent");
        }
    }
    
    public class Child : Parent
    {
        public override void Virtual()
        {
            Console.WriteLine("Virtual - Child");
        }
    
        public void NonVirtual()
        {
            Console.WriteLine("Nonvirtual - Child");
        }
    }
    

    通过这段代码可以最清楚地看到虚拟和非虚拟之间的区别:

    Parent childAsParent = new Child();
    childAsParent.Virtual();
    childAsParent.NonVirtual();
    

    打印出来:

    虚拟 - 孩子
    非虚拟 - 父级

    在虚拟方法的情况下,它在运行时看到childAsParent 的类型是一个孩子,因此执行孩子对Virtual 的定义。对于非虚方法,它看到变量的编译时类型是Parent,并忽略了实际实例是Child 并使用父级实现的事实。

    virtual 不用于根据参数的类型确定使用哪个方法的重载。确定要调用的方法的哪个重载总是在编译时完成(不使用 dynamic 时),从不在运行时完成,因此在您的示例中,它将始终选择 Method 的重载,基于 compile time 变量的类型。

    【讨论】:

      【解决方案3】:

      在这种情况下,参数的编译时类型将始终用于确定要调用的重载。虚拟分派取决于调用该方法的对象的运行时类型。

      编译时类型是编译器确定的对象类型,运行时类型是代码执行时的实际类型。使用您的示例:

      BaseClass bcDerived = new Derived()
      

      编译时类型为BaseClass,而运行时类型为Derived

      为了理解我们需要稍微扩展你的类的含义:

      class BaseClass 
      { 
        public virtual void SomeMethod() 
        {
          Console.WriteLine("In base class");
        }
      }
      
      class Derived : BaseClass
      { 
        public override void SomeMethod() 
        {
          Console.WriteLine("In derived class");
        }
      }
      

      现在调用bcDerived.SomeMethod() 将取决于bcDerived 的运行时类型,即是调用BaseClass 实现还是调用Derived 实现。

      Eric Lippert 在 .Net(其中 part one is here)中写了一个非常好的关于虚拟调度的三部分系列,我强烈建议您阅读它们以更全面地理解该主题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-10-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-13
        • 1970-01-01
        • 2010-09-13
        相关资源
        最近更新 更多