Передача лямбда-функций в качестве именованных параметров в языке C#


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

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   2011-11-08 20:11:14

3 ответа:

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

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

Позвольте мне объяснить это на несколько более сложном примере. Предположим, у вас есть это:
class CustomerCollection
{
    public IEnumerable<R> Select<R>(Func<Customer, R> projection) {...}
}
....
customers.Select( (Customer c)=>c.FristNmae );

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

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

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

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

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

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

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

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

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

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

  • Таким образом, набор кандидатов пуст.

  • Поэтому подходящих кандидатов быть не может.

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

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

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

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

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

  • потому что связанное множество R был пуст.

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

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

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

  • потому что у клиента нет члена по имени FristNmae.

И что является ошибкой, о которой мы фактически сообщаем.

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

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

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

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

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

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

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

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

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

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

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

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

  • потому что каждый кандидат в нем был отклоненный.
Был ли лучший из возможных кандидатов?
  • Да, там был только один кандидат.

Почему он был отвергнут?

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

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

Почему этот аргумент не был конвертируемым?

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

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

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

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

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

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

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

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

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

Тип основного примера () => 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 это именно то, что кажется: a вызов виртуального метода.

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

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

Вот другой рассказ:

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

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

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

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

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

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

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 не существует.
Сообщение Об Ошибке :
Система

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

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

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

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

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

Система

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

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

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

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

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

Система

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

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

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