csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Champion "Records" (VS 16.8, .NET 5)

Open MadsTorgersen opened this issue 8 years ago • 277 comments

LDM notes:

  • https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-05-14.md#records-and-discriminated-unions
  • https://github.com/dotnet/csharplang/blob/master/meetings/2018/LDM-2018-10-22.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-07-22.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-15.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-29.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-02-03.md (value equality)
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-02-10.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-02-12.md
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-02-19.md (value equality)
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-02-24.md (nominal records) ...
  • https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-04-13.md#init-only-members

MadsTorgersen avatar Feb 09 '17 18:02 MadsTorgersen

See #77 regarding with expressions that are more useful than bare With methods.

orthoxerox avatar Feb 15 '17 10:02 orthoxerox

As records are considering positional pattern matching, which is actually a tuple feature, and tuples can have named members, which is actually a record feature, I think there is some overlapping between the two. How about making making seamless translations between struct records and tuples based on position, if types match? Names of the members will be ignored in these translations. Struct records will then just become named tuples I guess. Implementations are already similar.

gulshan avatar Feb 16 '17 19:02 gulshan

@gulshan Tuples are good for places where you might have used a record, but the use is not part of an API and is nothing more than aggregation of a few values. But beyond that there are significant differences between records and tuples.

Record member names are preserved at runtime; tuple member names are not. Records are nominally typed, and tuples are structurally typed. Tuples cannot have additional members added (methods, properties, operators, etc), and its elements are mutable fields, while record elements are properties (readonly by default). Records can be value types or reference types.

gafter avatar Feb 16 '17 21:02 gafter

Copying my comment on Record from roslyn-

Since almost a year has passed, I want to voice my support for the point mentioned by @MgSam -

I still see auto-generation of Equals, HashCode, is, with as being a completely separable feature from records. I think this auto-generation functionality should be enabled its own keyword or attribute.

I think the primary constructor should just generate a POCO. class Point(int X, int Y) should just be syntactical sugar for-

class Point
{
    public int X{ get; set; }
    public int Y{ get; set; }
    public Point(int X, int Y)
    {
        this.X = X;
        this.Y = Y;
    }
}

And a separate keyword like data or attribute like [Record] should implement the current immutable, sealed class with auto-generated hashcode and equality functions. The generators may come into play here. Kotlin uses this approach and I found it very helpful. Don't know whether this post even counts, as language design has been moved to another repo.

gulshan avatar Feb 22 '17 14:02 gulshan

From this recent video by Bertrand Le Roy, it seems records are being defined with a separate keyword and the primary constructor is back with shorter syntax. So far I have understood, the new primary constructor means parameters of primary constructor are also fields of the class-

