Почему этот универсальный метод расширения не компилируется? - PullRequest
37 голосов
/ 17 мая 2011

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

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

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 ]

9 голосов
/ 17 мая 2011

Цитирование спецификации C #:

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

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

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

expr. идентификатор (аргументы)

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

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

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

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

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

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

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

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

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

· Ci не является универсальным, не вложенный класс

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

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

· неявная идентичность, существует ссылка или преобразование в бокс от expr до типа первого параметр МДж .

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

5 голосов
/ 17 мая 2011

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

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>
3 голосов
/ 11 июня 2011

Я нашел доказательства того, что это «ошибка».

Хотя не обязательно, чтобы язык 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();
}
1 голос
/ 27 мая 2011

Ваш кусок кода

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

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

1 голос
/ 17 мая 2011

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

public static void DoSomethingElse<T>(this IFoo<T> foo) where T : IBase
{
}
0 голосов
/ 09 июня 2011

Проблема в том, что дисперсия работает только со ссылочными типами или преобразованиями идентификаторов из спецификации (раздел 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)

Похоже, вы ничего не получите, сделав метод универсальным.

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

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

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

C # типовое ограничение типа

0 голосов
/ 09 июня 2011

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

...