dotnet / csharplang

The official repo for the design of the C# programming language
11.38k stars 1.02k forks source link

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

Open MadsTorgersen opened 7 years ago

MadsTorgersen commented 7 years ago

LDM notes:

orthoxerox commented 7 years ago

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

gulshan commented 7 years ago

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.

gafter commented 7 years ago

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

gulshan commented 7 years ago

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 commented 7 years ago

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.

gafter commented 7 years ago

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.

gulshan commented 7 years ago

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

gafter commented 7 years ago

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

miniBill commented 7 years ago

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?

jnm2 commented 7 years ago

@miniBill Yes.

Richiban commented 7 years ago

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.

orthoxerox commented 7 years ago

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

gulshan commented 7 years ago

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.

Richiban commented 7 years ago

@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}");
    }
}
gafter commented 7 years ago

@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}");
    }
}
lachbaer commented 7 years ago

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();
Richiban commented 7 years ago

@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?

gafter commented 7 years ago

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

Richiban commented 7 years ago

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/

MgSam commented 7 years ago

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.

orthoxerox commented 7 years ago

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

Richiban commented 7 years ago

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

orthoxerox commented 7 years ago

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

gafter commented 7 years ago

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

fubar-coder commented 7 years ago

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

jnm2 commented 7 years ago

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

fubar-coder commented 7 years ago
  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
jnm2 commented 7 years ago

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.

GeirGrusom commented 7 years ago

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.

gafter commented 7 years ago

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.

lachbaer commented 7 years ago

I just wrote over in https://github.com/dotnet/roslyn/issues/10154#issuecomment-300611986 but it seems to be more appropriate here. So I'm gonna copy my comment.

My 5 cents: parts of the record type could be automatically implemented by interfaces, if requested by the user with the auto keyword. Otherwise it is a POCO, with only the auto-properties and constructor.

class Point(int X, int Y) : auto IWithable, auto IEquatable<Point> {}

The standard object method overrides should always be created automatically, as should the operator is.

The advantage is that you can incrementally add functionality to the record types when the compiler and framework evolves. The record types are downward compatible, because the user can cast them to the necessary interface.

To ease with the available interfaces, there can be a summarizing interface, like 'IRecordTypeBase<>' that implements basic functionality as suggested currently, and 'IRecordTypeNet50<>' for additional functionality provided by .Net 5.0 and deriving 'IRecordTypeBase<>'.

gafter commented 7 years ago

I can't imagine what methods would be in IWithable. If it is empty, then : auto IWithable is an awfully strange syntax to opt in to the compiler generating something that has nothing to do with this interface.

lachbaer commented 7 years ago

I can't imagine what methods would be in IWithable

The With(...) methods. Obviously the concrete signature for the methods of that interface is missing. Well, it's a cause for thought. 😇

-- Synapse explosion 😝 - because the with keyword will probably be introduced with the emerge of record types, this keyword could be seen as a pseudo interface.

class Point(int X, int Y) : auto with, auto IEquatable<Point> {}

Without it no With(...) methods for this class are produced

With auto interfaces you leave it to the user how much functionality is synthesized and keep it open for future extensions, when some come to your mind 😊

lachbaer commented 7 years ago

Or to create a POCO, opt-out:

explicit class Point(int X, int Y) {}
class Point(int X, int Y) : explicit {}

(object overrides and c'tor should of course always be created)

jhickson commented 7 years ago

We would find record types very useful in implementing strong types (for want of a better name) to represent specific domain data types such as identifiers, names, etc. For instance, we would use a Port type that has an int data member rather than a bare int to represent a port for a socket. So we have a lot of types (usually structs) that:

Code snippets help the generation of new types of this kind but language support would obviously be of great benefit and records seem to mostly fit that bill. We don't see these strong types as records but the record syntax proposed here would help save a lot of boilerplate code particularly if they implement IEquatable<> automatically.

That said, as we tend to not directly expose the wrapped value - it's present as a private data member only. So we would find very useful if not only were we able to control the access modifier of the generated property. I realise it has been proposed to allow this by explicitly implementing the property in question, but it would be good to have a more straightforward means of doing that. So, rather than writing

public struct Port(int Value)
{
    private int Value { get; }
}

it would be good to be able to write something like the following instead

public struct Port(private int Value);

Is that a viable proposal?

It would also be good to be able to easily specify that if you have only one data member then explicit cast operators should be auto-generated as well, but I'm afraid I'm a loss as to how to express that cleanly, and can see that special-casing like this wouldn't make for a nice language feature.

Richiban commented 7 years ago

@jhickson

public struct Port(private int Value);

I'd love this, but I'm afraid I've asked for it before and I think the decision has been made.

As for casting, I think that people's expectations are a little too divergent to auto-generate this behaviour. Some won't like casting, some will be happy to have both casts, some will want a cast that only goes one way, and some will want a cast that goes implicitly one way but explicitly the other.

jhickson commented 7 years ago

@Richiban Somehow I missed you proposing that further up. I'm not sure how.

I can see proposing auto-generation of casting could open a can of worms, and I can also see it probably wouldn't be desirable anyway given it would only be applicable to a subset of record types (i.e. only those with one member).

Richiban commented 7 years ago

@jhickson

Somehow I missed you proposing that further up. I'm not sure how.

Don't worry, it's not on this thread!

gafter commented 7 years ago

@jhickson I'd rather get records of any kind sooner and then add this extension later. Adding it later would be completely upward compatible.

jhickson commented 7 years ago

@gafter I completely understand that. I'll have to make sure I'm earlier to the party when any extensions are proposed. Thanks.

gordanr commented 7 years ago

@jhickson Record types will really be useful for implementing strong domain types. I am particularly interested in some other extension. How to ensure that created domain object always satisfy some invariants (created object is always in valid state).

jhickson commented 7 years ago

@gordanr I agree: a big advantage of strong types is being able to specify constraints. I believe the proposal allows for the explicit definition of the primary constructor body though, and possibly that would be sufficient.

louthy commented 7 years ago

For anybody who really wants record like functionality but doesn't want to wait, then I have built a pretty slick solution in language-ext:

Record<A>

Simply derive your type from Record<A>, where A is the type you're defining:

    public sealed class TestClass : Record<TestClass>
    {
        public readonly int W;
        public readonly string X;
        public readonly Guid Y;
        public AnotherType Z { get; set; }

        public TestClass(int w, string x, Guid y, AnotherType z)
        {
            W = w;
            X = x;
            Y = y;
            Z = z;
        }
    }

This gives you Equals, IEquatable.Equals, IComparable.CompareTo, GetHashCode, operator==, operator!=, operator >, operator >=, operator <, and operator <= implemented by default. As well as a default ToString implementation and an ISerializable implementation. Equality operations are symmetric.

Note that only fields or field backed properties are used in the structural comparisons and hash-code building. So if you want to use properties then they must not have any code in their getters or setters.

No reflection is used to achieve this result, the Record type builds the IL directly, and so it's as efficient as writing the code by hand.

There are some unit tests to see this in action.

Opting out

It's possible to opt various fields and properties out of the default behaviours using the following attributes:

For example, here's a record type that opts out of various default behaviours:

    public class TestClass2 : Record<TestClass2>
    {
        [OptOutOfEq]
        public readonly int X;

        [OptOutOfHashCode]
        public readonly string Y;

        [OptOutOfToString]
        public readonly Guid Z;

        public TestClass2(int x, string y, Guid z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }

And some unit tests showing the result:

public void OptOutOfEqTest()
{
    var x = new TestClass2(1, "Hello", Guid.Empty);
    var y = new TestClass2(1, "Hello", Guid.Empty);
    var z = new TestClass2(2, "Hello", Guid.Empty);

    Assert.True(x == y);
    Assert.True(x == z);
}

RecordType<A>

You can also use the 'toolkit' that Record<A> uses to build this functionality in your own bespoke types (perhaps if you want to use this for struct comparisons or if you can't derive directly from Record<A>, or maybe you just want some of the functionality for ad-hoc behaviour):

The toolkit is composed of seven functions:

    RecordType<A>.Hash(record);

This will provide the hash-code for the record of type A provided. It can be used for your default GetHashCode() implementation.

    RecordType<A>.Equality(record, obj);

This provides structural equality with the record of type A and the record of type object. The types must match for the equality to pass. It can be used for your default Equals(object) implementation.

    RecordType<A>.EqualityTyped(record1, record2);

This provides structural equality with the record of type A and another record of type A. It can be used for your default Equals(a, b) method for the IEquatable<A> implementation.

    RecordType<A>.Compare(record1, record2);

This provides a structural ordering comparison with the record of type A and another record the record of type A. It can be used for your default CompareTo(a, b) method for the IComparable<A> implementation.

    RecordType<A>.ToString(record);

A default ToString provider.

    RecordType<A>.SetObjectData(record, serializationInfo);

Populates the fields of the record from the SerializationInfo structure provided.

    RecordType<A>.GetObjectData(record, serializationInfo);

Populates the SerializationInfo structure from the fields of the record.

Below is the toolkit in use, it's used to build a struct type that has structural equality, ordering, and hash-code implementation.

    public class TestStruct : IEquatable<TestStruct>, IComparable<TestStruct>, ISerializable
    {
        public readonly int X;
        public readonly string Y;
        public readonly Guid Z;

        public TestStruct(int x, string y, Guid z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        TestStruct(SerializationInfo info, StreamingContext context) =>
            RecordType<TestStruct>.SetObjectData(this, info);

        public void GetObjectData(SerializationInfo info, StreamingContext context) =>
            RecordType<TestStruct>.GetObjectData(this, info);

        public override int GetHashCode() =>
            RecordType<TestStruct>.Hash(this);

        public override bool Equals(object obj) =>
            RecordType<TestStruct>.Equality(this, obj);

        public int CompareTo(TestStruct other) =>
            RecordType<TestStruct>.Compare(this, other);

        public bool Equals(TestStruct other) =>
            RecordType<TestStruct>.EqualityTyped(this, other);
    }

I realise this isn't strictly C# language talk; but I figured this was such a big pain point for most C# devs that you all wouldn't mind :)

darcythomas commented 7 years ago

I like to write classes which are either just functional services (no state) or just hold data (all state, no functionality). It would be nice to be able to have my generic functions only accept types of T where T is a Record type. So if Record types either inherit from a Record abstract class or implement an interface (IWithable?) that would help with that.

alrz commented 6 years ago

After pattern-matching this is not much of a surprise.

http://cr.openjdk.java.net/~briangoetz/amber/datum.html (check parent dir for more)

orthoxerox commented 6 years ago

@alrz to paraphrase Picasso, "good language designers borrow..."

Joe4evr commented 6 years ago

Just had a thought about the Primary Constructor part of this: Since a dev is able to specify multiple constructors anyway (especially given the example of versioning a type, where the previous PC gets "demoted" to a hand-written one so back-compat remains maintained), is it too much effort to have the compiler put a generated marker attribute on the generated Primary Constructor?

The idea here is that an API consumer would get that specific constructor suggested first by IntelliSense after new RecordType(, rather than the traditionally top one (which I think is by number of parameters in the first pass).

GeirGrusom commented 6 years ago

Would it make sense to implement == and != for record structs?

AustinBryan commented 6 years ago

@Richiban You deliberately took the longest syntax to write that your example.

F#:

type Greeter(name : string) =
member this.SayHello () = printfn "Hello, %s" 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}");
}

}

If that bothers you, you can write this:

```csharp
class Greeter {
    readonly string _name;
    public Greeter(string name) => _name = name;
    public void Greet() => Console.WriteLine($"Hello, {_name}");
}

That went from 14 lines to only 5, which is only one more than Kotlin, two more than Scala and three more than F#.

we're still

I don't know about most C# programmers, but I for one use expression bodies whenever I can.

Richiban commented 6 years ago

@AustinBryan

You deliberately took the longest syntax to write that your example.

I think you've misunderstood my complaint. Yes, of course, you can make the code physically shorter by using expression-bodied members, but that's not the point. The point is that I'm forced to write declarations and statements that I shouldn't, because it's such a common scenario that there should be a shorthand for it. Even in your shorter example where we have a class with a single dependency called "name" of type string, you've still had to write the word "name" four times. Every single dependency requires 1. a field declaration 2. a constructor argument declaration and 3. an assignment from one to the other. This is frustrating and, honestly, quite indefensible. Using your shorter version I want to be able to write:

class Greeter(string name) {
    public void Greet() => Console.WriteLine($"Hello, {name}");
}

Using expression-bodied constructors also falls down as soon as there's more than one field (which would be 80% of the time), unless you're advocating for:

class MyService
{
    private readonly IServiceA _serviceA;
    private readonly IServiceB _serviceB;
    private readonly IServiceC _serviceC;

    public MyService(IServiceA serviceA, IServiceB serviceB, IServiceC serviceC) =>
      (_serviceA, _serviceB, _serviceC) = (serviceA, serviceB, serviceC);

    // Actual methods go here...
}

Not easy to read, which means it's a breeding ground for bugs.

In these examples the constructor is pure boiler plate; it serves no purpose for the actual behaviour of the class and merely introduces noise that distracts from the class's functionality.

Note: I'm (of course) not advocating that we remove the tried and true constructor syntax we already have from the language. If you want to do argument validation or run some code in the constructor then the full syntax will always be available to you.

I'm merely arguing that we need a syntax to support this very common scenario where a constructor exists for the sole purpose of setting fields. A way of declaring "This is my class and these are its dependencies", if you will.