csharplang/meetings/2020/LDM-2020-05-11.md
2020-05-19 00:54:16 -07:00

4 KiB

C# Language Design Meeting for May 11, 2020

Agenda

Records

Discussion

Today we tried to resolve some of the biggest questions about records, namely how to unify the semantics of nominal records and positional records, and what are the key scenarios that we are trying to resolve with records.

The main inconsistency is that the members record class Person(string FirstName, string LastName) are very different from the members in class Person(string firstName, string lastName). One way of resolving this is to unify the meaning of the declaration in the direction of primary constructors. In this variant, the parameters of a primary constructor always capture items by default.

To produce public init-only properties like we were exploring, we would require an extra keyword, data, that could be generalizable. So a record which has two public init-only members would be written

record Person(data string FirstName, data string LastName);

This would allow a generalizable data keyword that could be applied even in regular classes, e.g.

class Person
{
    data string FirstName;
    data string LastName;
    public Person(string first, string last)
    {

    }
}

The worry here is that we're harming an essential motivation for records, namely a short syntax for immutable data types. In the above syntax, record alone does not mean immutable data type, but instead only value equality and non-destructive mutation. A problem with this is that value equality is dangerous for mutable classes, since the hash code can change after being added to a dictionary. This was why we were previously cautious about adding a general feature for value equality. One option to discourage misuse would be to provide a warning for any non-immutable member in a record class.

The other problem is, frankly, it's not that short. Aside from some duplication of intent by requiring both record and data modifiers, it also requires applying the data modifier to each member, so the overhead grows larger as the type does.

Alternatively, we could go in the complete opposite direction: limit customizability by making records all about public immutable data.

For instance, nominal records could also have syntax abbreviation

record Person { string FirstName; string LastName; }

and we could avoid confusion by prohibiting other members entirely.

This would look at lot more like positional records, e.g.

record Person(string FirstName, string LastName);

and we could introduce further restrictions on those by also disallowing other members in the body, or even disallowing primary constructors entirely.

Disallowing all members inside of records is draconian, but not entirely without precedence. Enums work the same way in C# and members are added via extensions methods. That's not a ringing endorsement since we've considered proposals for allowing members in enums before, but it also doesn't put it outside the realm of possibility for C#, especially in earlier forms.

The main drawback of the simplest form is the risk that we might have trouble evolving the feature to fit all circumstances. If we wanted to allow a user to define private fields, the syntax with no accessibility modifier now means "public init-only property" so we might not be able to add support for private fields at all, or we might have to use a syntactic distinction that requires a private accessibility, which is a subtle change.

Conclusion

We largely prefer the short syntax for records. A nominal record would look like

record class Person { string FirstName; string LastName; }

This would create a class with public init-only properties named FirstName and LastName, along with equality and non-destructive mutation methods.

Similarly,

record class Person(string FirstName, string LastName);

would create a class with all of the above, but also a constructor and Deconstruct.

We have yet to confirm whether record disallows private fields entirely, or if it just changes the default accessibility.