Как объяснить эту ошибку «вызов неоднозначен»? - PullRequest
7 голосов
/ 19 марта 2020

Проблема

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

Отображение на тип значения работает нормально:

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

Вопрос

Может кто-нибудь объяснить, пожалуйста для меня, что здесь происходит? Я уже разочаровался в поиске возможного исправления этой ошибки, но, по крайней мере, я хотел бы понять причину root этого беспорядка.

Дополнительные примечания

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

И третий вариант - ограничить сопоставления endofunctions (то есть 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 ]

3 голосов
/ 21 марта 2020

Согласно C# Спецификации, Вызовы методов , следующие правила используются для рассмотрения обобщенного 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 методов). Примечание: для определения типа второго аргумента (лямбда-выражения) используется вывод типа.

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

Лучший метод из набора методов-кандидатов определяется с использованием правил разрешения перегрузки разрешения перегрузки. Если один лучший метод не может быть идентифицирован, вызов метода неоднозначен, и возникает ошибка времени привязки. При выполнении разрешения перегрузки параметры обобщенного c метода учитываются после подстановки аргументов типа (предоставленных или предполагаемых) для соответствующих параметров типа метода.

Выражение

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

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

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

2 голосов
/ 21 марта 2020

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

Во всех случаях ошибок тип ввода T1 в Fun<T1, T2> неоднозначен. Например:

И Task<int>, и int имеют метод ToString, поэтому невозможно определить, является ли это заданием или целым.

Однако, если + При использовании в выражении ясно, что тип ввода целочисленный, поскольку задача не поддерживает оператор +. .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)
0 голосов
/ 19 марта 2020

добавить фигурные скобки

var result = (await Task
            .FromResult<string?>("test"))
            .Map(x => $"result: {x}");

ваш метод FilterExt asyn c просто добавляет фигурные скобки в (ожидание x) и затем вызывает не асинхронный c метод, так зачем вам нужен asyn c method ??

ОБНОВЛЕНИЕ: Как я заметил во многих библиотеках. net, разработчики просто добавляют суффикс Asyn c к методам asyn c. Вы можете назвать метод MapAsyn c, FilterAsyn c

...