C#方法重载和泛型接口

8

我在我们的项目中遇到了一个问题,感到很困惑。我尝试简化它以重现这种效果:

interface IBar { }

class Bar : IBar {}

interface IFoo<T> where T : IBar { }

class Foo<T> : IFoo<T> where T : IBar { }


class Class1
{
    public void DoTheFoo<T>(T bar) where T : IBar
    {}

    public void DoTheFoo<T>(IFoo<T> foo) where T : IBar
    {}


    public void Test()
    {
        var bar = new Bar();
        var foo = new Foo<Bar>();

        DoTheFoo(bar); // works

        DoTheFoo<Bar>(foo); // works
        DoTheFoo((IFoo<Bar>)foo); // works
        DoTheFoo(foo); // complains
    }
}

对我来说,这看起来很好,但编译器在最后一个调用上抱怨,因为它尝试执行 DoTheFoo<T>(T bar),而不是 DoTheFoo<T>(IFoo<T> foo) 并抱怨参数类型不匹配。
  • 当我删除方法 DoTheFoo<T>(T bar) 时,最后一个调用可以正常工作!
  • 当我将其更改为 DoTheFoo<T>(Foo<T> foo) 时,它可以工作,但我不能使用它

在我们当前的代码中,解决这个问题并不太难。但是,a)这很奇怪,b)我们不能有这两个重载的方法。

是否有一个通用规则可以解释这种行为?是否可能使其工作(除了给方法命名不同的名称之外)?


我不认为这是一个重复的问题。问题在于通用约束中的方法不同。我的方法在声明的参数类型上也有所不同。 - Stefan Steinegger
不,那是重复的。请仔细阅读我的答案。它解释了为什么在你的情况下重载解析会选择这个方法。 - Eric Lippert
1个回答

7
这只是类型推断与重载解析相结合时没有完全发挥作用的问题。通过明确指定类型参数很容易解决,不需要进行强制转换:
DoTheFoo<Bar>(foo);

通常我会对参数类型差异较大的过载感到紧张。如果你只是给方法起不同的名称,代码往往会变得更简单。此外,你的读者不需要同时进行类型推断和过载解析...
编辑:我认为问题在于排序的工作方式如下:
- 找到两种方法 - 对两种方法应用类型推断而不验证约束-因此对于第一种方法,我们得到T = Foo ,对于第二种方法,我们得到T = Bar。此时两种方法都适用。 - 执行过载解析,决定第一种方法是最具体的方法。 - 只有在执行了过载解析之后才检查对T的约束-并且由于从Bar到IFoo没有引用转换,因此失败。

我写了一篇关于为什么语言被设计成这样的博客文章Eric Lippert blog post about why the language is designed this way,还有一篇我写的关于它的博客文章blog post I wrote about it,以及an article I wrote about overloading in general一篇我写的关于重载的文章。它们中的每一个可能会或可能不会有所帮助 :)

编辑:暂时不考虑类型推断,第一个方法更具体的原因在于,在一种情况下,我们从Foo<Bar>转换为Foo<Bar>,而在另一种情况下,我们从Foo<Bar>转换为IFoo<Bar>。根据C# 5规范7.5.3.3节的规定:

给定一个从表达式 E 转换到类型 T1 的隐式转换 C1,和一个从表达式 E 转换到类型 T2 的隐式转换 C2,如果以下至少一条成立,则 C1 是比 C2 更好的转换: - E 有一个类型 S,并且存在从 S 到 T1 的标识转换但不存在从 S 到 T2 的标识转换 - ...
标识转换是从一个类型到其本身的转换,对于第一个重载而言,这是成立的,但对于第二个重载则不成立。因此第一个转换更好。

1
我不太确定这是因为类型推断的原因。考虑到它没有抱怨参数无法确定类型。当第一个DoTheFoo方法被移除时,它也能正常工作... - Stefan Steinegger
1
@StefanSteinegger:类型推断在重载面前失败了。整个问题非常棘手——试着在规范中跟踪所有内容,但显然类型推断是问题的一部分,否则当您明确指定类型参数时它就不起作用了,对吧。 - Jon Skeet
我认为这可能是编译器查找正确重载的顺序问题。它似乎更喜欢具有通用参数的函数,而不是更具体类型的函数。(对我来说,IFoo<T>T更具体)。这很奇怪。 - Stefan Steinegger
@StefanSteinegger:这正是我的观点:类型推断和重载解析有时会以令人困惑的方式相互作用。如果只是为了对其迷宫般的复杂性有所感觉,真的值得尝试通过规范来理解发生了什么。同样,在编译器和任何阅读代码的人方面,通过给方法命名不同的名称来消除重载将使这个过程显着简化。我怀疑正在发生的是类型推断,然后是重载解析,然后是约束验证。 - Jon Skeet
2
@StefanSteinegger:无论IFoo<T>T更具体还是不太具体都不是相关的问题。方法类型推断成功,然后在它成功之后的问题是:“哪种转换更好:通过引用转换将foo转换为IFoo<Bar>还是通过标识转换将foo转换为Foo<Bar>?显然标识转换胜出,因此重载决议选择Foo<Bar>方法作为最佳方法。只有在这之后才会检查约束条件,并确定Foo<Bar>不符合IBar约束。 - Eric Lippert
显示剩余2条评论

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