[Proposal]: Records with sealed base ToString override
- Dominant language
- C#
- Stars
- 12.7k
- Forks
- 1.1k
- Avg merge
- 11h 1m
- Merged PRs (30d)
- 3
Description
# Records with sealed base ToString override
* [x] Proposed
* [x] Prototype: Done
* [x] Implementation: Done
* [ ] Specification: Not Started
## Summary
[summary]: #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
[motivation]: #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:
```cs
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.
```cs
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
[design]: #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
[drawbacks]: #drawbacks
## Alternatives
[alternatives]: #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.
## Unresolved questions
[unresolved]: #unresolved-questions
## Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-02-10.md#sealed-record-ToString
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.