当前位置: 首页 > 知识库问答 >
问题:

在C++不允许虚拟模板方法的情况下,C#如何允许虚拟泛型方法?

易焱
2023-03-14

相比之下,Java确实允许虚拟泛型方法。这里也清楚了如何实现:Java泛型在运行时被删除,因此泛型方法是运行时的常用方法,因此不需要修改vtable

但是现在到了C#。C#确实有具体化的泛型。对于具体化泛型,特别是当使用值类型作为类型参数时,泛型方法必须有不同的版本。但是我们遇到了与C++相同的问题:每当对泛型方法进行新的实例化时,我们都需要修改vtable。

我对C#的内部工作没有太深的了解,所以我的直觉可能是完全错误的。那么,对C#/.NET有更深入了解的人能告诉我他们是如何在C#中实现通用虚拟方法的吗?

[MethodImpl(MethodImplOptions.NoInlining)]
static void Test_GenericVCall()
{
    var b = GetA();
    b.M<string>();
    b.M<int>();
}

[MethodImpl(MethodImplOptions.NoInlining)]
static A GetA()
{
    return new B();
}

class A
{
    public virtual void M<T>()
    {
    }
}

class B : A
{
    public override void M<T>()
    {
        base.M<T>();
        Console.WriteLine(typeof(T).Name);
    }
}

共有1个答案

锺离玮
2023-03-14

运行这段代码并分析IL和生成的ASM,我们可以看到发生了什么:

internal class Program
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void Test()
    {
        var b = GetA();
        b.GenericVirtual<string>();
        b.GenericVirtual<int>();
        b.GenericVirtual<StringBuilder>();
        b.GenericVirtual<int>();
        b.GenericVirtual<StringBuilder>();
        b.GenericVirtual<string>();
        b.NormalVirtual();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static A GetA()
    {
        return new B();
    }

    private class A
    {
        public virtual void GenericVirtual<T>()
        {
        }

        public virtual void NormalVirtual()
        {
        }
    }

    private class B : A
    {
        public override void GenericVirtual<T>()
        {
            base.GenericVirtual<T>();
            Console.WriteLine("Generic virtual: {0}", typeof(T).Name);
        }

        public override void NormalVirtual()
        {
            base.NormalVirtual();
            Console.WriteLine("Normal virtual");
        }
    }

    public static void Main(string[] args)
    {
        Test();
        Console.ReadLine();
        Test();
    }
}

我中断了程序。用windbg测试:

.Loadby sos CLR;!BPMD CSharpNewTest CSharpNewTest.program.test

0:000> !muf
CSharpNewTest.Program.Test(): void
    b:A

        002e0080 55              push    ebp
        002e0081 8bec            mov     ebp,esp
        002e0083 56              push    esi
var b = GetA();
    IL_0000: call CSharpNewTest.Program::GetA()
    IL_0005: stloc.0  (b)
>>>>>>>>002e0084 ff15c0371800    call    dword ptr ds:[1837C0h]
        002e008a 8bf0            mov     esi,eax
b.GenericVirtual<string>();
    IL_0006: ldloc.0  (b)
    IL_0007: callvirt A::GenericVirtuallong
        002e008c 6800391800      push    183900h
        002e0091 8bce            mov     ecx,esi
        002e0093 ba50381800      mov     edx,183850h
        002e0098 e877e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e009d 8bce            mov     ecx,esi
        002e009f ffd0            call    eax
b.GenericVirtual<int>();
    IL_000c: ldloc.0  (b)
    IL_000d: callvirt A::GenericVirtuallong
        002e00a1 6830391800      push    183930h
        002e00a6 8bce            mov     ecx,esi
        002e00a8 ba50381800      mov     edx,183850h
        002e00ad e862e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00b2 8bce            mov     ecx,esi
        002e00b4 ffd0            call    eax