class Point(int x, int y)
// is same as
class Point
{
    int x { get; }
    int y { get; }

    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

It seems the field access modifier is defult/private and to expose them separate public properties are needed like this-

class Point(int x, int y)
{
    public int X => x;
    public int Y => y;
}

I like the idea and I think there should be more discussions about these ideas here.

gulshan avatar Mar 09 '17 08:03 gulshan

We are hoping to have records defined without a separate keyword. Parameters of the primary constructor become public readonly properties of the class by default. See https://github.com/dotnet/csharplang/blob/master/proposals/records.md#record-struct-example for an example.

gafter avatar Mar 09 '17 21:03 gafter

Object(and collection, index) initializers getting constructor level privilege for initializing fields/properties can enable some interesting scenarios of complex hierarchical object initialization.

gulshan avatar Mar 13 '17 12:03 gulshan

@gulshan Can you please back up that assertion with an example? I don't see how using an object initializer instead of a constructor enables anything.

gafter avatar Mar 13 '17 15:03 gafter

I see a little problem with the proposed GetHashCode implementation in the proposal: if one of the properties is null, the object hash code will always be zero. Wouldn't it be better to simply ?? 0 individual hash codes before multiplying and summing?

miniBill avatar Mar 21 '17 10:03 miniBill

@miniBill Yes.

jnm2 avatar Mar 21 '17 13:03 jnm2

While I'm very much looking forward to the introduction of Records into the language, I really don't like the chosen syntax: public class Point(int x, int y), primarily because it precludes the possibility of ever re-adding primary constructors into the language:

public class ServiceA(ServiceB serviceB)
{
    public void DoSomething()
    {
        // use field serviceB here...
    }
}

God I miss those... I am so sick of writing dumb constructors! :-P

Isn't

public record Point
{
    int X;
    int Y;
}

A better syntax? It leaves primary constructors open but is still about as syntactically short as is possible.

Richiban avatar Mar 22 '17 11:03 Richiban

@Richiban What about https://github.com/dotnet/csharplang/blob/master/proposals/records.md#primary-constructor-body ? That looks like a primary constructor to me.

orthoxerox avatar Mar 22 '17 12:03 orthoxerox

Wouldn't it be better if primary constructors were not exclusive to records? Now, according to this proposal, primary constructors cannot be considered without adding the baggage of extra "record" tidbits. Refactoring a current class to use primary constructors(and thus record) is not a good choice then, as the behavior may change.

gulshan avatar Mar 22 '17 12:03 gulshan

@orthoxerox I guess so, although the spec doesn't mention record parameters having accessibility modifiers, so I can't write:

public class ServiceA(private ServiceB serviceB)
{
    public void DoSomething()
    {
        // use serviceB here...
    }
}

And anyway, it would be a bit of an abuse of the records feature to accomplish this. My type isn't a record at all, and I don't want structural equality.

I remember when the C# team originally dropped primary constructors they said "We don't need this anymore because we're going to have records!" but I don't understand what this has to do with records... Sure, a record is also a type with a primary constructor, but any type should be able to have a primary constructor, not just records.

For reference, here are other languages which all support both primary constructors and records:

F#:

type Greeter(name : string) =
    member this.SayHello () = printfn "Hello, %s" name

Scala:

class Greeter(name: String) {
    def SayHi() = println("Hello, " + name)
}

Kotlin:

class Greeter(val name: String) {
    fun greet() {
        println("Hello, ${name}");
    }
}

Meanwhile, in C#, we're still writing this:

public class Greeter
{
    private readonly string _name;

    public Greeter(string name)
    {
        _name = name;
    }

