澄清C#扩展方法优先级

3

我正在尝试通过创建一个同名的扩展方法来修改现有扩展方法的行为。只要方法签名不同,我知道这是可能的。我也知道调用哪个方法是基于签名的“接近程度”。

如果我有:

public void DoStuff(this List<string> list) {
    //...
}

并且

public void DoStuff<T>(this List<T> list) {
    //...
}

我知道当调用 new List<string>().DoStuff()时会调用 DoStuff,而当调用new List<int>().DoStuff()时会调用DoStuff<T>

但是我不明白为什么在使用接口时没有看到相同的行为。

public interface IBar { }

public class Foo : IBar
{

}

public static class FooHelper
{
    public static void DoStuff(this IBar stuff)
    {
        Console.WriteLine("public static void DoStuff (this IBar stuff)");
    }

    public static void DoStuff<T>(this T stuff)
    {
        Console.WriteLine("public static void DoStuff<T>(this T stuff)");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var foo = new Foo();

        foo.DoStuff();
    }
}

在上面的例子中,我期望public static void DoStuff (this IBar stuff)被输出,但是实际却得到了public static void DoStuff<T>(this T stuff)
我假定接口类型比泛型类型更具体,因此对于我的DoStuff调用而言是更好的选择。
为什么泛型类型签名会优先于接口类型签名?

5
我不确定规范确切地说了什么,但我认为这是因为通用的可以精确匹配(T = Foo 是精确匹配),而 IBar 需要一些推断。如果您将 foo 实际声明为 IBar(即 IBar foo = new Foo();),那么它将按您所需的方式执行。我猜想您只需要阅读C#规范中的重载规则,就能发现为什么会选择一个而不是另一个。如果您想知道为什么做出这个设计选择,那就是另外一回事了……最后,我认为Eric Lippert 写了一些关于重载决策的文章,可能会涉及到这个问题。 - Chris
进一步说明,这与它们是扩展方法无关。如果您将它们作为普通方法调用,您将获得相同的行为。 - Chris
谢谢您的评论。我的想法也是沿着这个方向的。我还发现了Eric Lippert的文章,它们非常有帮助。 - Siggi
所有Eric的文章都非常有帮助。不幸的是,我认为他现在发布的文章比在微软时少了一些,但是花时间翻阅他的所有文章并阅读任何看起来有趣的内容都是值得的。 - Chris
1个回答

3

克里斯的评论是正确的,下面是来自C#语言规范的更多细节:

调用扩展方法foo.DoStuff()等同于调用静态方法FooHelper.DoStuff(foo)

编译器必须确定要调用的确切方法(§7.6.5.1 方法调用)。首先,它将创建一组候选方法。由于

如果F是非泛型的,则在以下情况下,F是候选项:

• M没有类型参数列表,并且

• F相对于A适用(§7.5.3.1)。

如果F是泛型的并且M没有类型参数列表,那么当:

• 类型推断(§7.5.2)成功时,为调用推断出一系列类型参数,并且

• 一旦替换所推断的类型参数以用于对应方法类型参数,所有构建类型在F参数列表中满足其约束条件(§4.4.4),并且F参数列表相对于A适用(§7.5.3.1)。

候选集将包括两个DoStuff方法。

泛型FooHelper.DoStuff方法将已经有类型参数被推断(§7.5.2):

类型推断出现在方法调用(§7.6.5.1)的绑定时间处理中,并且发生在调用的重载解析步骤之前。

如果类型推断成功,则推断出的类型参数用于确定随后重载解析的参数类型。

因此,候选集为

public static void DoStuff(this IBar stuff)
public static void DoStuff<Foo>(this Foo stuff)

编译器将从这个集合中选择最佳的方法进行调用。规则在 §7.5.3.2 中描述。在这种情况下,将调用通用方法,因为恒等转换(从 Foo 到 Foo)比从 Foo 到 IBar 的转换更好。


谢谢。我的理解是泛型类型的处理方法,而我的思维过程出了问题。这个解释对我很有帮助。 - Siggi
好东西。我试图在规范中找到相关内容,但我认为我使用的规范已经过时了(谷歌搜索结果显示https://msdn.microsoft.com/en-us/library/aa645597(v=vs.71).aspx,该页面顶部显示VS 2003,因此我怀疑这是早期版本,可能是泛型之前的版本)。不过还是感谢您帮我澄清了我的想法。 :) - Chris
1
@Chris 不用谢。C#语言规范已经与Visual Studio安装在一起,位于C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC#\Specifications\1033。 - Jakub Lortz

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