Skip to content

[Proposal]: Records with sealed base ToString override  #4174

Open
dotnet/roslyn
#52029
@jnm2

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.

Unresolved questions

Design meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-02-10.md#sealed-record-ToString

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions