为什么这个通用扩展方法无法编译?

37

这段代码有点奇怪,请耐心阅读(请记住,这种情况确实在生产代码中出现过)。

假设我有以下接口结构:

public interface IBase {  }
public interface IChild : IBase {  }

public interface IFoo<out T> where T : IBase {  }

通过这个扩展方法类构建了与接口相关的内容:

public static class FooExt
{
    public static void DoSomething<TFoo>(this TFoo foo)
        where TFoo : IFoo<IChild>
    {
        IFoo<IChild> bar = foo;

        //foo.DoSomethingElse();    // Doesn't compile -- why not?
        bar.DoSomethingElse();      // OK
        DoSomethingElse(foo);       // Also OK!
    }

    public static void DoSomethingElse(this IFoo<IBase> foo)
    {
    }
}

为什么在DoSomething中注释掉的那一行不能编译?编译器允许我将foo赋值给与泛型约束相同类型的bar,并在其上调用扩展方法,这也没有问题。也可以不使用扩展方法语法来调用扩展方法。
有人能否确认这是一个错误还是预期行为?
谢谢!
仅供参考,这是编译错误(类型已缩写):

'TFoo'不包含'DoSomethingElse'的定义,最佳的扩展方法重载'DoSomethingElse(IFoo)'具有一些无效参数


1
@Jackson:.NET 4(否则我认为 out 将无法工作) - Cameron
@Jackson Pope,使用IFoo接口的out协变泛型定义,这不是显而易见的吗? - Darin Dimitrov
我认为这是因为您将泛型参数声明为out T而不是in T。IChild无法转换为IFoo<IBase> - Tejs
@Anna Lear,啊。没注意到 :) - riwalk
@FacticiusVir:好问题!正如你所指出的,我这里实际上不需要TFoo类型。然而,在我的一些真实代码中,我对IFoo有额外的(接口)约束。 - Cameron
显示剩余6条评论
7个回答

9
引用C#规范:
7.6.5.2 扩展方法调用
在形式为以下之一的方法调用(§7.5.5.1)中:
expr.identifier() expr.identifier(args) expr.identifier() expr.identifier(args)
如果正常处理调用时未找到适用的方法,则尝试将构造视为扩展方法调用。如果expr或任何args具有编译时类型dynamic,则不适用扩展方法。
目标是找到最佳的类型名称C,以便进行相应的静态方法调用:
C.identifier(expr) C.identifier(expr, args) C.identifier(expr) C.identifier(expr, args)
如果满足以下条件,则扩展方法Ci.Mj是合格的:
· Ci是非泛型、非嵌套类 · Mj的名称是identifier · 当作为上述静态方法应用于参数时,Mj是可访问和适用的 · 从expr到Mj的第一个参数的类型存在隐式身份、引用或装箱转换。
由于DoSomethingElse(foo)可以编译,但foo.DoSomethingElse()不能,因此似乎是扩展方法重载解析的编译器错误:从foo到IFoo存在隐式引用转换。

有趣。DoSomethingElse(this IFoo<IBase> foo)不会违反Ci的非泛型规定吗?问题是,我可以在IFoo<IChild>上调用它作为扩展方法,但不能在一个泛型类型上调用,该类型恰好是IFoo<IChild>(通过泛型约束指定)。 - Cameron
1
Ci 指的是包含扩展方法 FooExt 的类,在你的示例代码中:这个从句基本上表示你不能将扩展方法放在泛型或嵌套类中。参数的唯一条件是最后一句话,通过了测试。 - Julien Lebosquain
好的,谢谢,现在更清楚了。引用规范值得一赞,虽然我还不知道为什么它不起作用;-) 我不太相信这是编译器的错误 - 这种情况实在太少见了。另一方面,我看到的行为似乎与规范不符... - Cameron

5

你能在IFoo中定义DoSomethingElse吗?

public interface IFoo<out T> where T : IBase
{
    void DoSomethingElse();
}

更新

也许您可以更改签名

public static void DoSomethingElse(this IFoo<IBase> foo)
=>
public static void DoSomethingElse<TFoo>(this TFoo foo) 
    where TFoo : IFoo<IChild>

