Как объяснить эту ошибку "вызов неоднозначен"?
Эта проблема
Рассмотрим эти два метода расширения, которые представляют собой простую карту любого типа. T1
к T2
, плюс перегрузка для плавного отображения Task<T>
:
public static class Ext {
public static T2 Map<T1, T2>(this T1 x, Func<T1, T2> f)
=> f(x);
public static async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f)
=> (await x).Map(f);
}
Теперь, когда я использую вторую перегрузку с сопоставлением с ссылочным типом...
var a = Task
.FromResult("foo")
.Map(x => $"hello {x}"); // ERROR
var b = Task
.FromResult(1)
.Map(x => x.ToString()); // ERROR
... Я получаю следующую ошибку:
CS0121: Вызов неоднозначен для следующих методов или свойств: "Ext.Map(T1, Func)" и "Ext.Map(Task, Func)"
Сопоставление с типом значения работает нормально:
var c = Task
.FromResult(1)
.Map(x => x + 1); // works
var d = Task
.FromResult("foo")
.Map(x => x.Length); // works
Но только до тех пор, пока отображение действительно использует ввод для создания вывода:
var e = Task
.FromResult(1)
.Map(_ => 0); // ERROR
Вопрос
Кто-нибудь может объяснить мне, что здесь происходит? Я уже отказался от поиска подходящего решения для этой ошибки, но, по крайней мере, я хотел бы понять основную причину этого беспорядка.
Дополнительные примечания
Пока что я нашел три обходных пути, которые, к сожалению, неприемлемы в моем случае использования. Первый - указать аргументы типаTask<T1>.Map<T1,T2>()
явно:
var f = Task
.FromResult("foo")
.Map<string, string>(x => $"hello {x}"); // works
var g = Task
.FromResult(1)
.Map<int, int>(_ => 0); // works
Другой обходной путь - не использовать лямбды:
string foo(string x) => $"hello {x}";
var h = Task
.FromResult("foo")
.Map(foo); // works
И третий вариант - ограничить сопоставления эндофункциями (т.е. Func<T, T>
):
public static class Ext2 {
public static T Map2<T>(this T x, Func<T, T> f)
=> f(x);
public static async Task<T> Map2<T>(this Task<T> x, Func<T, T> f)
=> (await x).Map2(f);
}
Я создал.NET Fiddle, где вы можете сами опробовать все приведенные выше примеры.
3 ответа
В соответствии со Спецификацией C# для вызовов методов следующие правила используются для рассмотрения универсального метода.F
в качестве кандидата на вызов метода:
Метод имеет то же количество параметров типа метода, которое было предоставлено в списке аргументов типа,
а также
После того, как аргументы типа заменяются соответствующими параметрами типа метода, все сконструированные типы в списке параметров
F
удовлетворяют их ограничениям (удовлетворяющие ограничениям), а список параметровF
применимо в отношенииA
(Применимый функциональный член).A
- необязательный список аргументов.
Для выражения
Task.FromResult("foo").Map(x => $"hello {x}");
оба метода
public static T2 Map<T1, T2>(this T1 x, Func<T1, T2> f);
public static async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f);
удовлетворяют этим требованиям:
- у них обоих есть два параметра типа;
их сконструированные варианты
// T2 Map<T1, T2>(this T1 x, Func<T1, T2> f) string Ext.Map<Task<string>, string>(Task<string>, Func<Task<string>, string>); // Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f) Task<string> Ext.Map<string, string>(Task<string>, Func<string, string>);
удовлетворять ограничениям типа (потому что нет ограничений типа для Map
методы) и применимы в соответствии с необязательными аргументами (поскольку также нет необязательных аргументов для Map
методы). Примечание: для определения типа второго аргумента (лямбда-выражения) используется вывод типа.
Итак, на этом этапе алгоритм рассматривает оба варианта как кандидатов на вызов метода. В этом случае он использует разрешение перегрузки, чтобы определить, какой кандидат лучше подходит для вызова. Слова из спецификации:
Лучший метод из набора методов-кандидатов определяется с помощью правил разрешения перегрузки Разрешения перегрузки. Если единственный лучший метод не может быть идентифицирован, вызов метода неоднозначен и возникает ошибка времени привязки. При выполнении разрешения перегрузки параметры универсального метода рассматриваются после замены аргументов типа (предоставленных или выведенных) на соответствующие параметры типа метода.
Выражение
// I intentionally wrote it as static method invocation.
Ext.Map(Task.FromResult("foo"), x => $"hello {x}");
можно переписать следующим образом, используя построенные варианты метода Map:
Ext.Map<Task<string>, string>(Task.FromResult("foo"), (Task<string> x) => $"hello {x}");
Ext.Map<string, string>(Task.FromResult("foo"), (string x) => $"hello {x}");
При разрешении перегрузки используется алгоритм Better function member, чтобы определить, какой из этих двух методов лучше подходит для вызова метода.
Я читал этот алгоритм несколько раз и не нашел места, где алгоритм может определять метод Exp.Map<T1, T2>(Task<T1>, Func<T1, T2>)
как лучший метод для вызова рассматриваемого метода. В этом случае (когда лучший метод не может быть определен) возникает ошибка времени компиляции.
Подводить итоги:
- алгоритм вызова метода рассматривает оба метода как кандидатов;
- лучший алгоритм функции-члена не может определить лучший метод для вызова.
Другой подход, помогающий компилятору выбрать лучший метод (как и в других обходных путях):
// Call to: T2 Map<T1, T2>(this T1 x, Func<T1, T2> f);
var a = Task.FromResult("foo").Map( (string x) => $"hello {x}" );
// Call to: async Task<T2> Map<T1, T2>(this Task<T1> x, Func<T1, T2> f);
var b = Task.FromResult(1).Map( (Task<int> x) => x.ToString() );
Теперь первый аргумент типа T1
явно определен, и двусмысленность не возникает.
При разрешении перегрузки компилятор выведет аргументы типа, если они не указаны.
Во всех случаях ошибки тип ввода T1
в Fun<T1, T2>
неоднозначно. Например:
Обе Task<int>
, а также int
имеют ToString
метод, поэтому невозможно определить, является ли это задачей или int.
Однако если +
используется в выражении, ясно, что тип ввода целочисленный, потому что задача не поддерживает +
оператор. .Length
та же история.
Это также может объяснить другие ошибки.
ОБНОВИТЬ
Причина прохождения Task<T1>
не заставит компилятор подобрать метод с Task<T1>
в списке аргументов компилятору нужно приложить усилия, чтобы вывести T1
снаружи Task<T1>
потому как T1
не находится непосредственно в списке аргументов метода.
Возможное исправление: Сделать Func<>
чтобы использовать то, что существует в списке аргументов метода, поэтому компилятор требует меньше усилий при выводе T1
.
static class Extensions
{
public static T2 Map<T1, T2>(this T1 obj, Func<T1, T2> func)
{
return func(obj);
}
public static T2 Map<T1, T2>(this Task<T1> obj, Func<Task<T1>, T2> func)
{
return func(obj);
}
}
Применение:
// This calls Func<T1, T2>
1.Map(x => x + 1);
// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(async _=> (await _).ToString())
// This calls Func<Task<T1>, T2>
Task.FromResult(1).Map(_=> 1)
// This calls Func<Task<T1>, T2>.
// Cannot compile because Task<int> does not have operator '+'. Good indication.
Task.FromResult(1).Map(x => x + 1)
Добавить подтяжки
var result = (await Task
.FromResult<string?>("test"))
.Map(x => $"result: {x}");
ваш асинхронный метод FilterExt просто добавляет фигурные скобки к (ожидание x), а затем вызывает неасинхронный метод, так зачем вам нужен метод async??
ОБНОВЛЕНИЕ: как я заметил во многих библиотеках.net, разработчики просто добавляют суффикс Async к методам async. Вы можете назвать метод MapAsync, FilterAsync