82 lines
3.2 KiB
Markdown
82 lines
3.2 KiB
Markdown
|
|
# C# Language Design for Feb. 19, 2020
|
|
|
|
## Agenda
|
|
|
|
State-based Value Equality
|
|
|
|
## Discussion
|
|
|
|
Proposal: https://github.com/dotnet/csharplang/issues/3213
|
|
|
|
* We haven't decided (yet) to add support for value equality on all
|
|
classes (separate from records)
|
|
|
|
* The behavior is actually that all fields _declared_ in the class are
|
|
members in the value equality, not all fields in the class (since inherited fields are not
|
|
included)
|
|
|
|
* Inheritance would be implemented using the previously described
|
|
proposals using the `EqualityContract` property
|
|
|
|
* Records wouldn't behave differently, except that they have `value` by
|
|
default
|
|
|
|
* The main difference with how records work in other places is that the semantics
|
|
of a record is otherwise decided by the members of the primary constructor, while in this
|
|
proposal the members of the record primary constructor have no special contribution to the value
|
|
equality semantics
|
|
|
|
* There's an evolution risk where we want to provide more complex things, like deep
|
|
equality, but these features don't support enough complexity to add it. Instead, we end up just
|
|
adding more keywords or more attributes. Consider array fields. The default equality is reference
|
|
equality, but sequence equality isn't particularly rare. How would users customize that?
|
|
A new keyword? Attribute? Writing Equals manually?
|
|
|
|
* Turns out we're finding a lot of customization pivots. String comparison is another one.
|
|
If we want to support all these scenarios attributes could be better. If we could use
|
|
attributes to supply an EqualityComparer that would be almost completely customizable.
|
|
|
|
* If equality is this complicated, should we only support simple generated equality for
|
|
records? Can we leave more complicated scenarios to tooling, like source generators?
|
|
|
|
Record equality: use the "primary" members or use all fields?
|
|
|
|
* Using all the fields is consistent with how structs work
|
|
|
|
* Using the "primary" members mirrors how the generation of `With` or other things
|
|
generated by a record with a primary constructor
|
|
|
|
* There does seem to be a possibility that after you get to a certain size, positional
|
|
records are less useful. In that case we want a path to the nominal record. If we do want the
|
|
nominal path, it's generally desirable that we want as little "record" syntax as possible.
|
|
If we choose the struct "use all the fields" approach, then we could use exactly the
|
|
same mechanism for both the "nominal" and the "positional" records.
|
|
|
|
* The nominal record syntax that has been floated is
|
|
|
|
```C#
|
|
record Point { int X; int Y; }
|
|
```
|
|
|
|
which generates
|
|
|
|
```C#
|
|
record Point
|
|
{
|
|
public int X { get; init; }
|
|
public int Y { get; init; }
|
|
}
|
|
```
|
|
|
|
Aside from the shorthand for properties, this generates Equals, GetHashCode and some form
|
|
of "With", which doesn't seem much different from proposals for a separable value equality. Is
|
|
there really much point in separating these proposals?
|
|
|
|
* One completely opposite possibility: bypass the question by prohibiting private members in the
|
|
positional record entirely
|
|
|
|
**Conclusion**
|
|
|
|
No hard decisions yet. Leaning slightly towards using "all declared fields" as the metric for
|
|
value equality. There's some support for the "no private fields approach."
|