b.GenericVirtual<StringBuilder>();
    IL_0012: ldloc.0  (b)
    IL_0013: callvirt A::GenericVirtuallong
        002e00b6 6870391800      push    183970h
        002e00bb 8bce            mov     ecx,esi
        002e00bd ba50381800      mov     edx,183850h
        002e00c2 e84de49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00c7 8bce            mov     ecx,esi
        002e00c9 ffd0            call    eax
b.GenericVirtual<int>();
    IL_0018: ldloc.0  (b)
    IL_0019: callvirt A::GenericVirtuallong
        002e00cb 6830391800      push    183930h
        002e00d0 8bce            mov     ecx,esi
        002e00d2 ba50381800      mov     edx,183850h
        002e00d7 e838e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00dc 8bce            mov     ecx,esi
        002e00de ffd0            call    eax
b.GenericVirtual<StringBuilder>();
    IL_001e: ldloc.0  (b)
    IL_001f: callvirt A::GenericVirtuallong
        002e00e0 6870391800      push    183970h
        002e00e5 8bce            mov     ecx,esi
        002e00e7 ba50381800      mov     edx,183850h
        002e00ec e823e49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e00f1 8bce            mov     ecx,esi
        002e00f3 ffd0            call    eax
b.GenericVirtual<string>();
    IL_0024: ldloc.0  (b)
    IL_0025: callvirt A::GenericVirtuallong
        002e00f5 6800391800      push    183900h
        002e00fa 8bce            mov     ecx,esi
        002e00fc ba50381800      mov     edx,183850h
        002e0101 e80ee49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e0106 8bce            mov     ecx,esi
        002e0108 ffd0            call    eax
b.NormalVirtual();
    IL_002a: ldloc.0  (b)
        002e010a 8bce            mov     ecx,esi
        002e010c 8b01            mov     eax,dword ptr [ecx]
        002e010e 8b4028          mov     eax,dword ptr [eax+28h]
    IL_002b: callvirt A::NormalVirtual()
        002e0111 ff5014          call    dword ptr [eax+14h]
}
    IL_0030: ret 
b.NormalVirtual();
    IL_002a: ldloc.0  (b)
        002e010a 8bce            mov     ecx,esi
        002e010c 8b01            mov     eax,dword ptr [ecx]
        002e010e 8b4028          mov     eax,dword ptr [eax+28h]
    IL_002b: callvirt A::NormalVirtual()
        002e0111 ff5014          call    dword ptr [eax+14h]
b.GenericVirtual<string>();
    IL_0024: ldloc.0  (b)
    IL_0025: callvirt A::GenericVirtuallong
        002e00f5 6800391800      push    183900h
        002e00fa 8bce            mov     ecx,esi
        002e00fc ba50381800      mov     edx,183850h
        002e0101 e80ee49b71      call    clr!JIT_VirtualFunctionPointer (71c9e514)
        002e0106 8bce            mov     ecx,esi
        002e0108 ffd0            call    eax

好的,一般的虚拟调用是通过加载我们的对象b(它在ESI中,被移动到ECX),然后调用CLR!JIT_VirtualFunctionPointer来处理的。还推送了两个常量:edx中的183850。我们可以得出结论,这可能是函数a.genericvirtual 的句柄,因为它对于6个调用站点中的任何一个都没有改变。另一个常量183900看起来是泛型参数的类型句柄。事实上,SSCLI证实了这些怀疑:

HCIMPL3(CORINFO_MethodPtr,JIT_VirtualFunctionPointer,Object*objectUNSAFE,CORINFO_CLASS_HANDLE classHnd,CORINFO_METHOD_HANDLE methodHnd)

因此,查找基本上被委托给JIT_VirtualFunctionPointer,它必须准备一个可以调用的地址。假设它将JIT它并返回一个指向JIT代码的指针,或者创建一个蹦床,当第一次调用时,它将JIT函数。

