逆变接口方法调度/选择 C#

发布于 2025-01-16 16:09:46 字数 1422 浏览 2 评论 0原文

考虑以下代码:

interface ITest<in T>
{
    void DoTest(T instance);
}

class A {}
class B : A {}
class C : B {}

class Test : ITest<A>, ITest<B>
{
    void ITest<A>.DoTest(A instance)
    {
        Console.WriteLine(MethodInfo.GetCurrentMethod());
    }
    
    void ITest<B>.DoTest(B instance)
    {
        Console.WriteLine(MethodInfo.GetCurrentMethod());
    }
}

public class Program
{
    public static void Main()
    {
        ITest<C> test = new Test();
        test.DoTest(new C());
    }
}

输出为:

Void ITest.DoTest(A)

对我来说,这不是预期的行为,或者至少不是大多数开发人员所期望的行为。预期输出为:

Void ITest.DoTest(B)

这里应该使用“最佳”实现,最好的含义是具有最多派生类型参数的泛型接口,直到逆变“静态” “ 类型。

相反,似乎选择了“最差”。

检查生成的 IL 并不会揭示选择机制,因为调用是通过正确的静态类型分派的,因此我假设由 CLR 来选择实现:

.method public hidebysig static 
    void Main () cil managed 
{
    // Method begins at RVA 0x207c
    // Code size 20 (0x14)
    .maxstack 2
    .locals init (
        [0] class ITest`1<class C>
    )

    IL_0000: nop
    IL_0001: newobj instance void Test::.ctor()
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: newobj instance void C::.ctor()
    IL_000d: callvirt instance void class ITest`1<class C>::DoTest(!0)
    IL_0012: nop
    IL_0013: ret
} // end of method Program::Main

为什么它会这样?这种情况下的规则是什么?它在幕后是如何运作的?

Consider the following code:

interface ITest<in T>
{
    void DoTest(T instance);
}

class A {}
class B : A {}
class C : B {}

class Test : ITest<A>, ITest<B>
{
    void ITest<A>.DoTest(A instance)
    {
        Console.WriteLine(MethodInfo.GetCurrentMethod());
    }
    
    void ITest<B>.DoTest(B instance)
    {
        Console.WriteLine(MethodInfo.GetCurrentMethod());
    }
}

public class Program
{
    public static void Main()
    {
        ITest<C> test = new Test();
        test.DoTest(new C());
    }
}

The output is:

Void ITest<A>.DoTest(A)

To me this is not the expected behavior, or at least not the one most developers are expecting. The expected output is:

Void ITest<B>.DoTest(B)

Here the "best" implementation should be used, with best meaning the generic interface with the most derived type parameter up to the contravariant "static" type.

Instead it seems the "worst" is chosen.

Inspecting the generated IL doesn't unveil the selection mechanism as the call is dispatched through the correct static type so I assume it's up to the CLR to select the implementation:

.method public hidebysig static 
    void Main () cil managed 
{
    // Method begins at RVA 0x207c
    // Code size 20 (0x14)
    .maxstack 2
    .locals init (
        [0] class ITest`1<class C>
    )

    IL_0000: nop
    IL_0001: newobj instance void Test::.ctor()
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: newobj instance void C::.ctor()
    IL_000d: callvirt instance void class ITest`1<class C>::DoTest(!0)
    IL_0012: nop
    IL_0013: ret
} // end of method Program::Main

Why it acts like this? What are the rules in this case? How does it work behind the scenes?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

注定孤独终老 2025-01-23 16:09:46

这是在 ECMA-335< 的第 II.12.2 节中指定的/a>.

规范的相关片段如下:

由于类型的差异,给定接口方法有多种实现
参数,类上接口的声明顺序决定了调用哪个方法。
...
类型 T 的继承/实现树是如下形成的 n 叉树:

  • 树的根是 T
    ...
  • 如果 T 具有一个或多个显式接口 Ix,则每个 Ix 的继承/实现树按顺序都是根节点的子节点。

类型 T 的接口和超类的类型声明顺序
使用任意类型 T 的继承/实现树的后序深度优先遍历
省略任何类型的第二个和后续重复项。相同情况的出现
具有不同类型参数的接口不被视为重复

因此我们将类型声明顺序定义为这些接口在类声明中出现的顺序。

当调用接口方法时,VES应使用以下算法来确定要调用的适当方法:

  • 从实例的运行时类开始,接口方法通过该实例
    被调用,使用上面构造的接口表,并替换泛型
    在调用类上指定的参数(如果有):

    1. 对于与接口方法关联的列表中的每个方法,如果存在
      其泛型类型参数与此实例化完全匹配的方法(或
      没有泛型类型参数),然后调用第一个方法
    2. 否则,如果列表中存在其泛型类型参数的方法
      具有正确的方差关系,然后调用列表中的第一个此类方法

这就是说,如果没有完全匹配,则采用类型声明顺序中类型参数具有正确方差关系的第一个方法。

您的示例在“II.12.2.1 接口实现示例”部分中显示为情况 6,其中类型 S4 实现了 IVarImpl (这意味着实现 IVar)和 IVar,示例显示调用该方法S4 实例上的 IVar::P(C) 会产生方法 S1::P(! 0:A)(即 void P(A))被调用。

事实上,如果我们在 Test 的声明中交换 ITestITest 的顺序,我们可以看到 < code>ITest.DoTest(B instance) 实现最终被调用。

This is specified in Section II.12.2 of ECMA-335.

Relevant snippets of the spec follows:

Where there are multiple implementations for a given interface method due to differences in type
parameters, the declaration order of the interfaces on the class determines which method is invoked.
...
The inheritance/implements tree for a type T is the n-ary tree formed as follows:

  • The root of the tree is T
    ...
  • If T has one or more explicit interfaces, Ix, then the inheritance/implements tree for each Ix is a child of the root node, in order.

The type declaration order of the interfaces and super classes of a type T is the
postorder depth-first traversal of the inheritance/implements tree of type T with any
second and subsequent duplicates of any type omitted. Occurrences of the same
interface with different type parameters are not considered duplicates

So we've defined the type declaration order as the order in which these interfaces appear in the class declaration.

When an interface method is invoked, the VES shall use the following algorithm to determine the appropriate method to call:

  • Beginning with the runtime class of the instance through which the interface method
    is invoked, using its interface table as constructed above, and substituting generic
    arguments, if any, specified on the invoking class:

    1. For each method in the list associated with the interface method, if there exists a
      method whose generic type arguments match exactly for this instantiation (or
      there are no generic type parameters), then call the first method
    2. Otherwise, if there exists a method in the list whose generic type parameters
      have the correct variance relationship, then call the first such method in the list

This is saying that if there's no exact match, then take the first method in the type declaration order whose type parameters have the correct variance relationship.

Your example appears as Case 6 in the section "II.12.2.1 Interface Implementation Examples", where the type S4<V> implements both IVarImpl (which means implementing IVar<A>) and IVar<B>, and the example shows that calling the method IVar<C>::P(C) on an instance of S4<A> results in the method S1<A,B>::P(!0:A) (that is, void P(A)) being called.

Indeed, if we swap the order of ITest<A> and ITest<B> in the declaration of Test, we can see that the ITest<B>.DoTest(B instance) implementation ends up being called.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文