C# Language Design Notes for Oct 22, 2018
Discuss two records proposals:
Nominal is more resilient to reordering or adding members. Part of the motivation here is the experience from CompilationOptions in Roslyn, which we accidentally broke a couple of times.
The classes that nominal records would replace are often mutable, just so that object initializers can work. Nominal are proposed to facilitate object initializers even for immutable ones. The trick for allowing that is a pattern that uses an underlying builder struct. The proposal would emit errors or warnings on object initializers that neglect to initialize properties which aren't declared with an initializer.
We think that object initializers are not just the result of "lazy" API design, but an initialization pattern that folks actually prefer. With many data elements it certainly has a better tooling experience. The presence of an accessible constructor with a (well recognized) builder at the end would be what allows an object initializer to work. It's a bit similar to params.
For inheritance, you would make a builder per layer of inheritance, which gets a little ugly. It might be an argument for making them unspeakable. The proposal for builders being "striped" also builds the structure of the inheritance hierarchy into the public surface area, so that changing the hierarchy would be breaking under the hood.
All alternatives that we can think of also have pretty horrendous downsides.
The proposal translates them into ref-returning getter-only properties, and sticks them in the builder. This would break reflection compared to today. VB doesn't have ref returns, so it's interesting how it deals with them when it encounters them. We need to make sure that this wouldn't degrade the VB experience.
- Open Issue: does it break anything else?
Mutable data members in classes should generally not participate in equality and hash code by default. Otherwise, there's the risk that the item gets added to a hash table, then its hash code value changes when one of the members gets mutated.
Q: does this feature buy us enough to be worth the cost? Maybe not just for compat, because most people won't rewrite old code the new way. It's the value to new code that's most important. That said, this is an important opportunity for cleanup that would help people not accidentally break themselves or their customers when they evolve.
Should we reserve the
class C(X x, Y y, Z z) syntax for primary
constructors? First idea is that
data as a modifier is necessary to make it
a positional record. But then adding data changes the meaning of the
parameter list quite radically.
"Positional" vs. "Nominal"
The positional proposal is syntactic sugar for something that people to a larger degree do today. The nominal is maybe more something that they wish they could do, but don't have the expressiveness to do yet.
The positional proposal produces With methods that are so nice it might not even be worth it to put a with expression syntax on top. For the nominal proposal, because of the builders, you really want a syntax on top, or it gets unsightly.
Summary of benefits of nominal over positional records:
- Object initializer syntax (could possibly be added to positional)
- Binary compat from version to version when you move to records (positional can't)
- Source compat
There are many components of the proposals that don't quite yet feel like they click together, but we should keep working on that.
The record syntax will probably affect discriminated union design. That doesn't mean we shouldn't keep exploring records.
Discriminated unions will also probably face the public/private API dichotomy being explored in records: if you ever want to add more cases, you break every consumer's exhaustiveness check.