【问题标题】:How C# resolve interface's "virtual" target?C#如何解析接口的“虚拟”目标?
【发布时间】:2018-03-11 18:16:11
【问题描述】:

假设我在C++,我有这个伪接口(@98​​7654325@,只有pure virtual methods):

class IErrorLog
{
public:
    virtual bool closeLog() = 0;
};

class FileErrorLog : public IErrorLog
{
public:
    FileErrorLog(){}
    ~FileErrorLog(){}

    bool closeLog() { 
        std::cout << "Close FileErrorLog" << std::endl; 
        return true;
    }
};

int main()
{   
    FileErrorLog fileErrorLog;
    IErrorLog *log = &fileErrorLog;
    log->closeLog();
}

对于I've learnedcloseLog,使用动态类型IErrorLog,称为将vtableFileErrorLog 去虚拟化并选择所需的目标函数(closeLog() 指针)。

因为在C#interfaces 中并没有真正被认为是类,所以当我做这样的事情时:

interface IErrorLog {
    void closeLog ();
}

public class FileErrorLog : IErrorLog
{
    public FileErrorLog() {}

    public void closeLog() {
        Console.WriteLine("Close FileErrorLog");
    }        
}

public class Program
{
    public static void Main(string[] args)
    {
        IErrorLog log = new FileErrorLog();
        log.closeLog();
    }
}

C# 将如何解决closeLog()?是一样的机制吗?

因为这里的IErrorLog log 不再是abstract class。它是原生类型。因此,我不认为log 是指向FileErrorLog 的指针。

你能给我解释一下吗?

【问题讨论】:

  • 仅仅因为您通过IErrorLog 引用log 并不能使其成为实际类型。实际上它仍然是FileErrorLog。 IIRC C# 在 C++ 中使用相同的 vtable 设计。
  • @SombreroChicken 我认为 c# 设计有点不同。考虑到在FileErrorLog 中,您可以同时实现closeLogIErrorLog.CloseLog(如果我没记错的话,后者将在OP 场景中调用)
  • 这是旧方法的入门指南.NET Framework Internals - Interface Vtable Map and Interface Map。这个模型在virtual stub dispatch 中发生了相当大的变化,但显示了“旧式”的 vtable 接口调度。

标签: c# c++ interface vtable


【解决方案1】:

C# 将如何解决 closeLog()?

运行时将使用实现定义的机制正确解析调用。

在接口的情况下,这种机制相当复杂,并且有有趣的性能影响。

是相同的机制吗?

它是否与某些 C++ 编译器用于不同类型系统的机制相同?几乎肯定不会。

然而,这些机制相似,因为有函数指针表挂在对象实例指针之外,并且在运行时进行查找以进行方法分派。

因此,我不认为 log 是指向 FileErrorLog 的指针。

我假设您的意思是“托管指针”;在 C# 中,我们希望您将引用描述为“引用”;非托管指针非常不同。

由于托管指针不同是错误,因此您有一个错误的信念。你从错误的信念中得出的任何结论都是不合理推理的结果,不可靠。

根据您的问题和您的一些 cmets,您的核心错误信念似乎是对对象的引用的内存表示取决于用于存储引用的变量的类型。 这种信念 100% 完全错误,所以现在停止相信它。在 CLR 中,引用转换是表示保留转换

如果对类类型 C 的对象的引用由数字 0x12345678 表示,然后您将其转换为对我用 C 实现的接口的引用,则表示仍然是 0x12345678。

任何类似于该 learncpp 源代码的教程用于理解 C# 行为?

本站不推荐教程。

【讨论】:

    【解决方案2】:

    在 C#(以及在 CLI 上运行的所有语言)中,引用是对对象实例的引用,而不是对继承类型层次结构中的特定 vtable 或级别的引用。编译时类型专门用于缩短方法的名称。在示例类型层次结构中:

    interface IFoo
    {
        void Bar();
    }
    
    class CFoo : IFoo
    {
        public virtual void Bar()
        {
        }
    
        void IFoo.Bar()
        {
        }
    }
    
    class CFoo2 : CFoo
    {
        public override void Bar()
        {
        }
    }
    

    var foo = new Foo(); foo.Bar() 发出callvirt 操作码时使用的“完整”名称是CFoo::Bar。编译器只是使用“RValue”的类型来避免你输入它。

    在编译时没有进行强制转换或其他转换来调用继承的方法。无论 c# 中指定的类型如何,引用的值都保持不变。

    考虑 C# 中的以下调用及其等效的 IL 编码:

    private static void CallFooBar()
    {
        // L_0000: newobj instance void InterfaceDemo.CFoo::.ctor()
        CFoo foo = new CFoo();
    
        // Note that the next call (since the variable was typed CFoo) is not calling
        // the interface implementation.
        //                                              VVVV
        // L_0005: callvirt instance void InterfaceDemo.CFoo::Bar()
        foo.Bar();
    
        // L_000a: ret
    }
    
    private static void CallFooIFooBar()
    {
        // Note that the type cast does not affect the value reference on the
        // stack (no cast is performed).  The instantiation looks identical to
        // CallFooBar above. 
        // 
        // L_0000: newobj instance void InterfaceDemo.CFoo::.ctor()
        CFoo foo = new CFoo();
        IFoo ifoo = foo;
    
        // Note that the call is made to the interface method (to be dispatched
        // through the interface method tables)
        //                                              VVVV
        // L_0005: callvirt instance void InterfaceDemo.IFoo::Bar()
        ifoo.Bar();
    
        // L_000a: ret
    }
    
    private static void CallFooIFooBar2()
    {
        // Note that all of the compiled IL is identical to CallFooIFooBar
        //
        // L_0000: newobj instance void InterfaceDemo.CFoo::.ctor()
        IFoo foo = new CFoo();
    
        // L_0005: callvirt instance void InterfaceDemo.IFoo::Bar()
        foo.Bar();
    
        // L_000a: ret
    }
    
    private static void CallCFoo2Bar()
    {
        // Note that all of the IL excepting for the newobj call is identical.
        // virtual method resolution takes place at runtime (or at JIT) - not 
        // at compile time.
        // 
        // L_0000: newobj instance void InterfaceDemo.CFoo2::.ctor()
        IFoo foo = new CFoo2();
        // L_0005: callvirt instance void InterfaceDemo.IFoo::Bar()
        foo.Bar();
        // L_000a: ret
    }
    

    从命名方法(例如:IFoo::BarCFoo2::Bar 的实现方法)的实际转换是在运行时或 JIT 时执行的,而不是在编译时。在较早的运行时,接口的callvirt 指令将被Jitted 为:

    ; C#: ((IFoo)foo).Bar(); 
    ; C:  (*(foo->TypeHandle->InterfaceMap[0x30]))(foo)
    mov ecx,edi                   ; move "foo" pointer into ecx 
    mov eax,dword ptr [ecx]       ; Dereference to place MethodTable into eax
    mov eax,dword ptr [eax+0Ch]   ; Dereference to interface map address
                                  ; (offset 12 is constant for that version of
                                  ; the CLR)
    mov eax,dword ptr [eax+30h]   ; move the ifc impl start slot into eax 
                                  ; (30h is discovered at time of JIT by
                                  ; examining the loaded type hierarchy)
    call dword ptr [eax]          ; call foo.Bar
    

    这里与 C++ 的区别在于,选择接口 vtable 的取消引用仅在 JIT 时间完成,并且仅用于调用接口方法。

    【讨论】:

      【解决方案3】:

      您的代码与以下内容基本相同:

          FileErrorLog tmp = new FileErrorLog();
          IErrorLog log = tmp;
          log.closeLog();
      

      您只需进行从FileErrorLogIErrorLog 的隐式引用转换。被引用的对象在这两种情况下完全相同; c# 中的引用转换始终保持身份。

      对接口成员IErrorLog.closeLog的调用则被视为虚拟调用; IIRC 接口成员是虚拟的“最终”成员。

      【讨论】:

      • 但是您没有“类”参考。接口不是一个类。使用哪种参考?接口参考?或者即使接口不是类(因此不能是对象),编译器也会将语法翻译为“指向该对象的指针”?
      • @markzzz 这有什么关系?两个变量都指向同一个对象,唯一改变的是引用的类型。接口类型变量是常规引用类型变量;它的值是被引用对象的“地址”。
      • 是的,但是 IErrorLog "is-not-a" FileErrorLog。它是一种“可以做”的关系。因为它是一个接口,而不是一个类。它就像一个指向“类”对象的“结构”引用。你不能那样做。
      • @Markzzz 嗯? struct 与这里有什么关系?在c#中,接口类型变量是一个普通的引用类型变量,它绝对与值类型引用(结构)没有任何相似之处。我不确定你从哪里得到这个想法,但它绝对是错误的。
      • 令人困惑的是接口和类在c#中是不同的“东西”。如果它们基本上是两个不同的“概念”,如何将 FileErrorLog 隐式转换为 IErrorLog?它不是从基类继承的派生类(强制转换有意义并自动执行)。这里的接口不是抽象类,是另外一回事。
      猜你喜欢
      • 2011-04-25
      • 2015-09-02
      • 2011-03-03
      • 1970-01-01
      • 2011-02-13
      • 2021-08-27
      • 1970-01-01
      • 1970-01-01
      • 2010-11-19
      相关资源
      最近更新 更多