csharplang/meetings/2020/LDM-2020-02-12.md
2020-02-19 20:37:27 -08:00

4.5 KiB

C# Language Design for Feb 12, 2020

Agenda

Records

Discussion

Value equality

Proposal: use the key keyword previously mentioned, but also require it on the type declaration as well, e.g.

key class HasValueEquality
{
    public key int X { get; }
}

There are a number of things we could pivot on

key class HasValueEquality1 { public key int X { get; } }
class HasValueEquality2 { public key int X { get; } }
key class HasValueEquality3 { public key X { get; } }
class HasValueEquality4 : IEquatable<HasValueEquality4> { public int X { get; } }

record Point1(int X); // Implies value equality over X
record Point2a(int X); // Implies inherited equality
key record Point2b1(int X); // Implies value equality over X
key record Point2b2a(int X); // Implies "empty" value equality
key record Point2b2b(key int X); // Implies value equality over X


key class Point3a(int X); // implies record + value equality over X
data class Point3b(int X); // implies record with inherited equality

Equality default

We originally considered adding value equality on records both because it's difficult to implement yourself and it fits the semantics we built for records in general. We want to validate that these things are still true, and new considerations, namely whether it is the appropriate default for records and whether it should be available to other types, like regular classes.

We left off in the previous discussion asking whether value equality is not just an inconvenient default, but actively harmful for key scenarios for records. Some examples we came up with are either classes with large numbers of members, where value equality may be unnecessary and slow, and circular graphs, where using value equality could cause infinite recursion.

These do seem bad, but it's not obvious that these scenarios either fit perfectly with the canonical record, or if the consequences are necessarily worse than default reference equality. Certainly producing infinite recursion in object graphs is bad, but silently incorrect behavior due to inaccurate reference equality is also harmful, in the same sense. It's also easier to switch from value equality to reference equality than it is to switch from reference equality to value equality, due to the complex requirements in a value equality contract.

Conclusion

Value equality seems a reasonable default, as long as they are immutable by default, and that there is a reasonable way to opt-in to a different equality.

Separable value equality

Given that we like value equality as a default, we have to decide if we want a separable equality feature as well. This is important for the scenario:

record Point1(int X)
{
    public int X { get; }
}

if there's a separate key feature, we need to decide if the substituted property should require, allow, or disallow the key modifier, e.g.

record Point1(int X)
{
    public key int X { get; }
}

We also need to decide what such a "separable" equality feature would look like, and if it has a difference between records and other classes. We could add a key feature for non-records, and disallow key entirely in records. The members of a record equality would then not be customizable.

The individual key modifiers on non-records seem deceptively complicated.

A common case is "opt-in everything". key modifiers wouldn't improve much on this, as they would be necessary on every element. On the other hand, there are often computed properties that may be seen as part of "everything", but not part of the equality inputs. The plus of record primary constructors is that they identify the "core" inputs to the type.

Individual key modifiers also do not help with the large custom classes that are written today where it's easy to forget to add new members to equality. With a key modifier you can still forget to add the modifier to a new member.

These decisions play into records as a whole because they affect the uniformity of record and non-record behavior. If records are defined by their "parameters", namely in this syntax the primary constructor parameters and identically named properties, then no other members should be a part of the equality. However, that would imply members in the body are not automatically included. For regular classes, it seems backwards. Members are not generally included, they have to be added specifically.

On the other hand, if we prioritize uniformity, general members in record bodies would be included in equality, which would harm a view of records as consisting primarily of the "record inputs."