altmann / FluentResults

A generalised Result object implementation for .NET/C#
MIT License
1.92k stars 109 forks source link

Don't understand, why an implicit conversion of matching generic types doesn't work #201

Closed olivermue closed 3 weeks ago

olivermue commented 6 months ago

When using FluentResults with generic types like IReadOnlyList<T> it seems that implicit conversation doesn't work and I can't understand why. Maybe someone can enlighten me by this simple example.

What works without any problems is this kind of code:

private async Task<string> FindMatchingItem()
{
    await Task.Delay(1);
    return "1";
}

public async Task<Result<string>> GetMatchingItem()
{
    var item = await FindMatchingItem();
    // Here we can simply return item, which will be automatically be converted into Result<string>
    return item;
}

But, what doesn't work is this one here:

private async Task<IReadOnlyList<string>> FindMatchingItems()
{
    await Task.Delay(1);
    return new List<string> { "1", "2", "3" };
}

public async Task<Result<IReadOnlyList<string>>> GetMatchingItems()
{
    var items = await FindMatchingItems();
    // Doesn't compile. Has to be: return Result.Ok(items);
    return items;
}

The compiler complains with this error message: CS0266: Cannot implicitly convert type 'System.Collections.Generic.IReadOnlyList<string>' to 'FluentResults.Result<System.Collections.Generic.IReadOnlyList<string>>'. An explicit conversion exists (are you missing a cast?)

If I'm going to change the signature of FindMatchingItems() to return Task<List<string>> the error is gone:

// We changed the return type to be List instead of IReadOnlyList
private async Task<List<string>> FindMatchingItems()
{
    await Task.Delay(1);
    return new List<string> { "1", "2", "3" };
}

// But not here. It is still IReadOnlyList
public async Task<Result<IReadOnlyList<string>>> GetMatchingItems()
{
    var items = await FindMatchingItems();
    return items;
}

So can anyone explain, why List<string> can be converted into Result<IReadOnlyList<string>>, but the very same type IReadOnlyList<string> can't be converted into the desired Result type?

Kilazur commented 1 month ago

If someone needs a simple workaround, return Result.Ok(items).

Kysluss commented 1 month ago

TLDR; You can also use items.ToList() to make this work. It seems to be a limitation of the C# spec.

I was messing around with this today and this same thing happens if you use IEnumerable instead of IReadOnly* variants as well. I have a feeling this is related to this SO answer and this SO answer by Eric Lippert where he talks about implicit conversion of interface types. The spec doesn't support this and there aren't plans to add it. What does work currently is considered a bug that they are leaving in place to not break existing production systems.

Because the result is wrapped in Task, it sort of obscures the defined implicit conversion that would work if it was a synchronous call. You can reproduce that same issue because Task<IEnumerable<string>> MyMethod() => Task.FromResult(["A"]); won't compile for the same reason. We know [] is an IEnumerable, but because it is wrapped in Task, it cannot implicitly convert to that type and you either need to return Task.FromResult<IEnumerable<string>>(["A"]) or await the task.