csharplang/meetings/2018/LDM-2018-12-12.md
2019-01-07 00:20:35 -08:00

3.2 KiB

C# LDM notes for Dec 12, 2018

Agenda

  1. Open issues with async streams
  2. Pattern dispose

Discussion

Cancellation in GetAsyncEnumerator

We previously decided that GetAsyncEnumerator takes a CancellationToken. When do we check if it's been cancelled?

Our design for cancellation in the user code itself is to have the user write the core logic for their IAsyncEnumerable using an IAsyncEnumerator iterator method. The user can manually check for cancellation inside the iterator body.

class C : IAsyncEnumerable
{
    IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token)
    {
        ...
        token.ThrowIfCancelled();
        ...
    }
}

Q: Do we want to generate any checks manually? One option is to check the cancellation token on every MoveNext call.

This comes down to: where is it most useful to request cancellation?

The most important place to include the cancellation token is in the await calls themselves. This isn't compiler generated code at all -- if the expressions being awaited need a CancellationToken the user will need to pass it themselves. Other places, like every entry to MoveNextAsync, will be called regularly, but will only be able to cancel synchronous code, instead of the long-running async operation.

Conclusion

The compiler will not generate checks for cancellation for now. If we get user feedback that it's important, we will revisit this decision.

Pattern binding

Which pattern do we want the compiler to recognize for GetAsyncEnumerator?

Binding options for some expression target of a foreach, e:

  1. e.GetAsyncEnumerable()
  2. e.GetAsyncEnumerable(default(CancellationToken))

One of the primary drawbacks for (1) is that all users must make their CancellationToken parameters optional. This may be a problem for implementors who consider it very important to have a CancellationToken.

Conclusion

We will attempt to bind the call e.GetAsyncEnumerable(default(CancellationToken)) and if it succeeds and has a conforming return type, the pattern binds successfully.

Recognizing an iterator

Consider the following:

class Program
{
    static async IAsyncEnumerable<string> M()
    {
        throw new NotImplementedException();
    }
}

As currently designed, this is illegal. async methods must either return a Task-like type or return IAsyncEnumerable/IAsyncEnumerator and contain a yield statement. This is neither.

The proposal is to produce an error. There are a number of fixes:

  1. Add a yield in the body.
  2. Remove the async.

Conclusion

Add an error, undecided on the error message.

When we start recognizing pattern Dispose, do we call it in foreach?

Certainly, it seems very important for the feature. Since a ref struct cannot implement interfaces, pattern Dispose is the only way to implement disposal.

There are a number of possible breaking changes:

  1. A pattern enumerable type did not implement IDisposable, but had a Dispose method. This method would now be call.
  2. If there are two Dispose extension methods, that will now produce an ambiguity and thus a compilation error.

Conclusion

Let's narrow the pattern-based Dispose change down to ref structs only. This means every other type will be required to implement IDisposable.