很遗憾,不是所有的IFoo都会有这个方法(在我的实际代码中,我对扩展方法中的TFoo类型参数有额外的泛型约束)。 - Cameron
在更新中加上+1。在另一个扩展方法中添加约束“where TFoo:IFoo<IChild>”,我认为你就可以做好了。 - JSBձոգչ
我喜欢你在更新中提出的建议,但在我的情况下,使用已经具有(非推断)类型参数的扩展方法相当痛苦,因为我的实际IFoo也是模板化的。 我最终会得到像thing.Method<FirstType, SecondType, IThirdType<FirstType, FourthType>>();这样的调用,而不是thing.Method<FirstType, SecondType>(); - Cameron
如果不深入研究 C# 规范,我无法解决这个问题,但至少对于这个例子,我会简单地更改 DoSomething 的签名,如下所示:public static void DoSomething(this IFoo<IChild> foo) { ... }在这里,泛型参数实际上有用吗? - Mike Strobel
@Mike:感谢您的建议,但我实际上正在使用通用的输出参数——我确实需要能够从IFoo<IBase>中调用DoSomethingElse - Cameron
@Mike:我第一次完全误解了你的评论,很抱歉。我以为你在谈论 IFoo<out T> 类型参数,但当然你指的是方法上的约束。实际上,我正在使用泛型参数,这样我就可以除了 IFoo<IChild> 之外还可以指定其他对 TFoo 的约束。 - Cameron

