csharplang/meetings/2022/LDM-2022-01-03.md

6.8 KiB

C# Language Design Meeting for January 3rd, 2022

Agenda

  1. Slicing assumptions in list patterns, revisited
  2. Parameterless struct constructors, revisited

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:

  1. Keep the status quo, with inconsistencies between value type slices and reference type slices in this corner case.
  2. 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.
  3. 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.
  4. 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 a null return from Slice 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:

  1. 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.
  2. 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).
  3. 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.