csharplang/meetings/2020/LDM-2020-02-19.md
2020-03-18 15:58:44 -07:00

3.2 KiB

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

record Point { int X; int Y; }

which generates

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."