csharplang
csharplang copied to clipboard
Champion "Records" (VS 16.8, .NET 5)
- [X] Proposal added: proposals/csharp-9.0/records.md
- [x] Discussed in LDM
- [x] Decision in LDM
- [x] Finalized (done, rejected, inactive)
- [x] Spec'ed
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
See #77 regarding with expressions that are more useful than bare With methods.
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 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.
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.
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.
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.
Object(and collection, index) initializers getting constructor level privilege for initializing fields/properties can enable some interesting scenarios of complex hierarchical object initialization.
@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.
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 Yes.
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 What about https://github.com/dotnet/csharplang/blob/master/proposals/records.md#primary-constructor-body ? That looks like a primary constructor to me.
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.
@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 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}");
}
}
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();
@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 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.
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/
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.
@Richiban last time I asked, the LDT planned to relax this restriction and compare runtime types in Equals.
@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 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.
@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
Equalswould 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.
The constructor in the abstract record class example should be protected, not public.
@fubar-coder I'm curious, does that ever make a difference besides being annoying if you ever refactor to being non-abstract?
- The visibility changing from protected to public should happen automatically as long as you don't provide the primary constructor
publicon an abstract class doesn't make sense, because you cannot instantiate this class using this publicly visible constructor
publicon 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.
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.
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.