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

时间:2011-05-17 17:43:03

标签: c# generics .net-4.0 extension-methods covariance

代码有点奇怪,所以请耐心等待(请记住,这个场景确实出现在生产代码中)。

说我有这个界面结构:

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)'有一些无效的参数

7 个答案:

答案 0 :(得分:9)

引用C#规范:

  

7.6.5.2扩展方法调用

     

在方法调用(第7.5.5.1节)中   其中一种形式

     

expr。 identifier()

     

expr。标识符(args)

     

expr。标识符&lt; typeargs&gt; ()

     

expr。标识符&lt; typeargs&gt; (args)

     

如果正常处理了   调用找不到适用的   方法,尝试处理   构造作为扩展方法   调用。如果 expr 或任何args   有编译时类型动态,   扩展方法不适用。

     

目标是找到最好的   type-name C ,这样就相应了   静态方法调用可以采取   位:

     

C. identifier(expr)

     

C. identifier(expr,args)

     

C.标识符&lt; typeargs&gt; (expr)

     

C.标识符&lt; typeargs&gt; (expr,args)

     

扩展方法 Ci.Mj 符合条件   如果:

     

· Ci 是非通用的,   非嵌套类

     

· Mj 的名称是标识符

     

· Mj 是可访问的   适用于   参数作为静态方法如图所示   上述

     

·隐含身份,   存在引用或装箱转换   从 expr 到第一个的类型   参数 Mj

由于DoSomethingElse(foo)编译但foo.DoSomethingElse()没有编译,因此在扩展方法的重载解析中似乎是编译器错误:从fooIFoo<IBase>存在隐式引用转换

答案 1 :(得分: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>

答案 2 :(得分:3)

我发现这是一个“错误”的证据。

虽然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();
}

答案 3 :(得分:1)

不知道为什么不编译,但这是否可以接受?

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

答案 4 :(得分:1)

你的代码

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

DoSomethingElseIFoo<IBase>个实例可用,{strong> foo 显然不是,因为它是IFoo<IChild>IChild派生自IBase的事实并未使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>个实例。

答案 5 :(得分: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) 
    { 
    }

所以希望它能更有意义地编译和编译什么,它不是编译器错误。

HTH

答案 6 :(得分: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)

通过使该方法具有通用性,您似乎无法获得任何收益。

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

Generic extension method : Type argument cannot be inferred from the usage

Eric Lippert - Constraints are not part of the signature

C# generics type constraint