-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discussion thread for records #10154
Comments
+1 will that be possible to extend with extension methods?
where |
@gafter what's the point of making record_parameters
: '(' record_parameter_list? ')'
; Won't an empty record parameter list simply create a regular class/struct with a useless |
@orthoxerox Perhaps an empty |
|
F# has something similar to this, namely |
From the spec it looks like @gafter adopted the syntax I threw at the wall in #206 (comment), specifically the second example: public class Point(int X, int Y) {
// can apply constructor attributes here
internal Point { // can apply different accessibility modifier here
// validation logic here
// record members assigned automatically
}
} |
@HaloFour You don't need to initialize record's properties in primary-constructor-body, according to the spec, this will be used to add additional code to the compiler generated primary constructor. |
@alrz Ok, I'll update my comment to reflect. I wonder if the constructor can at least override setting the members if they aren't explicitly, such as: public class Student(string Name) {
public Student {
this.Name = Name.ToUpper();
}
} |
Also, spec states that there can be more than one primary-constructor-body and they will be executed in source order. I don't know why this is required when we have sealed class Student(string Name) {
public Student { }
}
partial class Student {
replace public Student {
original; // no arg list?
}
} Actually it is up to the replaced ctor to decide when |
We didn't have primary constructor bodies in C# 6. We had "primary constructors", which were the same syntax as declaring a record but didn't give you any of the members that records automatically declare.
Records do not have to be immutable. You can declare the properties yourself as mutable public class Point(int X, int Y)
{
public int X { get; set; } = X;
public int Y { get; set; } = Y;
}
You mean like every interface ever written? This is something we will need to discuss.
This too is something we will need to discuss.
There is no such requirement in the current spec. Look for |
Then @HaloFour's example could be written like this, public class Student(string Name) {
public string Name { get; } = Name.ToUpper();
} to not assign it twice. |
@alrz True, a better example might be one that involves both validation and initialization: public class Student(string Name) {
public Student {
if (string.IsNullOrEmpty(Name)) {
throw new ArgumentException("Name can't be empty.", nameof(Name));
}
this.Name = Name.ToUpper();
}
} Depending on when the initializers execute perhaps the parameter itself could be modified before property initialization, although that might really confuse some people especially since it differs from how constructors/initializers work in normal classes: public class Student(string Name) {
public Student {
if (string.IsNullOrEmpty(Name)) {
throw new ArgumentException("Name can't be empty.", nameof(Name));
}
}
public string Name { get; } = Name.ToUpper();
} |
@HaloFour Considering that primary constructor body is the last one in the list, yes that could be an issue, however, it would be perfectly fine if you initialize the property inside the primary constructor body: public class Student(string Name) {
public Student {
if (string.IsNullOrEmpty(Name)) {
throw new ArgumentException("Name can't be empty.", nameof(Name));
}
this.Name = Name.ToUpper();
}
// avoids compiler generated assignment
public string Name { get; }
} Just to say, I still think |
@alrz I'm having trouble finding where in the spec that it specifies that a single record class may have multiple primary constructor bodies? I'm not completely sold on the primary constructor body syntax either, but I prefer it to Java-style initializers or having the code just appear within the record body. I do think that it's nicer than having to completely redefine the constructor, parameters and all, which is what was previously in the spec. |
As for Java-like syntax, it would make sense if they allow it in non-record types.
However, you lose access modifiers and probably attributes with that syntax. |
@gafter I believe primary constructor bodies proposed for C# 6.0 were something of the form: class Foo(string bar)
{
//This is the primary constructor body
{
if(bar == null) throw new ArgumentNullException();
}
} which proved to be very controversial. |
I must say that this looks great. Records are very useful, and the current design feels very C#-ish, which makes it integrate nicely into the language. I'm also in favor of keeping the |
@bbarry Re "will that be possible to extend There is nothing in the current specification that would enable that. |
Fine by me; I think the syntax seems a bit unnatural for that case (thinking about the default parameter value, not the fact that it is the To the main topic, It seems odd to create a language feature where adding a property to the type cannot be done without forcing a rebuild of any dependent code. Perhaps some tooling to convert a record class into a fully specified regular class might be necessary (would the reverse tooling even be possible in the general case?). In this spec:
Adding a parameter:
would require full rebuilds from source and potentially code fixes for almost any code that depends on
But if the class were converted back away from the record type, the necessary overloads could be added and existing code could continue to function:
(Ignore Edit: would the optional parameters be necessary on the 2 parameter version? You would want a recompile to switch immediately to the 3 param version and existing calls are already specified... |
I believe that's part of the nature of records. Their shape and their construction are intrinsically linked. F# records have the same behavior. 🍝 Perhaps adding an optional parameter to the end of the primary constructor argument list could result in a compatible type? public class Point(int X, int Y, int Z = 0);
// results in:
public class Point : IEquatable<Point> {
public int X { get; }
public int Y { get; }
public int Z { get; }
public Point(int X, int Y) : this(X, Y, 0) { }
public Point(int X, int Y, int Z = 0) {
this.X = X;
this.Y = Y;
this.Z = Z;
}
public virtual Point With(int X = this.X, int Y = this.Y) => With(X, Y, this.Z);
public virtual Point With(int X = this.X, int Y = this.Y, int Z = this.Z) => new Point(X, Y, Z);
public static void operator is(Point point, out int X, out int Y) {
X = point.X;
Y = point.Y;
}
public static void operator is(Point point, out int X, out int Y, out int Z) {
X = point.X;
Y = point.Y;
Z = point.Z;
}
// other members omitted
} |
Currently, deriving from a base type also causes to re-assignment of inherited properties.
Since moving record element declarations into the body of the type doesn't seem to be an option here, it would be nice to be able to call // causes to not generate assignment for `Name`
// or any property that is used in `BaseType(...)`
// obviously, either 'this' or 'base' can be called
public class Student(string Name, int Gpa) : this(Name), BaseType(...) {
private Student(string Name) {
if (string.IsNullOrEmpty(Name))
throw new ArgumentException("Name can't be empty.", nameof(Name));
this.Name = Name.ToUpper();
}
}
public class Student(string Name, int Gpa) {
// causes to not generate assignments for all properties
public Student {
if (string.IsNullOrEmpty(Name))
throw new ArgumentException("Name can't be empty.", nameof(Name));
if (Gpa > 0)
throw new ArgumentException(...);
this.Name = Name.ToUpper();
this.Gpa = Gpa;
}
} |
@alrz According to the spec, if a suitable property is inherited, then it is not generated. |
@gafter I was quoting the example from spec. So probably that is mistyped. |
One question: What's the reason for |
The public class Person {
public string Name { get; set; }
public virtual Person With(string Name) => new Person() { Name = Name };
}
public class Student : Person {
public double Gpa { get; set; }
public override Person With(string Name) => new Student() { Name = Name, Gpa = this.Gpa };
public virtual Student With(string Name, double Gpa) => new Student() { Name = Name, Gpa = Gpa };
}
Person person = new Student() { Name = "Billy", Gpa = 3.5 };
Person other = person.With(Name: "William");
Debug.Assert(other is Student && ((Student)other).Gpa == 3.5); As for |
Just another way to combine record types (to define right name and position of properties in Student for Person): public class Person(string First, string Last);
// These will not compile if in the constructor are not the all parameters of Person
public class Student(Person.First, Person.Last, double GPA): Person;
// Or to change positions
public class Student(Person.First, double GPA, Person.Last): Person; |
Are you expecting to allow generic records? I couldn't find anything about generics in the document. The main reason I ask is that I found the F# version of So based on what you have so far I guess a generic pair would be class Pair<A,B>
{
A X;
B Y;
Pair(A x, B y) { this.X = x; this.Y = y; }
Pair<A, B> With(A x = this.X, B y = this.Y) { return new Pair<A, B>(x, y); }
} Now this would not allow Pair<C,D> With<C,D>(C x = this.X, D y = this.Y) { return new Pair<C, D>(x, y); } Then this would be nice, but would require careful design. If you omit either parameter, then you effectively constrain Pair<C, D> With1<C, D>(C x, D y) { return new Pair<C, D>(x, y); }
Pair<A, D> With2<A, D>(D y) { return new Pair<A, D >(this.X, y); }
Pair<C, B> With3<C, B>(C x) { return new Pair<C, B>(x, this.Y); }
Pair<A, B> With4<A, B>() { return new Pair<A, B>(this.X, this.Y); } There are then worse cases if multiple types overlap in there usage of generic parameters, and really you want full type inference. So I guess this is going to be out of scope for C#? |
That's a pretty great spec. I love how easy it is to create immutable types! I do have one question - can records be serialized and deserialized using existing serializers? (Data Contract, JSON, etc.) I couldn't understand from the spec whether you could apply attributes to the generated fields/properties. For example: [DataContract]
class Person(
[field: DataMember(Name = nameof(FirstName))]
string FirstName,
[field: DataMember(Name = nameof(LastName))]
string LastName
); (I'm using the field since the property only has a getter) Thanks. |
@aelij I don't think |
@qrli DCS is still in use. It's shipped with .NET Core (unlike other older serializers). It's also the default serializer for Service Fabric's reliable collections. Besides, the question whether attributes can be applied to fields/properties has broader implications (for example, you could use the newly proposed Roslyn source generators to add functionality to some auto-generated members by applying attributes to them). |
Please consider generation of Other languages which have concept of recods (case classes, data classes) implement it (at least I saw in Scala, Kotlin). When you logging messages passing through your code in actor system / messaging bus etc. or in other distributed scenarios, you need logging. Also consider things like REPL - you have no Visual Studio experience here. |
I just saw the features this afternoon so I may have missed something critical in the discussion, for that I apologize in advance.
it just feels very unintuitive, with something that is an 'is' operation I am not expecting assignements. Something like this is perfectly readable to me though:
I also have a slight problem with readability of the already established convention
|
@sirgru If I understand correctly, you can use next syntax: if(person is Person p && p.FirstName == "Mickey") … In addition, in some cases, you can use PM syntax. |
After some looking around I have concluded I like that syntax, despite it being a bit strange at first. I guess even more benefits will come when looking at the bigger picture. Looking forward to the next release! |
The current proposal (Language Feature Status) for the compiler generated
This produces a hash code 0 as soon as one property is null. The following implementation I am proposing produces a useful hash code in any case:
|
Since almost a year has passed, I want to voice my support for the point mentioned by @MgSam -
I think the primary constructor should just generate a POCO. 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 |
Personally I think that this should be more than just a POCO. Equals, HashCode should both be auto implemented otherwise what is the point really? |
My 5 cents: parts of the record type could be automatically implemented by interfaces, if requested by the user with the class Point(int X, int Y) : auto IWithable, auto IEquatable<Point> {} The standard |
I've got to explain the advantages of my previous suggestion. 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 and 'IRecordTypeNet50' for additional functionality provided by .Net 5.0. |
Any further discussion should be moved to the |
/cc @agocke You might want to mine this thread for relevant comments to your current prototyping work. |
This is a discussion thread for records, as specified in https://github.com/dotnet/roslyn/blob/features/records/docs/features/records.md
Please check the spec before adding your comment - it might have been addressed in the latest draft.
The text was updated successfully, but these errors were encountered: