6.8 KiB
C# Language Design Meeting for January 3rd, 2022
Agenda
Quote of the Day
- "I just want to point out that slicing with [redacted] sounds like a cooking show, and I'd watch that" "Yeah" "Me too"
Discussion
Slicing assumptions in list patterns, revisited
https://github.com/dotnet/csharplang/issues/5599
We took another look at this corner case of list patterns, with an eye for trying to settle on a consistent and logical representation that will be both unsurprising to the user, and set the language up for success in the future if we ever want to build on list patterns in other ways. We generally agree that this is an extreme corner case: the overwhelming majority of slice patterns should be variable assignments, and those that aren't are almost certainly not going to be another a list of pattern with a finite number of elements in it. However, we think it's important to codify the language's expectations of how a well-behaved slice method should work, and that future pattern work could possibly suffer if we don't make those assumptions now.
Formally, our expectation is: a Slice
method should never return null
. It should return an empty slice, or throw if indexes are out
of range, but never return null
. In order to resolve the pattern handling of this, we have 4 options:
- Keep the status quo, with inconsistencies between value type slices and reference type slices in this corner case.
- Remove the null test from the DAG. If the
Slice
method violates the above expectation, the code might throw a null reference exception, depending on how the slice is used. - Perform just reachability analysis without the null test, and include the null test in codegen. Codegen will produce slightly less efficient code for reference types.
- Perform reachability analysis and codegen with a "slice and throw if null" node, which will throw a custom exception type if the above
expectation is violated. This has slightly worse codegen than 2, but better than 3. However, it cannot be worked around: an explicit
.. null
pattern would never match, as the custom exception would be thrown when anull
return fromSlice
was encountered. The only workaround would be to fall out of pattern matching entirely.
After some discussion, we were split between 2 and 4. We feel that 1, while an ok solution for this niche case, could potentially have
bad consequences long term for patterns if we want to evolve anything about slice patterns in the future. 3 is ok, but we're concerned
about the worse codegen for the 99% case here. Patterns can and do assume certain behaviors for the things they operate on: properties
might be called multiple times, or they might not be, and should return the same result across evaluations, for example. This is just
another similar expectation, this time on the Slice
method. After further discussion, we think that the inability to workaround the
checks in 4 are enough of a concern to tilt the balance between the two.
Conclusion
We will adjust our expectations around slice patterns to assume that Slice
will never return a null
value, and we will not insert a
null
test into the reachability or codegen DAGs.
Parameterless struct constructors, revisited
https://github.com/dotnet/csharplang/issues/5552
There has been some feedback on parameterless constructors in C# 10 that we want to address now, while C# 10 is still new. Specifically,
when a parameterless struct constructor is synthesized has raised concerns about silent breaks for new StructType()
when a user or
library updates their code.
In general, it's a bit unfortunate that C# allows new StructType()
syntax. default(T)
wasn't in C# 1 (it was introduced with C# 2's
generics), so that was the original way to get a default instance of a value type. We think there may be an opportunity to start correcting
this with a .NET 7 warning wave, but that will need more design, particularly around object initializers on struct types. In the meantime,
we have 3 options for addressing the concerns with the initial release of C# 10:
- No change from ship. This would mean that we stop synthesizing parameterless constructors when an explicit constructor is introduced. In particular, this has some serious community concerns about binary compat, namely that adding or removing an explicit constructor changes whether field initializers are run (changing the behavior of source and breaking binary compat). We think they're important enough to rule out this option.
- Synthesize parameterless constructors in more places. We have a good idea how to do this for regular structs: zero init, then run all
the field/property initializers. However, we don't know how to do this for record structs. Field and property initializers in record structs
can use primary constructor parameters so the synthesized constructor would need to invoke the primary constructor to run field initializers,
but using default values for primary constructor arguments may be invalid. Ultimately, we don't think the inconsistency here is any better
than option 1: there would be 3 sets of rules for when a parameterless constructor would be generated. One for reference types (always unless
there is an explicit constructor), one for regular structs (always if there are field initializers), and one for record structs (always
unless there is an explicit constructor, but you can call
new RecordStructType()
anyway). - Never synthesize a parameterless constructor. This means that the simple case of just wanting to add a field initializer isn't supported: a full constructor will have to be added. For example:
Console.WriteLine(new S().field1); // This prints 0
struct S
{
int field1 = 1; // No constructor is synthesized
int field2;
public S(int field2) => this.field2 = field2;
}
This will get easier in the future with primary constructors, making it simple to just append ()
on the end of the struct name to add
this constructor definition, but it will be unfortunate for now. Of the downsides in all of these options, this is the one we best
understand, and more importantly while it is the least ergonomic, it does fully solve the issue around accidentally removing the
parameterless constructor (which changes the behavior of source and is a binary break).
Conclusion
We will go with option 3, never synthesizing a parameterless constructor. If a struct has field initializers with no constructors, this is an error. It is a break over C# 10 as initially released, but the fix is both simple and backwards-compatible with the initial version of C# 10, so we think we're still within our ability to make this change.