Как компилятор знает, каким типом данных должно быть лямбда-выражение - PullRequest
1 голос
/ 02 октября 2019

Поэтому, когда вы используете EF Core и используете большинство расширений Linq, вы на самом деле используете System.Linq.Expressions вместо обычного Func.

Итак, допустим, вы используете FirstOrDefault на DbSet.

DbContext.Foos.FirstOrDefault(x=> x.Bar == true);

Когда вы ctrl + lmb на FirstOrDefault, это покажет вам следующую перегрузку:

public static TSource FirstOrDefault<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)

Но есть также перегрузка для Func:

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)

Когда вы хотите сохранить выражение в переменной, вы можете сделать что-то вроде следующего:

Func<Entity, bool> = x => x.Bar == true;

и

Expression<Func<Entity, bool>> = x => x.Bar == true;

Так как жекомпилятор решит, какую перегрузку следует использовать при использовании этих методов расширения?

Ответы [ 3 ]

4 голосов
/ 02 октября 2019

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

Итак, допустим, вы используете FirstOrDefault в DbSet. DbContext.Foos.FirstOrDefault(x=> x.Bar == true);

Прежде всего, я надеюсь, что вы не написали бы это. Если вы хотите спросить "идет дождь?"ты спрашиваешь "идет дождь?"или вы спрашиваете "является ли утверждение, что идет дождь, истинное утверждение?"Просто скажите FirstOrDefault(x => x.Bar).

Далее, учитывая следующие перегрузки:

public static TSource FirstOrDefault<TSource>(
    this IQueryable<TSource> source, 
    Expression<Func<TSource, bool>> predicate)

public static TSource FirstOrDefault<TSource>(
    this IEnumerable<TSource> source, 
    Func<TSource, bool> predicate)

Как компилятор выбирает, какая перегрузка является лучшей?

Сначала мы делаем введите логический вывод , чтобы определить, что TSource есть в каждом. Детали алгоритма вывода типов сложны;задайте более сфокусированный вопрос, если у вас есть вопрос по этому поводу.

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

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

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

  • DbContext.Foos можно преобразовать в IEnumerable<Foo> или IQueryable<Foo>. Что, если либо, является лучшим преобразованием?
  • Лямбда может быть преобразована либо в делегат, либо в дерево выражений. Что, если либо, является лучшим преобразованием?

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

Чтобы ответить на первый вопрос, мы применяем правило преобразование в конкретное лучше, чем преобразование в общее . Если у вас есть возможность перейти на Жирафа или Млекопитающее, то лучше перейти на Жирафа. Итак, теперь вопрос в том, что является более конкретным , IQueryable<Foo> или IEnumerable<Foo>?

Правило проверки специфичности является простым: если X может быть неявно преобразовано в Y, но Y не может бытьнеявно преобразуется в X, тогда X является более конкретным. Жираф можно использовать там, где требуется животное, но нельзя использовать животное там, где нужен жираф, поэтому жираф более конкретен. Или: каждый жираф - это животное, но не каждое животное - это жираф, поэтому жираф более специфичен.

По этому показателю IQueryable<T> более специфичен, чем IEnumerable<T>, поскольку каждый запрашиваемый является перечислимым, но некаждое перечисляемое является запрашиваемым.

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

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

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

Я рекомендую вам прочитать спецификацию, если у вас есть дополнительные вопросы.

3 голосов
/ 02 октября 2019

Унаследованная близость классов имеет большее значение, чем точные типы параметров метода

Обратите внимание, что вариант Expression<Func<T,bool>> применяется к IQueryable<T>, тогда как вариант Func<T, bool> применяется к IEnumerable<T>.
При поискеПри совпадении метода компилятор всегда выбирает тот, который ближе всего к типу объекта. Иерархия наследования выглядит следующим образом:

DbSet<T> : IQueryable<T> : IEnumerable<T>

Примечание: между ними могут быть и другие наследования, но это не имеет значения. Важно то, что ближе всего к DbSet<T>. IQueryable<T> ближе к DbSet<T>, чем IEnumerable<T>.

Следовательно, компилятор попытается найти подходящий метод в IQueryable<T>. Он задает два вопроса:

  • Имеет ли этот тип метод с таким именем?
  • Соответствуют ли типы параметров метода / сопоставляются?

IQueryable<T> имеет метод FirstOrDefault, поэтому пункт 1 маркированного списка выполняется);и поскольку x => x.MyBoolean можно неявно преобразовать в Expression<Func<T, bool>>, пункт 2 также будет удовлетворен.

Таким образом, вы получите вариант Expression<Func<T,bool>>, определенный для IQueryable<T>.

Предположим, x => x.MyBoolean может не быть неявно преобразованным в Expression<Func<T,bool>>, но может быть преобразовано в Func<T,bool> (примечание: это не так, но это может произойти для другихтипы / значения), тогда пункт 2 будет не удовлетворен.
На этом этапе, поскольку компилятор не нашел соответствия в IQueryable<T>, он будет продолжать искать, натыкаясь на IEnumerable<T> и задайте себе те же вопросы (маркеры). Обе маркированные точки были бы сохранены.

Следовательно, в этом случае вы бы получили вариант Func<T,bool>, определенный в IEnumerable<T>.

Обновление

Вот пример dotnetfiddle .

Обратите внимание, что хотя я передаю int значения (которые использует сигнатура базового метода), сигнатура double класса Derived подходит (потому что int неявно преобразуется в double), а компилятор никогда не ищет в классе Base.

Однако в Derived2 это не так. Поскольку int неявно не преобразуется в string, в Derived2 не найдено совпадений, а компилятор ищет в Base и использует метод int из Base.

0 голосов
/ 02 октября 2019

Я думаю, что наиболее полезным местом для поиска в спецификации C # является Выражения анонимных функций :

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

...

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

, что затем приводит к преобразованию анонимной функции :

Лямбда-выражение F совместимо с типом дерева выражений Expression<D>, если F совместимо с типом делегата D. Обратите внимание, что это не относится к анонимным методам, только к лямбда-выражениям.


Это сухие биты из спецификации. Однако также полезно прочитать Эрика Липперта. Как мы можем гарантировать, что вывод типа завершается для объединения битов.

...