Description
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.