[Proposal]: Records with sealed base ToString override #4174
Description
Records with sealed base ToString override
- Proposed
- Prototype: Done
- Implementation: Done
- Specification: Not Started
Summary
This proposal would lift the restriction on declaring sealed ToString
overrides in non-sealed records and lift the restriction on a record inheriting from a base type that has a sealed ToString
override. The semantics of sealed override
are clear, they are appropriate for closed hierarchies where ToString
is controlled by the base, and the current errors seem to deny its otherwise consistent functioning out of an overabundance of caution.
Motivation
There's currently no way to specify a ToString
implementation once for all derived records. Because of this, the need to override ToString
can have a jarring result. Consider this hand-rolled discriminated union based on records:
public abstract record SomeDiscriminatedUnion(string Name)
{
public sealed record FirstKind(string Name, int A) : SomeDiscriminatedUnion(Name);
public sealed record SecondKind(string Name, int B) : SomeDiscriminatedUnion(Name);
public sealed record ThirdKind(string Name, bool C) : SomeDiscriminatedUnion(Name);
public sealed record FourthKind(string Name) : SomeDiscriminatedUnion(Name);
}
Overriding ToString
has no effect on derived records unless each derived record is given a manual override that returns base.ToString()
. Sealing the override would perfectly describe the intended result, but this is currently blocked by the compiler with errors CS0239 and CS8870.
The simplest option currently available is much less pleasing. One factor is its repetition. Another factor is that the base record no longer contains a readable list of single-line members.
public abstract record SomeDiscriminatedUnion(string Name)
{
public sealed record FirstKind(string Name, int A) : SomeDiscriminatedUnion(Name)
{
public override string ToString() => Name;
}
public sealed record SecondKind(string Name, int B) : SomeDiscriminatedUnion(Name)
{
public override string ToString() => Name;
}
public sealed record ThirdKind(string Name, bool C) : SomeDiscriminatedUnion(Name)
{
public override string ToString() => Name;
}
public sealed record FourthKind(string Name) : SomeDiscriminatedUnion(Name)
{
public override string ToString() => Name;
}
}
Detailed design
Non-sealed records would support sealed overrides of ToString
the same way classes do rather than producing error "CS8870 'BaseRecord.ToString()' cannot be sealed because containing record is not sealed."
Derived records would support inheriting when the base ToString
is sealed by skipping the synthesized ToString
override rather than producing error "CS0239 'DerivedRecord.ToString()': cannot override inherited member 'BaseRecord.ToString()' because it is sealed."
Drawbacks
Alternatives
A community source generator could be developed that would look for an attribute on the base record or the base record's ToString
override and add public override string ToString() => base.ToString();
to each derived record that doesn't already declare a ToString
member. This feels like a workaround at best. The semantics of sealed override
are clear, they are appropriate for closed hierarchies where ToString
is controlled by the base, and the current errors seem to deny its otherwise consistent functioning out of an overabundance of caution.
Activity