0:000> uf clr!JIT_VirtualFunctionPointer
clr!JIT_VirtualFunctionPointer:
71c9e514 55              push    ebp
71c9e515 8bec            mov     ebp,esp
71c9e517 83e4f8          and     esp,0FFFFFFF8h
71c9e51a 83ec0c          sub     esp,0Ch
71c9e51d 53              push    ebx
71c9e51e 56              push    esi
71c9e51f 8bf2            mov     esi,edx
71c9e521 8bd1            mov     edx,ecx
71c9e523 57              push    edi
71c9e524 89542414        mov     dword ptr [esp+14h],edx
71c9e528 8b7d08          mov     edi,dword ptr [ebp+8]
71c9e52b 85d2            test    edx,edx
71c9e52d 745c            je      clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)

clr!JIT_VirtualFunctionPointer+0x1b:
71c9e52f 8b12            mov     edx,dword ptr [edx]
71c9e531 89542410        mov     dword ptr [esp+10h],edx
71c9e535 8bce            mov     ecx,esi
71c9e537 c1c105          rol     ecx,5
71c9e53a 8bdf            mov     ebx,edi
71c9e53c 03ca            add     ecx,edx
71c9e53e c1cb05          ror     ebx,5
71c9e541 03d9            add     ebx,ecx
71c9e543 a180832872      mov     eax,dword ptr [clr!g_pJitGenericHandleCache (72288380)]
71c9e548 8b4810          mov     ecx,dword ptr [eax+10h]
71c9e54b 33d2            xor     edx,edx
71c9e54d 8bc3            mov     eax,ebx
71c9e54f f77104          div     eax,dword ptr [ecx+4]
71c9e552 8b01            mov     eax,dword ptr [ecx]
71c9e554 8b0490          mov     eax,dword ptr [eax+edx*4]
71c9e557 85c0            test    eax,eax
71c9e559 7430            je      clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)

clr!JIT_VirtualFunctionPointer+0x47:
71c9e55b 8b4c2410        mov     ecx,dword ptr [esp+10h]

clr!JIT_VirtualFunctionPointer+0x50:
71c9e55f 395804          cmp     dword ptr [eax+4],ebx
71c9e562 7521            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x55:
71c9e564 39480c          cmp     dword ptr [eax+0Ch],ecx
71c9e567 751c            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x5a:
71c9e569 397010          cmp     dword ptr [eax+10h],esi
71c9e56c 7517            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x5f:
71c9e56e 397814          cmp     dword ptr [eax+14h],edi
71c9e571 7512            jne     clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x64:
71c9e573 f6401801        test    byte ptr [eax+18h],1
71c9e577 740c            je      clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)

clr!JIT_VirtualFunctionPointer+0x85:
71c9e579 8b4008          mov     eax,dword ptr [eax+8]
71c9e57c 5f              pop     edi
71c9e57d 5e              pop     esi
71c9e57e 5b              pop     ebx
71c9e57f 8be5            mov     esp,ebp
71c9e581 5d              pop     ebp
71c9e582 c20400          ret     4

clr!JIT_VirtualFunctionPointer+0x6a:
71c9e585 8b00            mov     eax,dword ptr [eax]
71c9e587 85c0            test    eax,eax
71c9e589 75d4            jne     clr!JIT_VirtualFunctionPointer+0x50 (71c9e55f)

clr!JIT_VirtualFunctionPointer+0x70:
71c9e58b 8b4c2414        mov     ecx,dword ptr [esp+14h]
71c9e58f 57              push    edi
71c9e590 8bd6            mov     edx,esi
71c9e592 e8c4800400      call    clr!JIT_VirtualFunctionPointer_Framed (71ce665b)
71c9e597 5f              pop     edi
71c9e598 5e              pop     esi
71c9e599 5b              pop     ebx
71c9e59a 8be5            mov     esp,ebp
71c9e59c 5d              pop     ebp
71c9e59d c20400          ret     4

可以在SSCLI中查看实现,看起来仍然适用:

HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE,
                                                       CORINFO_CLASS_HANDLE classHnd,
                                                       CORINFO_METHOD_HANDLE methodHnd)
{
    CONTRACTL {
        SO_TOLERANT;
        THROWS;
        DISABLED(GC_TRIGGERS);      // currently disabled because of FORBIDGC in HCIMPL
    } CONTRACTL_END;

    OBJECTREF objRef = ObjectToOBJECTREF(objectUNSAFE);

    if (objRef != NULL && g_pJitGenericHandleCache)
    {
        JitGenericHandleCacheKey key(objRef->GetMethodTable(), classHnd, methodHnd);
        HashDatum res;
        if (g_pJitGenericHandleCache->GetValueSpeculative(&key,&res))
            return (CORINFO_GENERIC_HANDLE)res;
    }

    // Tailcall to the slow helper
    ENDFORBIDGC();
    return HCCALL3(JIT_VirtualFunctionPointer_Framed, OBJECTREFToObject(objRef), classHnd, methodHnd);
}
HCIMPLEND

因此,它基本上检查缓存,看看我们以前是否见过这种类型/类组合,否则将它发送到jit_virtualfunctionpointer_framed,后者调用methoddesc::GetMulticallableadDrofVirtualizedCode以获得它的地址。methodDesc调用传递对象引用和泛型类型句柄,以便它可以查找要分派给哪个虚函数,以及虚函数的版本(即使用什么泛型参数)。

所有这些都可以在SSCLI中查看,如果您想更深入地了解--似乎这一点在CLR的4.0版本中没有改变。

 类似资料:
  • 我知道在C++中没有什么比虚拟模板方法更好的了,但似乎它正是我所需要的。有什么办法可以让我使用吗?我很感谢任何建议。 我想通过add方法将实体添加到向量中,这些实体需要是虚拟的,也需要模板,如何避免这种情况?

  • 本文向大家介绍C#处理datagridview虚拟模式的方法,包括了C#处理datagridview虚拟模式的方法的使用技巧和注意事项,需要的朋友参考一下 本文实例讲述了C#处理datagridview虚拟模式的方法。分享给大家供大家参考。具体如下: 希望本文所述对大家的C#程序设计有所帮助。

  • 我对虚拟函数感到困惑。有人告诉我,父类中的虚拟意味着我可以在子类中覆盖它。但是,如果我省略父类中的虚拟,我仍然可以覆盖它。

  • 问题内容: 在继承中虚拟函数如何在后台工作?编译器是否特别对待虚拟函数? 问题答案: 是的,编译器和运行时对虚拟方法的处理方式有所不同。JVM专门利用虚拟方法表进行虚拟方法分配: 对象的调度表将包含对象的动态绑定方法的地址。通过从对象的分派表中获取方法的地址来执行方法调用。属于同一类的所有对象的分发表都是相同的,因此通常在它们之间共享。属于类型兼容类的对象(例如,继承层次结构中的同级对象)将具有具

  • 我有一个非常简单的Spring引导应用程序,它由以下代码保护: 这个想法是为了保护“admin”部分。它公开了一个RESTAPI。问题是所有的帖子都被退回了 405方法不允许 如果我从应用程序中删除安全启动器,它就会工作。这让我相信安全配置是问题所在。但我不知道怎么做。

  • 问题内容: 我已经看完了这个演讲。 幻灯片编号:26引用 这些说法正确吗? 如果两个语句都正确,那么如何使代码可编译,以便jvm运行代码? 提前致谢。 问题答案: 这些说法是完全正确的。 请记住,Java是两件事- 一是语言,其二是虚拟机。尽管将语言限制为不允许基于类型的方法重载使Java成为一种更简单的语言,但JVM仍可以允许这样做以使其更强大。 作为一种语言,Java具有一种编译器,该编译器执