Передача лямбда-функций в качестве именованных параметров в C # - PullRequest
35 голосов
/ 08 ноября 2011

Скомпилируйте эту простую программу:

class Program
{
    static void Foo( Action bar )
    {
        bar();
    }

    static void Main( string[] args )
    {
        Foo( () => Console.WriteLine( "42" ) );
    }
}

Ничего странного там нет.Если мы допустим ошибку в теле лямбда-функции:

Foo( () => Console.LineWrite( "42" ) );

, то компилятор вернет сообщение об ошибке:

error CS0117: 'System.Console' does not contain a definition for 'LineWrite'

Пока все хорошо.Теперь давайте используем именованный параметр в вызове Foo:

Foo( bar: () => Console.LineWrite( "42" ) );

На этот раз сообщения компилятора несколько сбивают с толку:

error CS1502: The best overloaded method match for 
              'CA.Program.Foo(System.Action)' has some invalid arguments 
error CS1503: Argument 1: cannot convert from 'lambda expression' to 'System.Action'

Что происходит?Почему он не сообщает об фактической ошибке?

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

Foo( bar: delegate { Console.LineWrite( "42" ); } );

Ответы [ 3 ]

35 голосов
/ 09 ноября 2011

Почему не сообщается о фактической ошибке?

Нет, это проблема;это означает , сообщая о фактической ошибке.

Позвольте мне объяснить немного более сложным примером.Предположим, у вас есть это:

class CustomerCollection
{
    public IEnumerable<R> Select<R>(Func<Customer, R> projection) {...}
}
....
customers.Select( (Customer c)=>c.FristNmae );

ОК, что за ошибка в соответствии со спецификацией C # ?Вы должны внимательно прочитать спецификацию здесь.Давайте разберемся с этим.

  • У нас есть вызов Select как вызов функции с одним аргументом и без аргументов типа.Мы проводим поиск по Select в CustomerCollection, ища вызываемые объекты с именем Select, то есть такие, как поля типа делегата или методы.Поскольку у нас не указаны аргументы типа, мы сопоставляем любой универсальный метод Select.Мы находим один и строим из него группу методов.Группа методов содержит один элемент.

  • Теперь необходимо проанализировать группу методов по разрешению перегрузки, чтобы сначала определить набор кандидатов , а затем из этого определить применимый набор кандидатов , и из этого определяют лучший применимый кандидат , и из этого определяют окончательно утвержденный лучший применимый кандидат .Если какая-либо из этих операций завершается ошибкой, разрешение перегрузки должно завершиться с ошибкой.Какой из них терпит неудачу?

  • Мы начнем с построения набора кандидатов.Чтобы получить кандидата, мы должны выполнить вывод типа метода , чтобы определить значение аргумента типа R. Как работает вывод типа метода?

  • У нас есть лямбдачьи типы параметров все известны - формальным параметром является Customer.Чтобы определить R, мы должны сопоставить тип возврата лямбды с R. Какой тип возврата лямбды?

  • Мы предполагаем, что c - это Customer, и попыткапроанализировать лямбда-тело.При этом происходит поиск FristNmae в контексте Customer, и поиск завершается неудачей.

  • Следовательно, вывод типа лямбда-возврата завершается ошибкой, и к R. не добавляется граница.

  • После того, как все аргументы проанализированы, не существует границ для R. Следовательно, вывод типа метода не может определить тип для R.

  • Поэтому вывод типа метода завершается неудачно.

  • Поэтому в набор кандидатов не добавлен метод.

  • Следовательно, набор кандидатов пуст.

  • Следовательно, не может быть подходящих кандидатов.

  • Следовательно, сообщение об ошибке правильное здесь будет выглядеть примерно так: «При разрешении перегрузки не удалось найтиокончательно утвержденный лучший подходящий кандидат, потому что набор кандидатов был пуст. "

Клиенты будут очень недовольны этим сообщением об ошибке. Мы создали значительное количество эвристикв ошибкуАлгоритм создания отчетов, который пытается вывести более «фундаментальную» ошибку, которую пользователь мог бы предпринять, чтобы исправить ошибку.Мы рассуждаем:

  • Фактическая ошибка состоит в том, что набор кандидатов был пуст.Почему кандидат был пустым?

  • Поскольку в группе методов был только один метод, и вывод типа не удался.

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

  • Поскольку связанный набор R был пуст.

