Почему async/await допускает неявное преобразование из списка в IEnumerable?

Я просто играл с асинхронным/ожиданием и узнал что-то интересное. Взгляните на приведенные ниже примеры:

// 1) ok - obvious
public Task<IEnumerable<DoctorDto>> GetAll()
{
    IEnumerable<DoctorDto> doctors = new List<DoctorDto>
    {
        new DoctorDto()
    };

    return Task.FromResult(doctors);
}

// 2) ok - obvious
public async Task<IEnumerable<DoctorDto>> GetAll()
{
    IEnumerable<DoctorDto> doctors = new List<DoctorDto>
    {
        new DoctorDto()
    };

    return await Task.FromResult(doctors);
}

// 3) ok - not so obvious
public async Task<IEnumerable<DoctorDto>> GetAll()
{
    List<DoctorDto> doctors = new List<DoctorDto>
    {
        new DoctorDto()
    };

    return await Task.FromResult(doctors);
}

// 4) !! failed to build !!
public Task<IEnumerable<DoctorDto>> GetAll()
{
    List<DoctorDto> doctors = new List<DoctorDto>
    {
        new DoctorDto()
    };

    return Task.FromResult(doctors);
}

Рассмотрим случаи 3 и 4. Единственное отличие состоит в том, что 3 использует ключевые слова async/await. 3 строит отлично, однако 4 дает ошибку о неявном преобразовании List в IEnumerable:

Cannot implicitly convert type 
'System.Threading.Tasks.Task<System.Collections.Generic.List<EstomedRegistration.Business.ApiCommunication.DoctorDto>>' to 
'System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<EstomedRegistration.Business.ApiCommunication.DoctorDto>>'  

Что это значит, что здесь изменяются ключевые слова async/wait?

Ответ 1

Task<T> просто не является ковариантным типом.

Хотя List<T> можно преобразовать в IEnumerable<T>, Task<List<T>> connot быть преобразованным в Task<IEnumerable<T>>. И в # 4, Task.FromResult(doctors) возвращает Task<List<DoctorDto>>.

В № 3 мы имеем:

return await Task.FromResult(doctors)

Это то же самое, что:

return await Task<List<DoctorDto>>.FromResult(doctors)

Это то же самое, что:

List<DoctorDto> result = await Task<List<DoctorDto>>.FromResult(doctors);
return result;

Это работает, потому что List<DoctorDto> может быть преобразован IEnumerable<DoctorDto>.

Ответ 2

Подумайте о своих типах. Task<T> не вариант, поэтому он не конвертируется в Task<U>, даже если T : U.

Однако, если t - Task<T>, то тип await t равен t, а t может быть преобразован в U, если T : U.

Ответ 3

Ясно, что вы понимаете, почему List<T> можно хотя бы вернуть как IEnumerable<T>: просто потому, что он реализует этот интерфейс.

Также ясно, что третий пример делает что-то "лишнее", чем четвертое. Как говорили другие, 4-я неудача из-за отсутствия ковариантности (или наоборот), я никогда не могу вспомнить, в каком направлении они идут!), Потому что вы прямо пытаетесь предложить экземпляр Task<List<DoctorDto>> в качестве экземпляра Task<IEnumerable<DoctorDto>>.

Причина, по которой проходит третий проход, состоит в том, что await добавляет большой кусок "кода поддержки", чтобы заставить его работать по назначению. Этот код разрешает Task<T> на T, так что return await Task<something> возвращает тип, закрытый в общем Task<T>, в этом случае something.

То, что подпись метода затем возвращает Task<T>, и она работает, снова решается компилятором, который требует Task<T>, Task или void для асинхронных методов и просто массирует ваш T обратно в Task<T> как часть всего фона, созданного asyn/await продолжением gubbins.

Это добавленный шаг получения T от await и его нужно перевести обратно в Task<T>, который дает ему пространство, в котором он должен работать. Вы не пытаетесь использовать существующий экземпляр Task<U> для удовлетворения Task<T>, вместо этого вы создаете новый Task<T>, предоставляя ему U : T, а при построении неявное литье происходит так, как вы ожидали (точно так же, как вы ожидаете работать IEnumerable<T> myVar = new List<T>();).

Убей/поблагодарить компилятор, я часто делаю; -)