3
我发现这是一个“bug”的证据。
虽然CLR语言不一定支持MSIL中的所有功能,但事实上,您正在尝试做的在MSIL中是有效的。
如果您打算将代码转储到IL并使DoSomething方法看起来像这样:
.method public hidebysig static void  DoSomething<(class TestLib.IFoo`1<class TestLib.IChild>) T>(!!T foo) cil managed
{
  .custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       52 (0x34)
  .maxstack  1
  .locals init ([0] class TestLib.IFoo`1<class TestLib.IChild> bar)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  box        !!T
  IL_0007:  call       void TestLib.Ext::DoSomethingElse(class TestLib.IFoo`1<class  TestLib.IBase>)
  IL_000c:  nop
  IL_000d:  ret
} // end of method Ext::DoSomething

如果你尝试编译这段代码,你会发现它可以成功编译。在C#中,反射器将如何解析它呢?

public static void DoSomething<T>(this T foo) where T: IFoo<IChild>
{
    foo.DoSomethingElse();
}

太棒了,谢谢!至少现在我们知道它被CLR支持,即使不是编译器 :-) - Cameron

1

不知道为什么它无法编译,但这是一个可接受的替代方案吗?

public static void DoSomethingElse<T>(this IFoo<T> foo) where T : IBase
{
}

谢谢你的解决方法,但与oleksii的建议类似,我不想添加更多的类型参数(因为我已经有一些了,而且类型无法推断,这非常令人烦恼,因为我正在使用长类型名称)。 - Cameron

1

你的代码

public static void DoSomethingElse(this IFoo<IBase> foo)
{
}

使得DoSomethingElse只能在IFoo<IBase>实例上有效,而foo显然不是,因为它是一个IFoo<IChild>。虽然IChildIBase派生出来,但IFoo<IChild>并不继承自IFoo<IBase>。所以遗憾的是,foo不能被视为一种IFoo<IBase>,因此无法在其上调用DoSomethingElse

但如果稍微修改你的扩展方法,就可以很容易地避免这个问题:

public static void DoSomethingElse<T>(this IFoo<T> foo) where T : IBase
{
}

现在它已经编译并且一切正常。

最有趣的部分是,当使用静态方法语法调用DoSomethingElse(foo);时,它可以编译通过,但是使用扩展方法语法时则不行。显然,在常规的静态方法样式调用中,泛型协变很好地工作:参数foo被定义为IFoo<IBase>类型,但可以赋值为IFoo<IChild>类型,因此调用是可以的。但是作为扩展方法,由于它的声明方式,DoSomethingElse仅在形式上被定义为IFoo<IBase>实例上可用,即使它符合IFoo<IChild>,所以这种语法在IFoo<IChild>实例上无法工作。


谢谢您的回答。这很有道理,只是它没有解释为什么扩展方法可以在 bar 上调用(它仍然是一个 IFoo<IChild>)。似乎从 IFoo<IChild>IFoo<IBase> 的隐式转换 确实 适用于扩展方法语法,但不适用于指定类型的泛型约束。 - Cameron
我错过了这一点。你是完全正确的。拥有DoSomethingElse可用与否的区别不是我所认为的调用风格问题,而是实例是否显式地使用符合类型或通用类型:编译器并不聪明到意识到通用类型实际上是IFoo<IChild>类型的一种,可以应用DoSomethingElse - Ssithra

0
它无法编译,因为它抱怨“'TFoo'不包含'DoSomethingElse'的定义”。
你的DoSomething并未定义到TFoo上,而是定义在IFoo<IBase>IFoo<IChild>上。
以下是我所做的一些更改,请查看哪些变量可以编译。
public interface IBase { }
public interface IChild : IBase { }

public interface IFoo<out T> where T : IBase { }

public static class FooExt
{
    public static void DoSomething<TFoo>(this TFoo foo)     where TFoo : IFoo<IChild>
    {
        IFoo<IChild> bar = foo;
        //Added by Ashwani 
        ((IFoo<IChild>)foo).DoSomethingElse();//Will Complie
        foo.DoSomethingElseTotally(); //Will Complie

        //foo.DoSomethingElse();    // Doesn't compile -- why not?
        bar.DoSomethingElse();      // OK
        DoSomethingElse(foo);       // Also OK!

    }

    public static void DoSomethingElse(this IFoo<IBase> foo)
    {
    }

    //Another method with is actually defined for <T>
    public static void DoSomethingElseTotally<T>(this T foo) 
    { 
    }

希望现在更加明白什么会被编译和什么不会,而且这不是编译器的错误。

希望对你有所帮助。


我不希望TFoo有一个DoSomethingElse()方法,因为IFoo没有这个方法。完整的错误信息是:'TFoo'不包含'DoSomethingElse'的定义最佳扩展方法重载具有一些无效参数。我期望编译器使用该扩展方法——当约束(和DoSomethingElse的参数)更改为常规的、非协变接口(例如ICloneable)时,编译器可以很好地解决调用。 - Cameron
目前编译器还不能实现这个功能。像你说的那样,它可以用于非协变接口,但我不认为编译器在这种情况下能够做到这一点。 - Ash
好的。因此我的问题是询问这是否是一个错误 :-) - Cameron

0
问题在于方差只适用于引用类型或标识转换,根据规范(第13.1.3.2节):
A type T<A1, …, An> is variance-convertible to a type T<B1, …, Bn> if T is either an interface or a delegate type declared with the variant type parameters T<X1, …, Xn>, and for each variant type parameter Xi one of the following holds:
•         Xi is covariant and an implicit reference or identity conversion exists from Ai to Bi
•         Xi is contravariant and an implicit reference or identity conversion exists from Bi to Ai
•         Xi is invariant and an identity conversion exists from Ai to Bi

编译器无法验证TFoo是否是实现IFoo<IChild>的结构体,因此它找不到所需的扩展方法。将class约束添加到DoSomething中也无法解决问题,因为值类型仍然从object继承,因此满足约束条件。IFoo<IChild> bar = foo;DoSomethingElse(foo);都可以工作,因为每个都有从fooIFoo<IChild>的隐式转换,这是一个引用类型。

我会像Mike Strobel在上面的评论中问同样的问题:为什么不将DoSomething签名更改为

public static void DoSomething<TFoo>(this TFoo foo)
        where TFoo : IFoo<IChild>

public static void DoSomething<TFoo>(this IFoo<IChild> foo)

通过使方法通用化,您似乎没有获得任何好处。

我在这个主题上阅读的一些帖子:

通用扩展方法:无法从使用中推断类型参数

Eric Lippert - 约束不是签名的一部分

C# 通用类型约束


你的答案取决于规范摘录,如果您仔细阅读一些,它实际上并没有涉及引用类型。它谈论了隐式引用转换(参见规范§6.1.6,其中IChild->IBase是满足的)。另外,class约束限制只能使用引用类型(而不是结构)。此外,IFoo<IChild>不是引用类型(它是一个结构体可以实现的接口)。 - Cameron
还有,感谢指出Mike的评论; 我第一次误解了它。FacticiusVir在原始帖子的评论中确实提出了同样的问题。我在那里回答说,我正在使用泛型参数,以便除了“IFoo<IChild>”之外,我还可以指定有关TFoo的其他限制。 - Cameron
我在我的帖子中应该包含更多的规范说明。在第4.2节中,引用类型被列为类类型、接口类型、数组类型或委托类型,因此IFoo<IChild>实际上是引用类型。在你发的“约束”链接里也有提到。 - WarrenG
@Warren:有趣。这值得提出一个新问题——接口是引用类型,但结构体(值类型)可以实现接口。这是否意味着通过它们的接口公开的结构体是引用类型?无论如何,这都与我的原始问题无关——与引用/值类型无关,因为当IFoo不带类型参数时没有编译错误(尝试使用例如where TFoo:ICloneable代替——它将编译良好)。 - Cameron
@Cameron:在一个演示项目中更多地尝试后,我发现你关于这不是关于引用/值类型的说法是正确的。看起来在DoSomething中推断的类型与DoSomethingElse中指定的类型之间存在一些问题。有趣的是,如果两个扩展方法都是泛型的,即使使用像这样通过基础接口约束DoSomethingElse<TFoo>(this TFoo foo) where TFoo : IFoo<IBase>,代码也可以编译而不会出现问题。 - WarrenG
显示剩余2条评论

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