Подтвердить что ты не робот

Почему этот обобщенный метод расширения не компилируется?

Код немного странный, так что несите меня (имейте в виду, что этот сценарий появился в производственном коде).

Скажем, у меня есть эта структура интерфейса:

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 to bar, который имеет тот же тип, что и общее ограничение, и вместо этого вызывает метод расширения. Также не нужно вызывать метод расширения без синтаксиса метода расширения.

Может ли кто-нибудь подтвердить, является ли это ошибкой или ожидаемым поведением?

Спасибо!

Просто для справки, здесь ошибка компиляции (типы сокращены для удобочитаемости):

"TFoo" не содержит определения для "DoSomethingElse", а лучший метод перегрузки над расширением "DoSomethingElse (IFoo)" содержит некоторые недопустимые аргументы

4b9b3361

Ответ 1

Указание спецификации С#:

7.6.5.2 Вызов метода расширения

В вызове метода (п. 7.5.5.1) одна из форм

выражение. Идентификатор()

выражение. идентификатор (args)

выражение. идентификатор <typeargs> ()

выражение. идентификатор <typeargs> (args)

если нормальная обработка вызов не применим методов, делается попытка обрабатывать конструкция как метод расширения призывание. Если expr или любой из аргументов имеет динамику типа компиляции, методы расширения не будут применяться.

Цель состоит в том, чтобы найти лучшее имя типа C, так что соответствующий вызов статического метода может Место:

C. идентификатор (expr)

C. идентификатор (expr, args)

C. идентификатор <typeargs> (expr)

C. идентификатор <typeargs> (expr, args)

Способ расширения Ci.Mj имеет право если:

· Ci является не общим, не-вложенный класс

. Имя Mj является идентификатором

· Mj доступно и применимых при применении к аргументы как статический метод, как показано выше

. Неявное тождество, ссылка или конверсия бокса от expr до типа первого параметр Mj.

Так как DoSomethingElse(foo) компилируется, но foo.DoSomethingElse() не работает, это похоже на ошибку компилятора в разрешении перегрузки для методов расширения: существует неявное ссылочное преобразование от foo до IFoo<IBase>.

Ответ 2

Можете ли вы определить DoSomethingElse в IFoo?

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

UPDATE

Возможно, вы сможете изменить подпись

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

Ответ 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

вы обнаружите, что это компилируется. И что рефлектор разрешает это, как в С#?

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

Ответ 4

Не знаю, почему он не компилируется, но является ли это приемлемой альтернативой?

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

Ответ 5

Ваша часть кода

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

делает DoSomethingElse доступным только для экземпляров IFoo<IBase>, что foo, очевидно, нет, так как это a 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> экземпляры.

Ответ 6

Он не компилируется по той причине, что он жалуется на "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) 
    { 
    }

Итак, надеюсь, что это немного лучше, что компилирует, а что нет, и это не ошибка компилятора.

НТН

Ответ 7

Проблема заключается в том, что дисперсия работает только по ссылочным типам или преобразованиям идентификаторов из спецификации (раздел 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); работают, потому что каждый из них имеет неявный перевод из foo в IFoo<IChild>, который является ссылочным типом.

Я задал бы тот же вопрос, который задал Майк Штробель в комментариях выше: почему бы не изменить свою подпись DoSomething от

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

к

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

Кажется, вы ничего не получаете, создав общий метод.

Несколько сообщений, которые я прочитал по теме:

Общий метод расширения: аргумент типа не может быть выведен из использования

Эрик Липперт - Ограничения не входят в подпись

Ограничение типа генерируемых типов С#