Это тоже паршивая ошибка.Почему границы были установлены пустыми?

  • Поскольку единственным аргументом, по которому мы могли определить R, была лямбда, тип возвращаемого значения которой нельзя было вывести.

Хорошо, мы должны сообщить об ошибке "Не удалось разрешить перегрузку, поскольку вывод лямбда-типа возврата не смог определить тип возврата"? Опять , клиенты будут недовольны этим. Вместо этого мы задаем вопрос «почему лямбда не может определить тип возвращаемого значения?»

  • Поскольку у Клиента нет члена с именем FristNmae.

И , что - это ошибка, о которой мы действительно сообщаем.

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

код, который делает это чрезвычайно сложный ; он имеет дело с более сложными ситуациями, чем та, которую я только что представил, включая случаи, когда существует n различных универсальных методов, и вывод типов не выполняется по m различным причинам, и мы должны выяснить из всех, что является «наилучшей» причиной для предоставления Пользователь. Напомним, что на самом деле существует дюжина различных типов выбора, и разрешение перегрузки для всех из них может не работать по разным причинам или по одной и той же причине.

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

Итак, теперь давайте посмотрим на ваш конкретный случай. Что такое настоящая ошибка?

  • У нас есть группа методов с единственным методом, Foo. Можем ли мы построить набор кандидатов?

  • Да. Есть кандидат. Метод Foo является кандидатом на вызов, потому что он имеет каждый обязательный параметр, предоставленный - bar - и никаких дополнительных параметров.

  • ОК, в наборе кандидатов есть один метод. Есть ли подходящий член набора кандидатов?

  • Нет. Аргумент, соответствующий bar, не может быть преобразован в тип формального параметра, поскольку лямбда-тело содержит ошибку.

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

Так в чем же должна быть ошибка? Опять же, мы не можем просто сказать, что «при разрешении перегрузки не удалось найти окончательно подтвержденного наиболее подходящего кандидата», потому что клиенты нас ненавидят. Мы должны начать копать сообщение об ошибке. Почему не удалось разрешить перегрузку?

  • Поскольку применимый набор кандидатов был пуст.

Почему он был пуст?

  • Потому что каждый кандидат в нем был отклонен.

Был ли лучший кандидат?

  • Да, был только один кандидат.

Почему он был отклонен?

  • Поскольку его аргумент не был преобразован в формальный тип параметра.

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

Почему аргумент не обратим?

  • Поскольку лямбда-тело содержало ошибку.

И тогда мы сообщаем об этой ошибке.

Ошибка эвристики не совершенна ;отнюдь не.По совпадению, я на этой неделе делаю серьезную реархитектуру "простой" эвристики отчетов об ошибках разрешения перегрузки - просто такие вещи, как когда говорят "не было метода, который принял 2 параметра", а когда говорят "метод, который вы хотите, является частными когда сказать «нет параметра, соответствующего этому имени» и т. д .;вполне возможно, что вы вызываете метод с двумя аргументами, нет открытых методов с таким именем с двумя параметрами, есть один, который является закрытым, но у одного из них есть именованный аргумент, который не совпадает.Быстро, какую ошибку мы должны сообщить?Мы должны сделать лучшее предположение, и иногда есть лучшее предположение, которое мы могли бы сделать, но не были достаточно изощренными, чтобы сделать это.

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

Но поскольку полученное вами сообщение об ошибке полностью правильное , это не ошибка в компиляторе;скорее это просто недостаток эвристики сообщения об ошибках в конкретном случае.

6 голосов
/ 08 ноября 2011

РЕДАКТИРОВАТЬ: Ответ Эрика Липперта описывает ( намного лучше) проблему - пожалуйста, смотрите его ответ на «реальную сделку»

ЗАКЛЮЧИТЕЛЬНОЕ РЕДАКТИРОВАНИЕ: Как нелестно, чтобы оставитьпубличная демонстрация их собственного невежества в дикой природе, нет никакого смысла скрывать невежество за нажатием кнопки удаления.Надеюсь, кто-то другой может извлечь пользу из моего безумного ответа :)

Спасибо Эрику Липперту и svick за терпение и любезно исправляющие моё ошибочное понимание!


Причина, по которой вы получаете «неправильное» сообщение об ошибке, заключается в том, что типы и дисперсия выводятся компилятором в сочетании с тем, как компилятор обрабатывает разрешение типов именованных параметров

тип простого примера () => Console.LineWrite( "42" )

Благодаря магии вывода типа и ковариации, это имеет тот же конечный результат, что и

