- Are nullable annotations part of array specifiers?
- Cancellation of async-streams
Due to history, the syntax for array specifiers is actually "outside-in",
i.e. the first set of []
actually refers to the outermost array. For the
elements themselves, you always specify the ?
directly after the element
type name, so string?[][]
works regardless of interpretation.
For specifying the nullability of the "innermost" vs "outermost" arrays, it seems like we have two possibilities for describing nullability here:
string[]?[] a
string[][]? a
Either of these could specify that the innermost array is null. Here are full examples of the meaning of approaches (1) and (2) with a type declaration:
string?[][] a; new string?[3][] // 1. Array nullable string, 2. likewise
string[]?[] a; // 1. Nullable array of array of string, 2. Array of nullable array of string
string[][]? a; // 1. Array of nullable array of string, 2. Nullable array of array of string
If we look at instantiation, rather than type declaration, the two approaches look like:
new string?[3][]
new string[3]?[] // invalid
new string[3][]?
new string?[3][]
new string[3][]? // invalid
new string[]?[3]
There's also one more option that's a hybrid. Because of array covariance and
nullable covariance, assigning an array of non-nullable arrays to an array of
nullable arrays does not generate warnings. Declarations would like (2), but
instantiation would actually disallow ?
completely:
string?[][] a = new string?[3][];
// string[]?[] a = new string[]?[3]; // disallow ? in new
string[]?[] a = new string[3][];
string[][]? a = new string[3][];
There are essentially two mental models of multi-dimensional arrays that have served people writing multi-dimensional array code: the way the code actually works, and a "type nesting" model that is mostly non-observable in the current language. When choosing between options 1 and 2/3, we have a decision to either preserve how multi-dimensional arrays actually "work", regardless of whether or not it's confusing, or try to accommodate how we might prefer they work and how people may actually think they work.
Option (1) is attractive not only because it's how the feature currently
works, but also because Option 2/3 would involve changing the order of the
rank specifier specifically for nullable multi-dimensional arrays, so code
that previously used new string[3][]
would now be written new string[]?[3]
, changing not just the location of the ?
, but also the
location of 3
.
One reason to go with accommodation is to make nullability specifically more
similar to how it works in other places in the language. Generally, to make a
type nullable, you add a ?
to the end. This would not be the case with the
existing multi-dimensional array model.
If most people are adopting existing code with jagged arrays, Option 2/3 may make it easier to adopt the rules common in other areas of the language to make the warnings go away. Since we don't provide nullability warnings for everything but assigning null as the innermost array, the nullability of the innermost array matters a little less.
Decision
Let's stick with the current implementation (1). People may have incorrect mental models which have served them sufficiently until now, but we don't yet see sufficient motivation for complicating the language.
The first question is what kind of support for cancellation we want to put
into the interface itself. The primary proposal here is changing the
signature of IAsyncEnumerable<T>
to take an optional CancellationToken:
IAsyncEnumerable<T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token = default);
}
The user's implementation of the interface could then provide a mechanism to
pass a CancellationToken during construction, and we could provide an
extension method IAsyncEnumerable<T> WithCancellationToken<T>(this IAsyncEnumerable<T> e, CancellationToken token)
that could wrap an existing
IAsyncEnumerable<T>
with a cancellable one.
For the new language features there are two potential scenarios to support.
The first is allowing the user to pass in their own token for consumption
i.e., cancellation during an await foreach
.
The second scenario is allowing the user to consume a CancellationToken during production i.e., when writing an async iterator.
Most proposals center around some extra syntax for await foreach
that
allows a CancellationToken to be passed e.g.,
await foreach (var x in e, cancellationToken)
{
...
}
If we allow specifying the cancellation token during enumeration, passing the cancellation token when calling the generated iterator is probably the wrong place to pass it, as you should put it in through enumeration.
A user scenario is the user is writing an iterator and wants to react to a cancellation token that can in as a parameter. For example,
async IAsyncEnumerable<int> M(..., CancellationToken token = default)
{ ... }
If we're writing an iterator, the main feature we would need to support is putting the CancellationToken into the generated iterator and specifically connect it to a CancellationToken provided to the method.
One way of supporting this would have a special "value"-like keyword that refers to an implicit cancellation token that's used in the generated enumerable. An even more maximalist proposal would allow implicit CancellationTokens that can be added to the end of any method, and if a CancellationToken is passed anywhere then it can be implicitly threaded through. This is a much larger feature.
Another way of providing a CancellationToken could be specifying that a particular parameter that is the special CancellationToken, e.g.
IAsyncEnumerable<int> M(..., [Cancellation]CancellationToken token = default)`
Whichever way the custom token is specified, the token would be stored in the
IAsyncEnumerable
state machine, and if another token is passed to
GetEnumeratorAsync
then we would also store that and decide if we create a
combined token for both, or override the original token provided.
Decision
-
Change
IAsyncEnumerable<T>.GetAsyncEnumerator()
toIAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken token = default)
and provide aWithCancellation
extension method. -
In the constructed iterator state machine,
GetEnumerator
should throw if the given CancellationToken is cancelled. It's not decided exactly where or how often we will check for cancellation. -
Do nothing more, right now. Revisit the production and consumption sides of this before shipping.