Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
337 views
in Technique[技术] by (71.8m points)

c# - Why am I not allowed to return an IAsyncEnumerable in a method returning an IAsyncEnumerable

I have the following interface:

public interface IValidationSystem<T>
{
    IAsyncEnumerable<ValidationResult> ValidateAsync(T obj);
}

And I am trying to implement it this way:

public class Foo
{ }

public class Bar
{ }

public class BarValidationSystem : IValidationSystem<T>
{   
    public async IAsyncEnumerable<ValidationResult> ValidateAsync(Bar bar)
    {
        var foo = await GetRequiredThingAsync();

        return GetErrors(bar, foo).Select(e => new ValidationResult(e)).ToAsyncEnumerable();
    }

    private static IEnumerable<string> GetErrors(Bar bar, Foo foo)
    {
        yield return "Something is wrong";
        yield return "Oops something else is wrong";
        yield return "And eventually, this thing is wrong too";
    }
    
    private Task<Foo> GetRequiredThingAsync()
    {
        return Task.FromResult(new Foo());
    }
}

But this does not compile:

CS1622 Cannot return a value from an iterator. Use the yield return statement to return a value, or yield break to end the iteration.

I know I can fix by iterating the enumerable:

foreach (var error in GetErrors(bar, foo))
{
    yield return new ValidationResult(error);
}

Or by returning a Task<IEnumerable<ValidationResult>>:

public async Task<IEnumerable<ValidationResult>> ValidateAsync(Bar bar)
{
    var foo = await GetRequiredThingAsync;

    return GetErrors(bar, foo).Select(e => new ValidationResult(e));
}

But I would like to understand why I cannot return an IAsyncEnumerable in my case. When writing "classic" IEnumerable methods, you can either return an IEnumerable or yield return several values. Why am I not allowed to do the same with IAsyncEnumerable?

question from:https://stackoverflow.com/questions/65917317/why-am-i-not-allowed-to-return-an-iasyncenumerable-in-a-method-returning-an-iasy

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

This looks like a bug or at least an unintentional limitation, when reading the spec proposal.

The spec states that the presence of yield results in an iterator method; and the presence of both async and yield results in an asynchronous iterator method.

But I would like to understand why I cannot return an IAsyncEnumerable in my case.

The async keyword is making this into an asynchronous iterator method. Since you need the async for the await, then you'll need to use yield as well.

When writing "classic" IEnumerable methods, you can either return an IEnumerable or yield return several values. Why am I not allowed to do the same with IAsyncEnumerable?

With both IEnumerable<T> and IAsyncEnumerable<T>, you can perform synchronous work before returning the enumerable directly. In this case, the method is not special at all; it just does some work and then returns a value to its caller.

But you can't do asynchronous work before returning an asynchronous enumerator. In this case, you need the async keyword. Adding the async keyword forces the method to either be an asynchronous method or an asynchronous iterator method.

To put it another way, all methods can be classified into these different types in C#:

  • Normal methods. No async or yield present.
  • Iterator methods. A yield in the body without async. Must return IEnumerable<T> (or IEnumerator<T>).
  • Asynchronous methods. An async is present without yield. Must return a tasklike.
  • Asynchronous iterator methods. Both async and yield are present. Must return IAsyncEnumerable<T> (or IAsyncEnumerator<T>).

From yet another perspective, consider the state machine that must be used to implement such a method, and especially think about when the await GetRequiredThingAsync() code runs.

In the synchronous world without yield, GetRequiredThing() would run before returning the enumerable. In the synchronous world with yield, GetRequiredThing() would run when the first item of the enumerable is requested.

In the asynchronous world without yield, await GetRequiredThingAsync() would run before returning the async enumerable (and in that case, the return type would be Task<IAsyncEnumerable<T>>, since you have to do asynchronous work to get the async enumerable). In the asynchronous world with yield, await GetRequiredThingAsync() would run when the first item of the enumerable is requested.

Generally speaking, the only case when you want to do work before returning the enumerable is when you're doing precondition checks (which are synchronous by nature). Doing an API/DB call is not normal; most of the time the expected semantics are that any API/DB calls will be done as part of enumeration. In other words, even the synchronous code probably should have been using foreach and yield, just like the asynchronous code is forced to do.

On a side note, it would be nice in these scenarios to have a yield* for both synchronous and asynchronous iterators, but C# does not support that.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...