Skip to content

Non-interface class declarations. #704

Open
@lrhn

Description

@lrhn

As a simple approach to sealing classes (#349), I propose a minimal, yet powerful, feature: Non-interface classes.

Background

Dart classes are "Kotlin open" classes and interfaces. It means that anyone can implement the interface of a class and extend the class—at least if it has a public generative constructor.

That may sometimes be more affordance than desired. There are requests for "Sealed" classes intended to prevent other users from implementing or extending a class. The reasons given for that feature is to control dependencies, so that additions are not breaking changes, and to enable performance improvements when compilers can locally deduce properties of all implementations of a type.

Proposal: Non-Interface struct Classes

Introduce struct as a modifier on class and mixin declarations, struct class and struct mixin, which makes the class name not denote an interface. It still introduces a type, and a class/mixin, but not an interface name.

The class has an "interface" in the sense of a set of implemented types and a mapping from member names to member signatures (which is what the language uses to see if a member access is allowed), but the name will not be usable in implements clauses.

A struct class can be extended as long as it has a public generative constructor. Such a subclass of a struct class must also be a struct class.

If a struct mixin is applied in on any allowable super-type, the result is a struct class.

We allow a lone struct to work as a shorthand for struct class whenever struct is not immediately followed by class or mixin. This reduces the extra typing when declaring a struct class.

Examples

If we combine this with improved default constructors (#469), then you can declare a simple data class as:

struct Point {
  final int x, y;
}

It's possible to extend the class, but not implement it.

A properly sealed class would be:

struct Point {
  final int x, y;
  Point._(this.x, this.y);
  factory Point(int x, int y) = Point._;
}

Maybe we can even find a way to avoid the intermediate constructor, say factory Point._(this.x, this.y); would be a generative constructor which cannot be used as such. (Probably not the best syntax, but something to start from).

Benefits

The benefit of a non-interface class is that it is guaranteed that all objects matching the type will also extend the class (or mix in the mixin). That ensures that private members are available, and that invariants ensured by the constructor are maintained.

Together with only having a private generative constructor, this ensures that nobody else can implement the class.

If it's possible to restrict the availability of a subclass with a public generative constructor, say to the same package using package-local libraries and not exporting the subclass from a public library, it allows a class that can only be extended locally in the same package.

To ensure that specific methods are not changed, we will also need to be able to make methods final (non-overridable).

Drawbacks

A class that doesn't have an implementable interface, cannot be mocked. It also cannot be implemented for any other reason, which is a restriction compared to the flexibility provided by all current Dart classes.

Changing a class into a struct class is a breaking change. This makes it impossible to apply the struct to existing platform classes when the feature is implemented. Packages can increment their major version number and add struct where they prefer to have it. For clients who are not depending on implementing the package's interface, it's free to update to the new version, and everybody else gets a fair warning where things are going.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions