dotnet / csharplang

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

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

Open MadsTorgersen opened 7 years ago

MadsTorgersen commented 7 years ago

LDM notes:

lcsondes commented 4 years ago

Not sure if that "already" will make it into the final one if it's changing this fast.

Happypig375 commented 4 years ago

@lcsondes What is changing fast is the syntax and future features but already implemented features are unlikely to be removed.

carlreinke commented 4 years ago

It seems to me that (given init and with) you could get most of the benefits of records using an attribute and a source generator rather than introducing new concepts and syntax into the language.

orthoxerox commented 4 years ago

@carlreinke I think the idea is to have one standard implementation, otherwise records from different libraries could have subtly different semantics.

Richiban commented 4 years ago

@carlreinke

It seems to me that (given init and with) you could get most of the benefits of records using an attribute and a source generator rather than introducing new concepts and syntax into the language.

While it's true that a code generator could take care of the boilerplate for you it's not going to be able to do anything to make up for the lack of with-expressions.

Also, the fact that records are a language feature means that gains in efficiency can be made.

tooloudwind commented 4 years ago

Instead of using data class, why not just use readonly class? So that it does not introduce any new keyword and remain consistency with other parts of language.

Totally agree! In addition, I really wish C# stop adding complexity that only brings minor advantages. Please, think about readability and simplicity.

Happypig375 commented 4 years ago

@tooloudwind Because they mean different things, duh

tooloudwind commented 4 years ago

@tooloudwind Because they mean different things, duh

@Happypig375 , don't you think that it brings confusion for guys who new to C#? If it is completely different things, why not use a simpler keyword like 'record' instead of 'data class'? Imagine how much effort you guys would need to explain those different yet similar and overlapping concepts/keywords. Sigh.

CyrusNajmabadi commented 4 years ago

@tooloudwind First, syntax hasn't been decided. Second, we are planning on using record.

A core issue here is that prototyping and churning designs is a lot of work. So we want to get something out that is easy to make the compiler support and easy to consume so we can validate things. We can then use that to drive final syntax.

Please keep that in mind in terms of your critiques.

CyrusNajmabadi commented 4 years ago

@Happypig375 , don't you think that it brings confusion for guys who new to C#?

C# is a large language with lots to learn. I don't see how readonly class is any clearer than data class especially since readonly class doesn't actually convey anything about that type. i.e. it's not actually readonly.

tooloudwind commented 4 years ago

@CyrusNajmabadi , ok maybe I'm too rush. I'll wait and see how it goes before I write more comment. I appreciate you guys' hard work, but I also wish C# keep its simplicity and don't scare people and make them leave C#. I can almost imagine that in the future, some team will have a guideline that forbid developers to use some syntax. We'll see.

CyrusNajmabadi commented 4 years ago

but I also wish C# keep its simplicity and don't scare people and make them leave C#. ... I can almost imagine that in the future, some team will have a guideline that forbid developers to use some syntax. We'll see.

A couple of points:

  1. that feedback is something we hear with every language feature we add. Every time we add something there are always people who believe it makes C# too complex and who will not use it. It's part and parcel of making any changes to any language.
  2. records are being created primarily to actually make things simpler. i.e. to make it so that instead of having to write 50+ lines of code to express a common concept, you can now do the same with just a handful of lines.

As such, simplicity is in hte eye of the beholder. Some people will appreciate the simplicity this brings to their codebases and how they can express a lot more with a lot less code. Some people will not appreciate that now the language got larger and more complex and will involve understanding and learning more. C'est la vie with langauge evolution.

tooloudwind commented 4 years ago

@CyrusNajmabadi , thank you for taking time and writing those points. I don't know others, but I always welcome C# new features and I've never complained or doubted about them, until C# 8 NRT, which I'm still not sure whether I'll use it or not. But now seeing C# 9 new features, I started worry the future of C#.

p.s. I use C# since v1. In my early career I've used 80x86 assembly, C/C++, Object Pascal (Delphi).

Joe4evr commented 4 years ago

until C# 8 NRT, which I'm still not sure whether I'll use it or not

Hint: You really should! 😉

gen-xu commented 4 years ago

but I also wish C# keep its simplicity and don't scare people and make them leave C#. ... I can almost imagine that in the future, some team will have a guideline that forbid developers to use some syntax. We'll see.

A couple of points:

  1. that feedback is something we hear with every language feature we add. Every time we add something there are always people who believe it makes C# too complex and who will not use it. It's part and parcel of making any changes to any language.
  2. records are being created primarily to actually make things simpler. i.e. to make it so that instead of having to write 50+ lines of code to express a common concept, you can now do the same with just a handful of lines.

As such, simplicity is in hte eye of the beholder. Some people will appreciate the simplicity this brings to their codebases and how they can express a lot more with a lot less code. Some people will not appreciate that now the language got larger and more complex and will involve understanding and learning more. C'est la vie with langauge evolution.

Thanks for you time and reply to this. I have some questions about this new feature. Isn't the record just another way to say immutable types? If that's case then I couldn't think nothing better than just using readonly class.

At least from what I have seen from here

public data class Person(string FirstName, string LastName);

will be rewritten to

public class Person
{
    [CompilerGenerated]
    private readonly string <FirstName>k__BackingField;

    [CompilerGenerated]
    private readonly string <LastName>k__BackingField;

    public string FirstName
    {
        get
        {
            return <FirstName>k__BackingField;
        }
    }

    public string LastName
    {
        get
        {
            return <LastName>k__BackingField;
        }
    }

    public bool Equals(Person P_0)
    {
        if (P_0 != null && EqualityComparer<string>.Default.Equals(<FirstName>k__BackingField, P_0.<FirstName>k__BackingField))
        {
            return EqualityComparer<string>.Default.Equals(<LastName>k__BackingField, P_0.<LastName>k__BackingField);
        }
        return false;
    }

    public override bool Equals(object P_0)
    {
        return Equals(P_0 as Person);
    }

    public override int GetHashCode()
    {
        return 0;
    }

    public Person(string FirstName, string LastName)
    {
        <FirstName>k__BackingField = FirstName;
        <LastName>k__BackingField = LastName;
    }
}

I don't see why readonly class couldn't be used in this case.

I feel we stick to the word Record too much but forget what we really want is just a lightweight way to write immutable type. I agree with you that the complexity of the language will increase in order for the language to evolve, which is inevitable. I also believe most of us want the immutable type feature, but we are just concerning the current design/implement of this feature may introduce extra complexity that is avoidable.

HaloFour commented 4 years ago

@xgstation

I feel we stick to the word Record too much but forget what we really want is just a lightweight way to write immutable type.

But it's not just an immutable type. It's a type with value identity. You're also missing the auto-generated Deconstruct method and the auto-generated Clone method.

It's even possible that records won't be strictly immutable. Records in F# allow for mutable members, so do case classes in Scala. So it doesn't make sense to conflate the goals of "records" with immutability.

gen-xu commented 4 years ago

@xgstation

I feel we stick to the word Record too much but forget what we really want is just a lightweight way to write immutable type.

But it's not just an immutable type. It's a type with value identity. You're also missing the auto-generated Deconstruct method and the auto-generated Clone method.

It's even possible that records won't be strictly immutable. Records in F# allow for mutable members, so do case classes in Scala. So it doesn't make sense to conflate the goals of "records" with immutability.

Thanks for your reply and that makes more sense. If that's the case, do you have plan on readonly record class as well as the readonly record struct?

CyrusNajmabadi commented 4 years ago

If that's the case, do you have plan on readonly record class as well as the readonly record struct?

There are no plans for that currently. And it's really important to understand that readonly in .net/c# does not Immutable. I personally really really really really want truly immutable types. But we're far away from taht, and a lot more work will need to be done to get us there (esp. if we don't want to have to reinvent the entire BCL).

lcsondes commented 4 years ago

@CyrusNajmabadi in particular it would be great if records could contain and return arrays without copying them and not needing to worry that whoever gets the array reference is completely free to write into it.

HaloFour commented 4 years ago

@lcsondes

in particular it would be great if records could contain and return arrays without copying them and not needing to worry that whoever gets the array reference is completely free to write into it.

Given records have nothing to do with immutability (shallow or deep) that's not something that they will enable. There's no facility in the runtime to pass an array as an array while preventing writes. Best you can do is to use something like Array.AsReadOnly to wrap the array in a ReadOnlyCollection<T> wrapper.

lcsondes commented 4 years ago

@HaloFour I'm aware that this is not part of records, but if this happened I feel it would be a useful combination with records.

333fred commented 4 years ago

HaloFour I'm aware that this is not part of records, but if this happened I feel it would be a useful combination with records.

To be frank, it won't happen. Far too little time left to design such a thing, and it's not really related to records as a concept.

lcsondes commented 4 years ago

@333fred definitely not now. But if it does in C# >=10 I hope it will nicely work well with records to enhance them this way. That's all. The lack of a const T[] has been a sore point for me for ages having come from C++.

Alxandr commented 4 years ago

@lcsondes You have ReadOnlyMemory<T> which basically gives you the same thing.

lcsondes commented 4 years ago

@Alxandr yup that's the workaround I sometimes end up using, but it comes with heavy drawbacks. It's not IEnumerable<T>.

Alxandr commented 4 years ago

That should be trivial to fix with an extension method though. It also has the added benefit of being sliceable.

lcsondes commented 4 years ago

@Alxandr you lose the efficient iteration over them though in that case. Anyway that's a different topic.

zivkan commented 4 years ago

I'm sorry if this has already been discussed, but unfortunately I haven't been able to find it. Also, I understand the feature is still under development, so things are prone to change and no guarantees can be made.

I was wondering if record types are likely to be binary compatible with classes?

For example, I'm designing an API that I need to ship ASAP, but the long term ideal API would be something like:

public Something GetSomething();

record class Something { string A; string B }

Is there a way I can design my API today to maximise the chance that it will be ABI compatible with record types when it comes out?

public Something GetSomthing();

public class Something
{
  public string A { get; }
  public string B { get; }
  public Something(string a, string b) { A = a; B = b; }
}

Although I imagine that the two parameter Something constructor may not work when the class becomes a record, particularly if the type adds extra properties in the future?

Perhaps using a factory method until the language feature is complete is a workaround?

public Something GetSomthing();

public class Something
{
  public string A { get; }
  public string B { get; }
  internal Something(string a, string b) { A = a; B = b; }
}

public static class SomethingFactory
{
  public static Something Create(string a, string b) => new Something(a, b);
}

The factory would become obsolete once the language feature comes out, but can remain for ABI compatibility.

Or is the current design of record types such that the IL will not be compatible, so a record class could never replace a current class?

lrhoulemarde commented 4 years ago

Admittedly, I'm struggling with the concept; the first thing that come to mind is that after decades, the language is not powerful enough and after all this time we need a new keyword. My first thought is that are we really just being forced to align with other language concepts, maybe for interoperability?

The main "plus" seems to be record generates comparison code automatically like another language. The issue I have with is I have now have cognitive load now of precisely knowing under what conditions auto-generated code will work and when I have to override. In particular, IEqualityComparer<T> is not a whole lot of code to implement, so I'd probably disallow records in production codebases on this alone.

It really feels like recordshould be an library-provided interface or class using the new default interface methods, or maybe an autogenerated class by tooling. Perhaps we just simply need some way of declaring (de)constructability? But 50 lines of code or less is not worth a keyword in C#.

Here's when I would think record should be: it tells the compiler that the data will be specially stored using some CLR magic so that access is an order of magnitude faster--and because of that it has tons of limitations on the properties and members. That is, record is a low-level, programmer-inaccessible optimization for ultra fast data record access. OK, I'd get that and the need for a language feature and hence a new keyword. But this is not the case.

CyrusNajmabadi commented 4 years ago

the first thing that come to mind is that after decades, the language is not powerful enough and after all this time we need a new keyword.

The language commonly adopts new features to make things that were possible before, much less verbose and unpleasant. This is one of those cases.

It really feels like record should be an library-provided interface

How would that work?

or maybe an autogenerated class by tooling.

This was considered, and will still be possible using Source-Generators. We discussed this at length, but in the end decided that hte latter would be useful for a long tail of customers that need different things. Whereas we wanted one known 'blessed' form that everyone could use and recognize across all codebases with a very clear set of semantics around it.

Perhaps we just simply need some way of declaring (de)constructability?

Yes. that's what this is. It's unclear what you're asking for. It sounds like you want some way to 'declare' that something has the behavior of a record, you just take issue with the way we've chosen to to that?

Here's when I would think record should be: it tells the compiler that the data will be specially stored using some CLR magic so that access is an order of magnitude faster

Sure. Feel free to create a proposal on that. If you can make such a thing, i would be very intrigued and interested. I am truly interested how that would work.

mkane91301 commented 4 years ago

I'm not sure if this is a language design issue or an implementation issue for the Roslyn team, but in the latest .NET and VS previews:

internal record Foo { internal int Bar { get; init; } }

var foo1 = new Foo { Bar = 1 };
var foo2 = new Foo { Bar = 1 };

Console.WriteLine(foo1.Equals(foo2));
Console.WriteLine(EqualityComparer<Foo>.Default.Equals(foo1, foo2));
Console.WriteLine(foo1 == foo2);

prints:

true
true
false

and if you look at the decompiled type, you'll see that there is no overloading of == and !=.

HaloFour commented 4 years ago

@mkane91301

See:

https://github.com/dotnet/csharplang/issues/3707#issuecomment-661800278 https://github.com/dotnet/csharplang/issues/3707#issuecomment-662015261

So hopefully we'll know in a couple of days.

Serentty commented 4 years ago

I said this here, but I don't think that this feature can be implemented with reasonable defaults until must-init properties land.

mikewodarczyk commented 4 years ago

While the abbreviated for is convenient:

   public record Person(string FirstName, string LastName);

Does using this mean that I can no longer override methods? I guess that I can use extension methods to add new methods, but I find that I commonly want to override "ToString()". Am I forced into the verbose syntax to override ToString()?

333fred commented 4 years ago

You can have braces and a body like normal.

public record Person(string FirstName, string LastName)
{
    public override string ToString() => "My fancy impl";
}
mikewodarczyk commented 4 years ago

I want to combine using nullable, records, and EntityFramework Core. In the current c# 9 preview, the compiler adds a copy-constructor with an unnamed parameter. This causes EntityFramework Core to throw an exception trying to bind to that generated constructor. I can work around this by adding my own copy constructor, but I end up writing a lot of boiler-plate code. My code ends up looking like this:

 #nullable enable
 namespace MyNamespace {
      public record  Person {
            public int Id {get; init;}
            public string FirstName {get; init;}
            public string LastName {get; init;}

            // needed for nullable constraint to be satisfied.
            public Person(int id, string firstName, string lastName) {
                Id = id;
                FirstName = firstName;
                LastName = lastName;
            }

            // this copy constructor required by entity framework core.
            // Without this, EF core hits the unnamed parameter in the generated copy constructor and
            // it throws an exception.
            public Person( Person other) {
                   Id = other.Id;
                   FirstName = other.FirstName;
                   LastName = other.LastName;
            }
      }
 }

I think that I could reduce all of this down to the abbreviated code:

  public record Person(int Id, string FirstName, string LastName);

But for this to work with EF core, the generated copy constructor would need a named property instead of an unnamed one. I guess technically this is an EF core bug, but if c# 9 records named this property, then EF core seems to work out of the box with records.

Now added to EF Core issue list: https://github.com/dotnet/efcore/issues/21844

CyrusNajmabadi commented 4 years ago

I guess technically this is an EF core bug,

Sounds like it.

but if c# 9 records named this property, then EF core

We don't really design the language to work around bugs in other systems that are fixable.

jnm2 commented 4 years ago

@mikewodarczyk Please file a bug in the EF Core repo so this can be fixed before C# 9 is released. ParameterInfo.Name is documented to sometimes return null and it's on the caller to handle null gracefully.

svick commented 4 years ago

@mikewodarczyk

In the current c# 9 preview, the compiler adds a copy-constructor with an unnamed parameter.

That was recently changed in https://github.com/dotnet/roslyn/pull/46069, so I expect this will work by the next preview. (Though filing an EF Core issue might still be worth it, since, as @jnm2 mentioned, missing parameter names are valid in .Net.)

333fred commented 4 years ago

Closing as already done.

Reading proposals too fast, I thought this was a separate issue. 😅

andre-ss6 commented 4 years ago

Out of curiosity, why does the compiler does this though? Why use an unnamed parameter?

HaloFour commented 4 years ago

@andre-ss6

The compiler is emitting that parameter name now so the issue is likely to go away. From what I can tell in SharpLab the parameter name for the copy constructor is original.

jnicholes commented 4 years ago

I think I may have stumbled into unintended behavior of init-only properties. The following code sample demonstrates that an init-only property can be modified after initialization by casting the containing object as dynamic. It also demonstrates that this behavior is not the same for private setters or read only properties. Is this intended? I would have expected the code to compile but I would also have expected a runtime error similar to both the read only property and the private setter property.

My Environment: VisualStudio: VisualStudio 2019 Community 2019 Preview 16.7.0 Preview 5.0 dotnet --version: 5.0.100-preview.6.20318.15

 class Program
    {
        public class ThingClass
        {
            public string Name { get; init; }
        }

        public record ThingRecord(string Name);

        public class PrivateSetterThing
        {
            public PrivateSetterThing(string name)
            {
                SetName(name);
            }

            public string Name { get; private set; }

            public void SetName(string name)
            {
                this.Name = name;
            }
        }

        public class GetOnlyThing
        {
            public string Name { get; }

            public GetOnlyThing(string name)
            {
                Name = name;
            }
        }

        static void Main()
        {
            // It is possible to reassign init only properties by casting the class as dynamic
            ThingClass thing = new ThingClass { Name = "ThingClass" };
            Console.WriteLine(thing.Name); // prints ThingClass

            // Correctly fails to build due to error CS8852
            //thing.Name = "Susan";

            // Casting as dynamic and then assigning works without error.
            // This code compiles and runs and permits modification of the property
            ((dynamic)thing).Name = "Changed Dynamically";
            Console.WriteLine(thing.Name); // prints Changed Dynamically

            ThingRecord thingRecord = new ThingRecord("Test ThingRecord");
            // Correctly fails to build due to error CS8852
            // thingRecord.Name = "Changed";

            // Casting as dynamic and then assigning works without error.
            // this code compiles and runs and permits setting the property.
            ((dynamic)thingRecord).Name = "Changed Dynamically";
            Console.WriteLine(thingRecord.Name); // prints "Changed Dynamically"

            // this example shows that the traditional private setter approach does not permit this behavior
            PrivateSetterThing traditional = new PrivateSetterThing("PrivateSetterThing");
            //Fails to compile due to CS0272
            //traditional.Name = "Changed";

            // this compiles but throws A RuntimeBinderException at runtime
            ((dynamic)traditional).Name = "Changed Dynamically";
            Console.WriteLine(traditional.Name);

            // with a get only property
            GetOnlyThing getOnlyThing = new GetOnlyThing("GetOnlyThing");

            // fails to compiled: CS0200
            // getOnlyThing.Name = "Changed";
            // this compiles but throws A RuntimeBinderException at runtime
            ((dynamic)getOnlyThing).Name = "Changed Dynamically";
            Console.WriteLine(getOnlyThing.Name);
        }
    }
HaloFour commented 4 years ago

@jnicholes

Sounds like the C# runtime binder also doesn't respect modreq annotations and should probably be fixed for .NET 5.0.

I imagine that many more ways will be found to trivially defeat init. This is partially expected as the team expressed interest in having deserializers capable of hydrating these types without requiring changes by virtue of ignoring the modreq on the accessor method.

wsugarman commented 4 years ago

Records sound like a great idea! I have been struggling lately creating immutable data structures with a fair amount of properties. If every property is read-only, does that mean there is a ctor to populate the properties? Does that ctor take 10+ arguments? If I instead opt to use initializers with internal setters, as opposed to a verbose ctor, how do users create their own objects without internal access? Records seem to solve this problem.

However, with an explicit ctor I can validate the arguments (like a null check) before setting the value. Out-of-box, I suppose you could manually create a backing field and perform the set/init yourself after checking the value, but this seems like such a common scenario. Is there some way the language could accommodate argument validation without the boilerplate of a backing field?

(I'm sorry if this has already been addressed. I may have missed it!) Edit: I missed it in the blog post, but there is an example with explicit backing fields. I think this could become onerous with enough properties in the record

mbernard commented 4 years ago

Are you planning on supporting generic constraints with the record keyword? eg.

// This is not valid and won't compile right now
public void ThisIsADummyMethod<T>(T t) where T : record
HaloFour commented 4 years ago

@mbernard

Are you planning on supporting generic constraints with the record keyword?

Out of curiosity when would you want to constraint the generic type argument to a record?

mbernard commented 4 years ago

Honestly, I haven't thought much about it. But as you can restrain with class and struct keywords it would make sense to have the same thing with record. One thing I could think of is to give a hint about how equality is going to behave as records use structural equality and classes use referential equality. Another use case I can see is if you want to use the with keyword on that generic type, the compiler will need to know that it's a record type.

BreyerW commented 4 years ago

@mbernard actually if im not mistaken withers arent tied to records its just that its autogenerated for them. As such shapes would be more fitting for it

Besides for with to work you would need to know record shape the constraint wouldnt provide, meaning with wouldnt work. With shapes you would be limited to very particular set of records that share the same with signature

HaloFour commented 4 years ago

@mbernard

Records aren't really a separate type, at least in terms of how they're implemented in the runtime. They're still implemented as a class for C# 9.0, and it's likely that by C# 10.0 there will be a struct flavor as well. As generic constraints are, for the most part, handled by CLR metadata, there really isn't a clear way to define a constraint for a record type that wouldn't be wide open. The closest you could get is a constraint like where T : IEquatable<T> as all records would implement that interface, but tons of other types would as well and that doesn't imply the value identity that records confer.

Also, "wither"s depend on a specific shape of the type that isn't driven by interfaces. The generic methods would need to the actual clone method to call and the properties to be assigned. Maybe that's something that can be handled via shapes or implementation extensions in the future.