【问题标题】:Weird extension method overload resolution奇怪的扩展方法重载解析
【发布时间】:2016-07-25 22:10:31
【问题描述】:

我无法让编译器解决扩展方法的正确重载问题。对我来说最好的解释方法是使用一些代码。这是一个演示问题的LINQPad 脚本。由于我遇到的问题,这不会编译:

void Main(){
    new Container<A>().Foo(a=>false);
}

interface IMarker{}
class A : IMarker{
    public int AProp{get;set;}
}
class B : IMarker{
    public int BProp{get;set;}
}
class Container<T>{}

static class Extensions{
    public static void Foo<T>(this T t, Func<T, bool> func)
        where T : IMarker{
        string.Format("Foo({0}:IMarker)", typeof(T).Name).Dump();
    }
    public static void Foo<T>(this Container<T> t, Func<T, bool> func){
        string.Format("Foo(Container<{0}>)", typeof(T).Name).Dump();
    }
}

我得到的错误是:

以下方法或属性之间的调用不明确:“Extensions.Foo&lt;Container&lt;A&gt;&gt;(Container&lt;A&gt;, System.Func&lt;Container&lt;A&gt;,bool&gt;)”和“Extensions.Foo&lt;A&gt;(Container&lt;A&gt;, System.Func&lt;A,bool&gt;)

在我看来,它一点也不含糊。第一种方法不接受Container&lt;T&gt;,只接受IMarker。似乎通用约束没有帮助解决重载问题,但在这个版本的代码中,它们似乎是:

void Main(){
    new A().Bar();
    new A().Foo(a=>a.AProp == 0);
    new A().Foo(a=>false); // even this works
    new A().Foo(a=>{
        var x = a.AProp + 1;
        return false;
    });

    new Container<A>().Bar();
    new Container<A>().Foo(a=>a.AProp == 0);
    new Container<A>().Foo(a=>{
        var x = a.AProp + 1;
        return false;
    });
}

interface IMarker{}
class A : IMarker{
    public int AProp{get;set;}
}
class B : IMarker{
    public int BProp{get;set;}
}
class Container<T>{}

static class Extensions{
    public static void Foo<T>(this T t, Func<T, bool> func)
        where T : IMarker{
        string.Format("Foo({0}:IMarker)", typeof(T).Name).Dump();
    }
    public static void Foo<T>(this Container<T> t, Func<T, bool> func){
        string.Format("Foo(Container<{0}>)", typeof(T).Name).Dump();
    }

    public static void Bar<T>(this T t) where T : IMarker{
        string.Format("Bar({0}:IMarker)", typeof(T).Name).Dump();
    }
    public static void Bar<T>(this Container<T> t){
        string.Format("Bar(Container<{0}>)", typeof(T).Name).Dump();
    }
}

这会编译并产生预期的结果:

条形图(A:IMarker)
Foo(A:IMarker)
Foo(A:IMarker)
Foo(A:IMarker)
酒吧(容器
Foo(容器
)
Foo(容器
)

似乎只有当我没有在 lambda 表达式中引用 lambda 参数时才有问题,然后才使用 Container&lt;T&gt; 类。在调用Bar 时,没有 lambda,它工作正常。当使用基于 lambda 参数的返回值调用 Foo 时,它工作正常。即使 lambda 的返回值与示例中未编译的返回值相同,但 lambda 参数被虚拟赋值引用,它仍然有效。

为什么它在这些情况下有效,但在第一种情况下无效?我做错了什么,还是我发现了编译器错误?我已经确认了 C# 4 和 C# 6 中的行为。

【问题讨论】:

  • @ManoDestra:编辑是因为你不喜欢我放置花括号的位置。
  • 这是 C#。因此进行了编辑。
  • 我不明白你的意思。
  • 一方面,该文档没有定义大括号的位置。第二,那是一套指导方针,而不是严格的规则。三,你不是风格警察。四个人,请去拖钓别人,不要试图强迫我的代码适合你的个人口味。

标签: c# extension-methods overload-resolution


【解决方案1】:

哦,我是在重新阅读自己的答案后才知道的!好问题=) 重载不起作用,因为它在解决重载时没有考虑约束where T:IMaker(约束不是方法签名的一部分)。当您在 lambda 中引用参数时,您(可以)向编译器添加提示:

  1. 这行得通:

    new Container<A>().Foo(a => a.AProp == 0);
    

    因为这里我们确实暗示了 a:A;

  2. 即使对参数的引用也不起作用:

    new Container<A>().Foo(a => a != null);
    

    因为仍然没有足够的信息来推断类型。

据我了解规范,在“Foo 场景”中,第二个 (Func) 参数的推理可能会失败,从而使调用不明确。

这是规范 (25.6.4) 所说的:

类型推断作为方法调用(第 14.5.5.1 节)的编译时处理的一部分发生,并且发生在调用的重载解决步骤之前。如果在方法调用中指定了特定方法组,并且没有将类型参数指定为方法调用的一部分,则类型推断将应用于方法组中的每个泛型方法。如果类型推断成功,则推断的类型参数用于确定后续重载决议的参数类型。

如果重载决议选择一个泛型方法作为调用的方法,那么推断的类型参数将用作调用的运行时类型参数。如果特定方法的类型推断失败,则该方法不参与重载决策。类型推断的失败本身并不会导致编译时错误。但是,当重载解析找不到任何适用的方法时,通常会导致编译时错误。

现在让我们进入非常简单的“酒吧场景”。在类型推断之后,我们将得到只有一个方法,因为只有一个适用:

  1. Bar(Container&lt;A&gt;) for new Container&lt;A&gt;()(不实现 IMaker)
  2. Bar(A) 代表new A()(不是容器)

这是ECMA-334 specification,以防万一。 附:我不能 100% 确定我做对了,但我更愿意认为我掌握了关键部分。

【讨论】:

  • 好的,到目前为止,我和你在一起,约束不是方法签名的一部分(这也是你不能单独通过约束重载方法的原因),以及 lambda 参数的使用给编译器一个明确选择重载的提示。我怀疑这一点。但是我仍然不明白为什么对Bar 的调用在我的第二个sn-p 中起作用。他们完全没有 lambda。
  • 我猜我们的答案在语言规范的 25.6.4 和 14.4.2 中。但我现在是早上 7 点,所以直到明天我才能提出任何想法。
  • 您是否愿意在您的答案中进一步扩展这一点,也许可以引用规范中的一些引用?
  • 我在答案中添加了一些信息并对其进行了润色。抱歉,在写这么多编辑时正在考虑)
  • 我想你明白了。混淆的是类型推断步骤,而这正是我没有真正考虑的。
【解决方案2】:

Sergey 想通了为什么我试图做的事情不起作用,我想。这就是我决定做的事情:

void Main(){
    new A().Bar();
    new A().Foo(a=>a.AProp == 0);
    new A().Foo(a=>false);

    new Container<A>().Bar();
    new Container<A>().Foo(a=>a.AProp == 0);
    new Container<A>().Foo(a=>false); // yay, works now!
}

interface IMarker<T>{
    T Source{get;}
}

class A : IMarker<A>{
    public int AProp {get;set;}
    public A   Source{get{return this;}}
}
class B : IMarker<B>{
    public int BProp {get;set;}
    public B   Source{get{return this;}}
}

class Container<T> : IMarker<T>{
    public T Source{get;set;}
}

static class Extensions{
    public static void Foo<T>(this IMarker<T> t, Func<T, bool> func){}
    public static void Bar<T>(this IMarker<T> t){}
}

不幸的是,这对我的应用程序来说是一个很大的变化。但至少扩展层会更简单,最终对于编译器和人类来说都不会那么模糊,这是一件好事。

【讨论】:

  • 如果 Container 真的是一个有意义的 IMaker,那么它看起来是一个不错的重构决定。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多