csharplang/meetings/2020/LDM-2020-05-04.md
2020-05-09 17:09:46 -07:00

4.7 KiB

C# Language Design for May 4, 2020

Agenda

Design review feedback

Discussion

We had a design review on 2020-04-29 to bring our latest designs to the full review team and get feedback. Today we went over the feedback and how it would affect our design.

Final initializers

- Design review said it was very complicated, when do I use an initializer vs a constructor?

A possible fix would be to try to run initializers before constructors, instead of after. The main problem is that this is not where object initializers (using setters) run today. It would be very distasteful to have init-only setters run at a different time from regular setters, and worse to subtly run the setters at a different time just because of the presence of a different init-only field.

This is a difficult piece of feedback to reconcile, because it doesn't present a clear direction. However, we're not sure we need to finish the design for final initializers now. We still think the scenarios are useful, but there are many scenarios which don't rely on those semantics. One of the most important scenarios that we were worried about was how to copy a type that had private fields that should not be copied. One proposal was to write a final initializer which either resets certain fields, or throws if the state is invalid. Our proposed alternative for this situation is to write your own copy constructor, which sets up the appropriate state for the copy.

However, final initializers do address a significant shortfall in existing scenarios, namely that there's no way to validate a whole object in a property setter (or initter). In that sense we do have many existing issues, separate from our records designs, which would be addressable with the feature. There is also no way to validate an object after a with expression since necessarily.

Factory methods

The review team agreed about the necessity of "factory" semantics in the with expression, namely that the with expression essentially requires a virtual Clone method to work correctly through inheritance, but was not convinced that the feature was generally useful.

We're also not convinced that it's generally useful, but limiting with to only be usable on a record is a significant change from where we were before, where records are currently fully representable as regular classes.

We need to consider if we are willing to live with this limitation, or need a way of specifying the appropriate Clone method in source.

Structs as records

Can every struct be a record automatically? We don't need a Clone method, because structs already copy themselves and they already implement value equality (albeit sometimes inefficiently). If we take this stance, would we want to explicitly design records as "struct behavior for classes?" If that's true, we would seek to use the behavior of structs as a template for records.

Positional records

The feedback was negative about making a primary constructor parameters different from positional record parameters. The proposal during the design meeting was that primary constructors would see parameters as "captured" in the scope of the class, while records would generate public properties for each parameter. This is a big semantic divergence, as expressions like this.parameter would be legal in the body of a positional record, but illegal in the body of a class with a primary constructor. One way of shrinking the semantic gap would be to always generate members based on primary constructor parameters, but in regular classes those members would be private fields, while in records they would be public init-only properties. Even this semantic difference was perceived as too inconsistent.

We have two proposals to unify the behavior inside and outside of records. On one end, we could try to view primary constructors as a syntactic space to contain more elements. By default, primary constructors would be simple parameters, which could be closed over in the class body. By allowing member syntax in the parameter list, the user would have more control over the declaration. For instance,

public class Person(
    public string Name { get; init; }
);

would generate a public property named Name instead of simply a parameter and the property would be implicitly assigned in the constructor.

On the other hand, we could always make public properties, abandoning the idea of primary-constructor-parameters-as-closures. In this formulation,

class C(int X, int Y);

would generate two properties, X and Y. If this is made into a record e.g., data class C(int X, int Y), then the same record members would be synthesized as in a nominal record.

We did not settle on a conclusion, but have a rough sense that having a primary constructor always generate properties is preferred.