Почему Func <T>неоднозначен с Func? - PullRequest
20 голосов
/ 01 января 2011

Это заставило меня обескуражиться, поэтому я решил спросить здесь в надежде, что гуру C # сможет мне это объяснить.

Почему этот код генерирует ошибку?

class Program
{
    static void Main(string[] args)
    {
        Foo(X); // the error is on this line
    }

    static String X() { return "Test"; }

    static void Foo(Func<IEnumerable<String>> x) { }
    static void Foo(Func<String> x) { }
}

Ошибка в вопросе:

Error
    1
    The call is ambiguous between the following methods or properties:
'ConsoleApplication1.Program.Foo(System.Func<System.Collections.Generic.IEnumerable<string>>)' and 'ConsoleApplication1.Program.Foo(System.Func<string>)'
    C:\Users\mabster\AppData\Local\Temporary Projects\ConsoleApplication1\Program.cs
    12
    13
    ConsoleApplication1

Неважно, какой тип я использую - если вы замените объявления "String" на "int" в этом коде, вы получите такую ​​же ошибку. Как будто компилятор не может определить разницу между Func<T> и Func<IEnumerable<T>>.

Может кто-нибудь пролить свет на это?

Ответы [ 2 ]

26 голосов
/ 01 января 2011

ОК, вот сделка.

Краткая версия:

  • Ошибка неоднозначности, как ни странно, правильная.
  • Компилятор C # 4 также выдает ложную ошибку после правильной ошибки неоднозначности. Это похоже на ошибку в компиляторе.

Длинная версия:

У нас проблема с разрешением перегрузки. Разрешение перегрузки очень точно указано.

Шаг первый: определить набор кандидатов. Это легко. Кандидатами являются Foo(Func<IEnumerable<String>>) и Foo(Func<String>).

Шаг второй: определить, какие члены набора кандидатов являются применимыми . Применимый член имеет каждый аргумент, конвертируемый в каждый тип параметра.

Применимо ли Foo(Func<IEnumerable<String>>)? Ну, X конвертируется в Func<IEnumerable<String>?

Мы обращаемся к разделу 6.6 спецификации. Эта часть спецификации - то, что мы, языковые дизайнеры, называем «действительно странным». В основном, это говорит, что преобразование может существовать, но использование этого преобразования является ошибкой . (Есть веские причины, по которым мы имеем эту странную ситуацию, в основном связанную с тем, чтобы избежать будущих переломных изменений и избежать ситуаций "курица и яйцо", но в вашем случае в результате мы получаем несколько неудачное поведение.)

По сути, правило здесь таково, что преобразование из X в тип делегата без параметров существует , если разрешение перегрузки при вызове вида X() будет успешным. Очевидно, что такой вызов будет успешным, и, следовательно, преобразование существует. На самом деле с использованием это преобразование является ошибкой, поскольку возвращаемые типы не совпадают, но разрешение перегрузки всегда игнорирует возвращаемые типы .

Итак, существует преобразование из X в Func<IEnumerable<String>, и поэтому эта перегрузка является подходящим кандидатом.

Очевидно, что по той же причине другая перегрузка также является подходящим кандидатом.

Шаг третий: Теперь у нас есть два подходящих кандидата. Какой из них "лучше"?

Тот, который «лучше», - это тот, у кого более конкретный тип . Если у вас есть два подходящих кандидата, M(Animal) и M(Giraffe), мы выбираем версию Giraffe, потому что Giraffe более специфичен, чем Animal. Мы знаем, что Жираф более конкретен, потому что каждый Жираф - Животное, но не каждое Животное - Жираф.

Но в вашем случае ни один тип не является более конкретным, чем другой. Нет преобразования между двумя типами Func.

Следовательно, ни то, ни другое лучше, поэтому разрешение перегрузки сообщает об ошибке.

Компилятор C # 4 затем имеет то, что кажется ошибкой, когда его режим восстановления после ошибки выбирает одного из кандидатов в любом случае и сообщает другую ошибку. Мне не понятно, почему это происходит. В основном это говорит о том, что для восстановления после ошибок выбирается перегрузка IEnumerable, а затем отмечается, что преобразование группы методов приводит к несостоятельному результату; а именно, эта строка не совместима с IEnumerable<String>.

Вся ситуация довольно неудачная; могло бы быть лучше сказать, что нет преобразования метода группы в делегат, если возвращаемые типы не совпадают. (Или, что преобразование, которое приводит к ошибке, всегда хуже, чем преобразование, которое этого не делает.) Однако мы застряли с этим сейчас.

Интересный факт: правила конвертации лямбд do учитывают типы возврата. Если вы скажете Foo(()=>X()), тогда мы поступим правильно. Тот факт, что лямбды и группы методов имеют разные правила конвертируемости, весьма прискорбен.

Итак, подытоживая, компилятор на самом деле является правильной реализацией спецификации в этом случае, и этот конкретный сценарий является непреднамеренным следствием некоторых, возможно, неудачных выборов спецификации.

7 голосов
/ 01 января 2011

Ваш код требует, чтобы «волшебство» происходило дважды, один раз для преобразования из названной группы методов в делегат и один раз для выполнения разрешения перегрузки.

Несмотря на то, что у вас есть только один метод с именем X, правила компилятора построены для случая, когда их несколько.

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

Ваш конкретный случай выглядит достаточно просто, но общий случай очень и очень сложен, поэтому язык этого не позволяет.

Если вы сделаете часть работы вручную, вы решите проблему. например

Func<string> d = X;
Foo(d);

должен скомпилироваться просто отлично.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...