怪异的扩展方法过载解决方案

本文关键字:解决方案 方法 扩展 | 更新日期: 2023-09-27 17:58:32

我在让编译器解决扩展方法的正确重载时遇到了问题。对我来说,最好的解释方式是用一点代码。这里有一个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>)"answers"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();
    }
}

这编译并产生预期结果:

条形图(A:IMarker)
Foo(A:IMarker)
Foo(A:IMarker)
Foo(A:IMarker)
条形图(容器<A>)
Foo(容器<A>)
Foo(Container<A>)

它似乎只有在我不引用lambda表达式中的lambda参数,然后只引用Container<T>类时才有问题。当调用Bar时,没有lambda,并且工作正常。当使用基于lambda参数的返回值调用Foo时,它可以正常工作。即使lambda的返回值与示例中未编译的返回值相同,但lambda参数由伪赋值引用,它也能工作。

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

怪异的扩展方法过载解决方案

哦,我是重读自己的答案后得到的!好问题=)重载不起作用,因为它在解析重载时没有考虑约束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<A>)用于new Container<A>()(不实现IMaker)
  2. new A()Bar(A)(不是容器)

这是ECMA-334的规格,以防万一。附言:我不能百分之百肯定我做对了,但我更愿意认为我抓住了关键部分。

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){}
}

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