Как объяснить эту ошибку "вызов неоднозначен"?

Эта проблема

Рассмотрим эти два метода расширения, которые представляют собой простую карту любого типа. 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

Другие вопросы по тегам