Часто нам приходится представлять ограниченный набор возможностей: веб-запрос либо успешно выполняется, либо не выполняется, User
может быть либо про-пользователем, либо обычным.
Чтобы смоделировать это, мы могли бы использовать enum
, но это несет в себе ряд ограничений. Классы Enum допускают только один экземпляр каждого значения и не могут кодировать дополнительную информацию о каждом типе, например случай Error
, имеющий соответствующее свойство Exception
.
Вы можете использовать абстрактный класс и ряд расширений, но при этом теряется преимущество ограниченного набора типов, добавляемое перечислениями. Запечатанные классы берут лучшее из обоих миров: свободу представления абстрактных классов и ограниченный набор типов перечислений. Читайте дальше, чтобы узнать больше о запечатанных классах, или, если вы предпочитаете видео, посмотрите его здесь (англ):
Как и абстрактные классы, запечатанные классы позволяют представлять иерархии. Дочерними классами могут быть классы любого типа: класс данных, объект, обычный класс или даже другой запечатанный класс. В отличие от абстрактных классов, вы должны определить эти иерархии в том же файле, где и вложенные.
// Result.kt
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
Попытка расширить запечатанный класс за пределы файла, в котором он был определен, приводит к ошибке компиляции:
Cannot access ‘<init>’: it is private in ResultЧасто мы хотим обрабатывать все возможные типы:
when(result) {
is Result.Success -> { }
is Result.Error -> { }
}
Но что делать, если кто-то добавляет новый тип Result
: InProgress
:
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object InProgress : Result<Nothing>()
}
Вместо того, чтобы полагаться на память или поиск средствами IDE, для гарантии того, что все использования when
обрабатывают новый класс, компилятор может выдать нам ошибку, если ветвь не покрыта. when
, как и оператор if
, требует от нас лишь охватить все варианты (т.е. быть исчерпывающими), создавая ошибку компилятора, когда он используется в качестве выражения:
val action = when(result) {
is Result.Success -> { }
is Result.Error -> { }
}
Выражение when должно быть исчерпывающим, поэтому добавьте необходимую ветвь “is InProgress” или какую-либо другую. Чтобы получить это замечательное преимущество, даже если мы используем when в качестве оператора, добавьте следующее свойство вспомогательного расширения:
val <T> T.exhaustive: T
get() = this
Так что теперь, добавляя .exhaustive
, если ветвь отсутствует, компилятор выдаст нам ту же ошибку, которую мы видели ранее.
when(result){
is Result.Success -> { }
is Result.Error -> { }
}.exhaustive
Поскольку известны все подтипы запечатанного класса, IDE может заполнить все возможные ветви оператора when за нас:
Эта функция действительно очень полезна при работе с более сложными иерархиями запечатанных классов, поскольку IDE может распознавать все ветви:
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
sealed class Error(val exception: Exception) :
Result<Nothing>() {
class RecoverableError(exception: Exception) :
Error(exception)
class NonRecoverableError(exception: Exception) :
Error(exception)
}
object InProgress : Result<Nothing>()
}
Это тип функциональности, который не может быть реализован с абстрактными классами, поскольку в данном случае компилятор не знает иерархию наследования, следовательно IDE не может генерировать ветви.
Так что же заставляет запечатанные классы вести себя именно так? Давайте посмотрим, что происходит в декомпилированном коде Java:
sealed class Result
data class Success(val data: Any) : Result()
data class Error(val exception: Exception) : Result()
@Metadata(
…
d2 = {“Lio/testapp/Result;”, “T”, “”, “()V”, “Error”,
“Success”, “Lio/testapp/Result$Success;”, “Lio/testapp/Result$Error;” …}
)
public abstract class Result {
private Result() {}
// $FF: синтетический метод
public Result(DefaultConstructorMarker
$constructor_marker) {
this();
}
}
Метаданные запечатанного класса сохраняют список дочерних классов, позволяя компилятору использовать эту информацию там, где это необходимо.
Result
реализован в виде абстрактного класса с двумя конструкторами:
Таким образом, это означает, что ни один другой класс не может непосредственно вызвать конструктор. Если мы посмотрим на декомпилированный код класса Success, то увидим, что он вызывает синтетический конструктор:
public final class Success extends Result {
@NotNull
private final Object data
public Success(@NotNull Object data) {
super((DefaultConstructorMarker)null);
this.data = data;
}
Начните использовать запечатанные классы для моделирования ограниченных иерархий классов, позволяя компилятору и IDE помочь вам избежать ошибок согласования типов.
Перевод статьи Florina Muntenescu: Sealed with a class
Комментарии