96 lines
4.7 KiB
Markdown
96 lines
4.7 KiB
Markdown
|
# 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 `throw`s 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,
|
||
|
|
||
|
```C#
|
||
|
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,
|
||
|
|
||
|
```C#
|
||
|
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.
|