    public void Greet()
    {
        Console.WriteLine($"Hello, {_name}");
    }
}

Richiban avatar Mar 22 '17 12:03 Richiban

@Richiban In the primary constructor feature, you could have only used the primary constructor parameter in field and property initializers; parameters are not captured to a field automatically. You could get what you want using records in essentially the same way you would have done using primary constructors:

public class Greeter(string Name)
{
    private string Name { get; } = Name;
    public void Greet()
    {
        Console.WriteLine($"Hello, {Name}");
    }
}

gafter avatar Mar 22 '17 17:03 gafter

Why do you need the with keyword in

p = p with { X = 5 };

Wouldn't it be equally understandable when there were a .{-Operator? It would allow for more brief chaining

var r = p.{ X = 5, Y = 6 }.ToRadialCoordinates();

lachbaer avatar Mar 22 '17 17:03 lachbaer

@gafter If we go with the record definition public class Greeter(string Name) doesn't Name get lifted into a public property? That's the main reason I wouldn't want to use it for something that's not strictly a record--I don't necessarily want a type to expose its dependencies. Can I give accessibility modifiers to record fields?

Richiban avatar Mar 22 '17 17:03 Richiban

@Richiban No, if a property is explicitly written into the body by the programmer, as in my example, then the compiler does not produce one. That is described in the specification.

gafter avatar Mar 22 '17 20:03 gafter

By the way, do I understand the spec right that class records must be either sealed or abstract?

There are serious problems with allowing classes to derive types that have defined custom equality: https://richiban.uk/2016/10/26/why-records-must-be-sealed/

Richiban avatar Apr 04 '17 12:04 Richiban

If records remain the only way to get auto-generation of Equals and HashCode then I think they absolutely should not be sealed. As you yourself state in your post, doing a simple type check in the equals method solves the issue you bring up. Seems pretty Byzantine to wall off an entire use case because of the fact that developers "might" misuse a feature.

Getting structural equality right in C# is already a minefield that most sensible developers let IDE tools generate code for. Compiler autogeneration of the equality methods should be enabled for the widest net of situations possible.

MgSam avatar Apr 04 '17 14:04 MgSam

@Richiban last time I asked, the LDT planned to relax this restriction and compare runtime types in Equals.

orthoxerox avatar Apr 04 '17 14:04 orthoxerox

@orthoxerox @MgSam Yes, a runtime check is correct if you assert that the two objects have exactly the same type, not just that they have some common base type, i.e.

a.GetType() == typeof(Person) && b.GetType() == typeof(Person)

rather than

a is Person && b is Person

Also, I would like to clarify my position in that I'm not trying to prevent "developers misusing a feature" but rather pointing out that the language / runtime will not only allow a blatantly incorrect comparison between two objects of different types but could potentially return true at runtime.

Richiban avatar Apr 04 '17 14:04 Richiban

@Richiban The comparison goes something like this:

public override Type TypeTag => typeof(Foo);

public bool Equals(Foo other)
{
    if (typeof(Foo) != other.TypeTag) return false;
   ...
}

Of course, if anyone inherits from Foo and doesn't override TypeTag, they have only themselves to blame. Maybe the devs will switch to GetType(), which is slower, but works automatically.

orthoxerox avatar Apr 04 '17 16:04 orthoxerox

@Richiban We no longer restrict record types to be sealed or abstract. The following design note in the spec describes how equality is likely to work:

Design notes: Because one record type can inherit from another, and this implementation of Equals would not be symmetric in that case, it is not correct. We propose to implement equality this way:

    public bool Equals(Pair other) // for IEquatable<Pair>
    {
        return other != null && EqualityContract == other.EqualityContract &&
            Equals(First, other.First) && Equals(Second, other.Second);
    }
    protected virtual Type EqualityContract => typeof(Pair);

Derived records would override EqualityContract. The less attractive alternative is to restrict inheritance.

gafter avatar Apr 04 '17 19:04 gafter

The constructor in the abstract record class example should be protected, not public.

fubar-coder avatar May 02 '17 13:05 fubar-coder

@fubar-coder I'm curious, does that ever make a difference besides being annoying if you ever refactor to being non-abstract?

jnm2 avatar May 02 '17 14:05 jnm2

  1. The visibility changing from protected to public should happen automatically as long as you don't provide the primary constructor
  2. public on an abstract class doesn't make sense, because you cannot instantiate this class using this publicly visible constructor

fubar-coder avatar May 02 '17 14:05 fubar-coder

public on an abstract class doesn't make sense, because you cannot instantiate this class using this publicly visible constructor

Sure it does, as much sense as public members on an internal class. public never overrides other visibility restrictions. It just indicates that there are no additional restrictions being imposed. public on a abstract class's constructor means "there's nothing special about this member, it just follows the visibility rules of the containing class." The fact that the class is abstract imposes a visibility restriction on the constructor already so in that sense it's redundant to specify protected unless you're trying to encode an extra bit of information that the constructor would still be protected even if the class were not abstract.

jnm2 avatar May 02 '17 16:05 jnm2

A public constructor for an abstract class is for all intents and purposes protected. It can't be called except by a derived class. Public members are not necessarily pointless on internal classes; they can implement interfaces. Constructors, however, are never part of an interface declaration and will never be publicly accessible.

In my opinion, if only for reflection, the abstract class constructor should be protected. There is literally no point in making them public.

GeirGrusom avatar May 03 '17 11:05 GeirGrusom

We could make a special rule saying that in an abstract class the compiler-generated constructor is protected instead of public. But there would be literally no point in making that rule or implementing it in the compiler.

gafter avatar May 03 '17 20:05 gafter