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

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

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

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" ); } );
4b9b3361

Ответ 1

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

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

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

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

ОК, какова ошибка в соответствии со спецификацией С#? Здесь вы должны внимательно прочитать спецификацию. Пусть это будет работать.

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

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

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

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

  • Предположим, что c является Клиентом и пытается проанализировать лямбда-тело. Это делает поиск FristNmae в контексте Клиента, и поиск не работает.

  • Следовательно, вывод типа lambda return не выполняется, и в R не добавляется никаких ограничений.

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

  • Поэтому вывод метода невозможен.

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

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

  • Поэтому не могут быть применимые кандидаты.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Почему он пуст?

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

Здесь другие говорят:

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

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

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

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

Ответ 3

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

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

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: Я буду рад, когда Эрик Липперт приблизится к эвристике для этих более сложных случаев.