Author: Bob Nystrom
Status: Abandoned
This proposal was superceded by the class-modifiers proposal. Please refer to that document for more recent information.
Version 1.1 (see Changelog at end)
This proposal specifies sealed types, which is core capability needed for exhaustiveness checking of subtypes in pattern matching. This proposal is a subset of the type modifiers proposal. (We may wish to do all or parts of the rest of that proposal, but they aren't needed for pattern matching, so this proposal separates them out.) For motivation, see the previously linked documents.
Marking a type sealed
applies two restrictions:
-
If it's a class, the type itself can't be directly constructed. The class is implicitly
abstract
. -
All direct subtypes of the type must be defined in the same library. Any types that directly implement, extend, or mix in the sealed type must be defined in the library where the sealed type is defined.
In return for those restrictions, sealing provides two useful properties for exhaustiveness checking:
-
All of the direct subtypes of the sealed type can be easily found and enumerated.
-
Any concrete instance of the sealed type must also be an instance of at least one of the known direct subtypes. In other words, if you match on a value of the sealed type and you have cases for all of the direct subtypes, the compiler knows those cases are exhaustive.
Note that it is not necessary for the subtypes of a sealed type to themselves be sealed or closed to subclassing or implementing. Given:
sealed class Either {}
class Left extends Either {}
class Right extends Either {}
Then this switch is exhaustive:
test(Either either) {
switch (either) {
case Left(): print('Left');
case Right(): print('Right');
}
}
And this is still true even if some unrelated or unknown library contains:
class LeftOut extends Left {}
Or even:
class Ambidextrous implements Left, Right {}
The only property we need for exhaustiveness is that all instances of the sealed type must also be an instance of a direct subtype. More precisely, any instance of the sealed supertype must have at least one of the direct subtypes in its superinterface graph.
At the same time, it can be useful to seal not just a supertype but one or more of its subtypes. Doing so lets you define a sealed hierarchy where matching various subtypes will exhaustively cover various branches of the hierarchy. For example:
// UnitedKingdom --+-- NorthernIreland
// |
// +-- GreatBritain --+-- England
// |
// +-- Scotland
// |
// +-- Wales
sealed class UnitedKingdom {}
class NorthernIreland extends UnitedKingdom {}
sealed class GreatBritain extends UnitedKingdom {}
class England extends GreatBritain {}
class Scotland extends GreatBritain {}
class Wales extends GreatBritain {}
Marking not just UnitedKingdom
sealed
, but also GreatBritain
means that
all of these switches are exhaustive:
test1(UnitedKingdom uk) {
switch (uk) {
case NorthernIreland(): print('Northern Ireland');
case GreatBritain(): print('Great Britain');
}
}
test2(UnitedKingdom uk) {
switch (uk) {
case NorthernIreland(): print('Northern Ireland');
case England(): print('England');
case Scotland(): print('Scotland');
case Wales(): print('Wales');
}
}
test3(GreatBritain britain) {
switch (britain) {
case England(): print('England');
case Scotland(): print('Scotland');
case Wales(): print('Wales');
}
}
Note that the above examples are all exhaustive regardless of whether
NorthernIreland
, England
, Scotland
, and Wales
are marked sealed
.
In short, sealed
is mostly a property that affects how you can use the
supertype and does not apply any restrictions to the direct subtypes of the
sealed type, except that they must be defined in the same library.
A class or mixin declaration may be preceded with the identifier sealed
:
classDeclaration ::=
( 'abstract' | 'sealed' )? 'class' identifier typeParameters?
superclass? interfaces?
'{' (metadata classMemberDeclaration)* '}'
| ( 'abstract' | 'sealed' )? 'class' mixinApplicationClass
mixinDeclaration ::= 'sealed'? 'mixin' identifier typeParameters?
('on' typeNotVoidList)? interfaces?
'{' (metadata classMemberDeclaration)* '}'
Note that the grammar disallows sealed
on a class marked abstract
. All
sealed types are abstract, so it's redundant to allow both modifiers.
It is a compile-time error to extend, implement, or mix in a type marked
sealed
outside of the library where the sealed type is defined. It is fine,
however to subtype a sealed type from another part file or augmentation
library within the same library.
A typedef can't be used to subvert this restriction. If a typedef refers to a sealed type, it is also a compile-time error to extend, implement or mix in that typedef outside of the library where the sealed type the typedef refers to is defined. Note that the library where the typedef is defined does not come into play.
A class marked sealed
is implicitly an abstract class with all of the
existing restrictions and capabilities that implies. It may contain abstract
member declarations, it is a compile-time error to directly invoke its
constructors, etc.
There are no runtime semantics.
The "dart:core" types bool
, double
, int
, Null
, num
, and String
are
all marked sealed
. These types have always behaved like sealed types by
relying on special case restrictions in the language specification. That
existing behavior can now be expressed in terms of this general-purpose
feature.
- Don't make
sealed
a built-in identifier. It's just a normal identifier that is only treated specially when precedingclass
ormixin
.