Foo( bar: delegate { Console.LineWrite( "42" ); } );

Первый блок может бытьлибо типа LambdaExpression или delegate;что это зависит от использования и вывода.

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

Давайте посмотрим на IL для дальнейших подсказок: все приведенные примеры компилируют это в LINQPad:

IL_0000:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0005:  brtrue.s    IL_0018
IL_0007:  ldnull      
IL_0008:  ldftn       UserQuery.<Main>b__0
IL_000E:  newobj      System.Action..ctor
IL_0013:  stsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_0018:  ldsfld      UserQuery.CS$<>9__CachedAnonymousMethodDelegate1
IL_001D:  call        UserQuery.Foo

Foo:
IL_0000:  ldarg.0     
**IL_0001:  callvirt    System.Action.Invoke**
IL_0006:  ret         

<Main>b__0:
IL_0000:  ldstr       "42"
IL_0005:  call        System.Console.WriteLine
IL_000A:  ret

Обратите внимание, что ** вокруг вызова System.Action.Invoke: callvirt - это именно то, на что это похоже: вызов виртуального метода.

Когда вы вызываете Foo с именованным аргументом, вы сообщаете компилятору, что вы передаете Action, когда то, что вы действительно передаете, является LambdaExpression.Обычно это компилируется (обратите внимание на CachedAnonymousMethodDelegate1 в IL, вызванном после ctor для Action) в Action, но, поскольку вы явно сообщили компилятору, что передаете действие, он пытается использовать LambdaExpressionпередается как Action вместо обработки его как выражения!

Short: разрешение именованного параметра завершается неудачно из-за ошибки в лямбда-выражении (что само по себе является серьезной ошибкой)

Вот еще одно сообщение:

Action b = () => Console.LineWrite("42");
Foo(bar: b);

выдает ожидаемое сообщение об ошибке.

Возможно, я не на 100% точен в некоторых вещах IL, но я надеюсь, что передалобщая идея

РЕДАКТИРОВАТЬ: dlev в комментариях ОП особо отметил, что порядок разрешения перегрузки также играет свою роль.

4 голосов
/ 09 ноября 2011

Примечание: не совсем ответ, но слишком большой для комментария.

Более интересные результаты, когда вы добавляете тип вывода. Рассмотрим этот код:

public class Test
{
    public static void Blah<T>(Action<T> blah)
    {
    }

    public static void Main()
    {
        Blah(x => { Console.LineWrite(x); });
    }
}

Он не скомпилируется, потому что нет хорошего способа определить, каким должно быть T.
Сообщение об ошибке :

Аргументы типа для метода 'Test.Blah<T>(System.Action<T>)' не могут быть выведены из использования. Попробуйте указать аргументы типа явно.

Имеет смысл. Давайте определим тип x явно и посмотрим, что произойдет:

public static void Main()
{
    Blah((int x) => { Console.LineWrite(x); });
}

Теперь все идет не так, потому что LineWrite не существует.
Сообщение об ошибке :

'System.Console' не содержит определения для 'LineWrite'

Также разумно. Теперь давайте добавим именованные аргументы и посмотрим, что произойдет. Во-первых, без указания типа x:

public static void Main()
{
    Blah(blah: x => { Console.LineWrite(x); });
}

Мы ожидаем получить сообщение об ошибке из-за невозможности определить аргументы типа. И мы делаем. Но это еще не все .
Сообщения об ошибках :

Аргументы типа для метода 'Test.Blah<T>(System.Action<T>)' не могут быть выведены из использования. Попробуйте указать аргументы типа явно.

'System.Console' не содержит определения для 'LineWrite'

Ухоженная. Вывод типа завершается неудачно, и мы точно знаем, почему не удалось преобразовать лямбда Итак, давайте определим тип x и посмотрим, что мы получим:

public static void Main()
{
    Blah(blah: (int x) => { Console.LineWrite(x); });
}

Сообщения об ошибках :

Аргументы типа для метода 'Test.Blah<T>(System.Action<T>)' не могут быть выведены из использования. Попробуйте указать аргументы типа явно.

'System.Console' не содержит определения для 'LineWrite'

Теперь , что неожиданно. Вывод типа все еще не выполняется (я полагаю, что преобразование лямбда -> Action<T> не выполняется, тем самым опровергая предположение компилятора о том, что T равно int) и сообщают о причине сбоя.

TL; DR : Я буду рад, когда Эрик Липперт найдет эвристику для этих более сложных случаев.

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