奇怪的扩展方法重载解析

4

我在编译器中遇到了一个问题,无法解析扩展方法的正确重载。最好的方法是通过一些代码来解释。这里有一个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<Container<A>>(Container<A>, System.Func<Container<A>,bool>)' 和 'Extensions.Foo<A>(Container<A>, System.Func<A,bool>)' 方法或属性不明确。
对我来说,这根本不是模棱两可的。第一个方法不接受 Container<T>,只接受 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();
    }
}

这段代码编译并输出了预期的结果:

Bar(A:IMarker)
Foo(A:IMarker)
Foo(A:IMarker)
Foo(A:IMarker)
Bar(Container<A>)
Foo(Container<A>)
Foo(Container<A>)

只有在lambda表达式中不引用lambda参数,并且仅涉及Container<T>类时,才会出现问题。当调用Bar时,没有lambda表达式,因此可以正常工作。当以基于lambda参数的返回值调用Foo时,它也可以正常工作。即使lambda的返回值与未编译的示例中的返回值相同,但是通过一个虚拟赋值来引用lambda参数,它也可以正常工作。

为什么它们能正常工作而第一种情况不能?我做错了什么,还是我发现了编译器的错误?我已经在C# 4和C# 6中确认了这种行为。


@ManoDestra:不欢迎因为你不喜欢我的花括号放置方式而进行的修改。 - P Daddy
这是C#。因此需要编辑。 - ManoDestra
我不明白你的观点。 - P Daddy
C# 编码规范 - ManoDestra
让我们在聊天中继续这个讨论。点击链接 - P Daddy
显示剩余2条评论
2个回答

5

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

  1. This works:

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

    because here we do hint that a:A;

  2. This does not work even with a reference to parameter:

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

    because there is still not enough information to infer the type.

据我理解规范,在“Foo场景”中,推断可能会在第二个(Func)参数上失败,从而使调用不明确。 以下是规范(25.6.4)的内容: 引用: 1.类型推断发生在方法调用(§14.5.5.1)的编译时处理的一部分,并且在调用的重载决定步骤之前进行。当在方法调用中指定了特定的方法组,并且没有将类型参数指定为方法调用的一部分时,将对方法组中的每个泛型方法应用类型推断。如果类型推断成功,则使用推断的类型参数来确定后续重载决定的参数类型。 2.如果重载决定选择一个泛型方法作为要调用的方法,则使用推断的类型参数作为调用的运行时类型参数。如果特定方法的类型推断失败,则该方法不参加重载决策。类型推断本身的失败不会导致编译时错误。但是,当重载决议无法找到任何适用的方法时,它经常会导致编译时错误。 让我们进入相当简单的“Bar场景”。在类型推断之后,我们将获得仅有一个方法,因为只有一个方法是适用的: 1.对于new Container<A>()(未实现IMaker):Bar(Container<A>) 2.对于new A()(不是Container):Bar(A) 如果需要,这里是ECMA-334规范。 注:虽然我不能100%确定我理解正确,但我更愿意认为我掌握了其基本部分。

好的,到目前为止我理解了约束不是方法签名的一部分(这也是为什么你不能仅通过约束重载一个方法的原因),而且 lambda 参数的使用给编译器提供了一个提示来明确选择一个重载。我猜到了这一点。但我仍然不明白为什么在我的第二个代码片段中对 Bar 的调用可以工作。它们完全没有 lambda。 - P Daddy
我猜我们的答案在语言规范的25.6.4和14.4.2之间。但现在是早上7点,所以我得等到明天才能想出任何主意。 - Sergey.quixoticaxis.Ivanov
你能否在你的答案中更详细地阐述一下,也许可以引用一些规范文件中的内容? - P Daddy
我在我的回答中添加了一些信息并进行了修改。抱歉,写的时候在思考,所以有很多编辑。 - Sergey.quixoticaxis.Ivanov
我认为你明白了。混淆的是类型推断步骤,这就是我没有真正考虑到的。 - P Daddy

1

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,那么它看起来是一个不错的重构决策。 - Sergey.quixoticaxis.Ivanov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接