We brought in the design review team to look at some of our recent and open decisions in C# LDM.
- Nullable reference types: shipping annotations
- Pattern-based indexing with
Index
andRange
- Cancellation tokens in async streams
Up until recently we were planning to add a new type of file, a "sidecar" file, that could be an external source for describing nullability annotations for existing libraries. This has some significant advantages: It works "down-target", and it can get you nullability without breaking open files that are not maintained, or that you do not have access to.
On the other hand, this would be a new file format that we'd need to understand in all compilers (not just C#), and plumb through extensive amounts of tooling. If we're lucky, lack of nullability annotations is mostly a temporary, transitional problem. But a new file format would be a new permanent layer of complexity that would be around forever. The fix might be worse than the problem.
We therefore recently decided to drop sidecar files, at least for now. The review team agreed with this. Instead we should double down on annotating the .NET Core libraries themselves.
To this point, we consider it highly plausible that we will not get all the annotations done for the .NET Core release timeframe (though feedback was that we should try!). Instead we are going to have to roll them out over a period of time, maybe a year after initial release.
Feedback is that we should message clearly that our core libraries won't be done with this transition, and that new warnings are likely to hit with every release until we are. We expect the warnings to be broadly useful, but if folks can't live with the churn they should opt out.
We're better off focusing on quality than quantity of annotations we ship in each go-around. Once shipped, it's important that we don't change our minds, much as with other surface area.
We'd prefer not to add too much user-facing mechanism for this, because again, some day we'll be done, but the mechanism can't be removed.
That said, we can consider features for more granular opt-out, instead of just "don't give nullable warnings." E.g. we could have attributes that say "treat a given imported member, type or namespace as null-oblivious when accessing in my code."
We do have more local "fixes": there's !
and pragmas. It's a delicate balance, though. The more you drive opt-outs into details of the code, the more likely you are to accidentally leave them in place, and the harder it is going to be to eventually remove them.
Adding indexers to existing types to consume the Index
and Range
types is too invasive, and we are planning to embrace a more pattern-based approach. For Index
the compiler can synthesize the behavior from int-based indexers and access to a Length
or Count
property. For Range
there is typically no existing behavior, except in recently added types that have a Slice
method. If we base a pattern for Range
indexing on Slice
, then people can add extension methods to existing types.
Both patterns are open to some compile time optimizations, when the Index
es and Range
s are directly given to the indexers as index and range expressions. In those cases we won't necessarily need to construct the Index
and Range
values but can take compiler shortcuts in terms of the constituent expressions.
The reviewers were supportive of this direction and we will go forward with it.
In the current state of design, there is no way for an async
iterator method to get hold of the CancellationToken
that the consumer passed to the enumerator.
We discussed options for "naming" the cancellation token in the body of the iterator. There could either be a specially marked parameter (the value of which gets supplanted on each enumeration of the IAsyncEnumerable
), or a magic variable that's automatically in scope, similar to value
in a property setter.
We agreed on the latter approach, using the variable name cancellationToken
. This is the name most commonly used when cancellation tokens are passed as parameters.