188 lines
6.8 KiB
Markdown
188 lines
6.8 KiB
Markdown
|
|
# C# LDM notes for Nov 28, 2018
|
|
|
|
1. Are nullable annotations part of array specifiers?
|
|
2. Cancellation of async-streams
|
|
|
|
# Discussion
|
|
|
|
## Nullable array specifiers
|
|
|
|
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:
|
|
|
|
1. `string[]?[] a`
|
|
2. `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:
|
|
|
|
1.
|
|
|
|
```
|
|
new string?[3][]
|
|
new string[3]?[] // invalid
|
|
new string[3][]?
|
|
```
|
|
|
|
|
|
2.
|
|
|
|
```
|
|
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:
|
|
|
|
3.
|
|
|
|
```
|
|
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.
|
|
|
|
|
|
## Cancellation of async streams
|
|
|
|
### CancellationToken in the IAsyncEnum* interfaces
|
|
|
|
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:
|
|
|
|
```C#
|
|
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.
|
|
|
|
### Async iterators and `await foreach`
|
|
|
|
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.
|
|
|
|
#### Consumption
|
|
|
|
Most proposals center around some extra syntax for `await foreach` that
|
|
allows a CancellationToken to be passed e.g.,
|
|
|
|
```C#
|
|
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.
|
|
|
|
#### Production
|
|
|
|
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,
|
|
|
|
```C#
|
|
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.
|
|
```C#
|
|
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**
|
|
|
|
1. Change `IAsyncEnumerable<T>.GetAsyncEnumerator()` to `IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken token = default)` and
|
|
provide a `WithCancellation` extension method.
|
|
|
|
2. 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.
|
|
|
|
3. Do nothing more, right now. Revisit the production and consumption sides
|
|
of this before shipping. |