【问题标题】:Why does the behavior of overloaded member functions differ from the behavior of non-member functions为什么重载成员函数的行为与非成员函数的行为不同
【发布时间】:2015-06-04 19:39:13
【问题描述】:

我注意到传入全局函数的参数总是使用“call”,但重载的成员函数调用总是使用“callvirt”。

为什么当类被传递到全局函数(Print())时,它总是调用与基类相关的重载函数而不是派生类?

它似乎选择了在编译时运行 Print 的哪个重载函数,我是否做了一些特别的事情来让它决定不应该在运行时解决这个问题?

这里有一些代码可以说明我的意思:

Module Module1

    Class BaseClass
        Friend Overridable Sub Print()
            Console.WriteLine("BaseClass.Print")
        End Sub
    End Class

    Class DerivedClass
        Inherits BaseClass

        Friend Overrides Sub Print()
            Console.WriteLine("DerivedClass.Print")
        End Sub
    End Class

    Sub Print(iObject As Object)
        Console.WriteLine("Object")
    End Sub

    Sub Print(iClass1 As BaseClass)
        Console.WriteLine("BaseClass")
    End Sub

    Sub Print(iClass2 As DerivedClass)
        Console.WriteLine("DerivedClass")
    End Sub

    Sub Main()
        Dim tBaseClass As New BaseClass
        Dim tDerivedClass As New DerivedClass
        Dim tBaseClassRef As BaseClass
        Dim tObjPtr As Object

        Console.WriteLine()
        Console.WriteLine("Test 1")
        Console.WriteLine()

        'in IL it always uses callvirt for an overloaded member function

        'from MSDN:     The callvirt instruction calls a late-bound method on an object. 
        '               That is, the method is chosen based on the runtime type of obj rather than the compile-time class visible in the method pointer.

        'prints "BaseClass.print"
        'callvirt   instance void Overloading.Module1/BaseClass::Print()
        tBaseClass.Print()

        'in IL it uses "call", even though Print() is overloaded? Why is this?

        'We slip by here because this type is not late bound

        'prints "BaseClass"
        'call       void Overloading.Module1::Print(class Overloading.Module1/BaseClass)
        Print(tBaseClass)




        Console.WriteLine()
        Console.WriteLine("Test 2")
        Console.WriteLine()

        'prints "DerivedClass.print"
        'callvirt   instance void Overloading.Module1/BaseClass::Print()
        tDerivedClass.Print()

        'call works out okay here too because we're still not late bound

        'prints "DerivedClass"
        'call       void Overloading.Module1::Print(class Overloading.Module1/BaseClass)
        Print(tDerivedClass)




        Console.WriteLine()
        Console.WriteLine("Test 3")
        Console.WriteLine()

        tBaseClassRef = tBaseClass



        tBaseClassRef.Print()
        'prints "BaseClass.print"
        'callvirt   instance void Overloading.Module1/DerivedClass::Print()

        'call took our word for it that tBaseClassRef is BaseClass typed
        'which is correct

        Print(tBaseClassRef)
        'prints "BaseClass"
        'call       void Overloading.Module1::Print(class Overloading.Module1/DerivedClass)



        Console.WriteLine()
        Console.WriteLine("Test 4")
        Console.WriteLine()


        tBaseClassRef = tDerivedClass



        tBaseClassRef.Print()
        'prints "DerivedClass.print"
        'IL_0098:  callvirt   instance void Overloading.Module1/BaseClass::Print()

        'Callvirt correctly handles our tBaseClass having a derived class's type


        Print(tBaseClassRef)
        'prints "BaseClass" <!>

        'IL_009f:  call       void Overloading.Module1::Print(class Overloading.Module1/BaseClass)

        '"Call" is ill-equipped to handle our Derived class

        Console.WriteLine()
        Console.WriteLine("Test 5")
        Console.WriteLine()

        tObjPtr = tDerivedClass

        tObjPtr.Print() 
            '(I don't expect this to work, but the error message surprised me)
            '   -- unhandled exception -- "Public member 'Print' on type 'DerivedClass' <??> not found.

        'IL instructions: http://en.wikipedia.org/wiki/List_of_CIL_instructions

        'a. where is it getting DerivedClass from for the exception? 
        'b. Why did LateCall know what type it was, but was unable to find the method?

        '  [ILDASM]
        '  IL_00be:  ldloc.3    //Load local [3] object tObjPtr)
        '  IL_00bf:  ldnull     // push NULL to stack (no string)
        '  IL_00c0:  ldstr      "Print" // push string object for literal string -- is this what we're using as our object?
        '  IL_00c5:  ldc.i4.0   //Push False
        '  IL_00c6:  newarr     [mscorlib]System.Object
        '  IL_00cb:  ldnull     // no string array
        '  IL_00cc:  ldnull     // no class array
        '  IL_00cd:  ldnull     // no bool array
        '  IL_00ce:  ldc.i4.1   //Push True
        '  IL_00cf:  call       object [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.NewLateBinding::LateCall(object,
        '                                                                                                                    class [mscorlib]System.Type,
        '                                                                                                                    string,
        '                                                                                                                    object[],
        '                                                                                                                    string[],
        '                                                                                                                    class [mscorlib]System.Type[],
        '                                                                                                                    bool[],
        '                                                                                                                    bool)

        Console.WriteLine("Marker") 'divider so I can figure out what it is in idasm

        Print(tObjPtr)                                                                                                           
        ' prints "object"

        'again, why use "call" instead of "callvirt"? There is a more specific definition of Print that can handle this
        '[ILDASM]
        '   IL_00e0:  ldloc.3
        '   IL_00e1:  call       object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
        '   IL_00e6:  call       void Overloading.Module1::Print(object)

    End Sub

End Module

编辑:另外,请分享您的资源,我想了解更多有关 IL 工作原理的信息。如果重要的话,这是使用 .NET 4.0。

【问题讨论】:

  • 我在很久以后才找到this answer,这说明编译器旨在确定要使用的 Print() 的重载版本。重载是在编译时根据参数的编译时类型决定的(除了在 C# 4 中使用动态类型),请注意,在 tObjPtr 上对 Print 的 VB.NET 后期绑定调用失败,因为 PrintDerivedClass 中是Friend, not Public

标签: vb.net il


【解决方案1】:

callvirt 指令调用对象的后期绑定方法。

这意味着:在实例对象上,该方法“属于”;对于模块方法(如果是类,则为共享方法),不涉及实例 -> 不需要 callvirt。

这里有两个概念,重载和覆盖(这是一种特定的重载形式); callvirt 只有后者才真正需要。

至于如何选择打印重载可以看related documentation

【讨论】:

  • 我想添加“调用实例方法”部分:en.wikipedia.org/wiki/Common_Intermediate_Language,我缺少的一个微妙之处是 callvirt 假设方法的名称/参数不会改变,但是它允许加载到堆栈上的对象及其类型发生变化。
猜你喜欢
  • 1970-01-01
  • 2011-06-05
  • 2010-12-26
  • 1970-01-01
  • 2012-04-06
  • 1970-01-01
  • 2013-06-29
  • 2019-03-14
相关资源
最近更新 更多