Изолированные классы
Изолированные классы и интерфейсы позволяют выразить ограниченные иерархии классов, которые обеспечивают больший контроль над наследованием. Во время компиляции известны все прямые наследники изолированного класса. Никакие другие наследники не могут появиться после компиляции модуля с изолированным классом. Например, сторонние клиенты не могут расширить ваш изолированный класс в своем коде. Таким образом, каждый экземпляр изолированного класса имеет тип из ограниченного набора, который известен при компиляции этого класса.
То же самое справедливо для изолированных интерфейсов и их реализаций: новые реализации не могут появиться после компиляции модуля с изолированным интерфейсом.
Изолированные классы похожи на enum-классы: набор значений enum типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса может иметь несколько экземпляров, которые могут нести в себе какое-то состояние.
В качестве примера рассмотрим API библиотеки. Вероятно, он будет содержать классы ошибок, чтобы пользователи библиотеки могли обрабатывать возникающие ошибки. Если иерархия таких классов ошибок включает интерфейсы или абстрактные классы, видимые в общедоступном API, то ничто не препятствует их реализации или расширению в клиентском коде. Однако библиотека не знает об ошибках, объявленных за её пределами, поэтому не может обрабатывать их согласованно с помощью собственных классов. Благодаря изолированной иерархии классов ошибок авторы библиотек могут быть уверены, что им известны все возможные типы ошибок, и никакие другие не могут появиться позже.
Чтобы описать изолированный класс или интерфейс, укажите модификатор sealed
перед его именем.
sealed interface Error
sealed class IOError(): Error
class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()
object RuntimeError : Error
Сам по себе изолированный класс является абстрактным, он не может быть создан напрямую и может иметь абстрактные компоненты.
Конструкторы изолированных классов могут иметь одну из двух видимостей: protected
(по
умолчанию) или private
.
sealed class IOError {
constructor() { /*...*/ } // protected по умолчанию
private constructor(description: String): this() { /*...*/ } // private допускается
// public constructor(code: Int): this() {} // Ошибка: public и internal не допускаются
}
Расположение прямых наследников
Прямые наследники изолированных классов и интерфейсов должны быть объявлены в том же пакете. Они могут быть верхнего уровня или вложены в любое количество других именованных классов, именованных интерфейсов или именованных объектов. Наследники могут иметь любую видимость, если они совместимы с обычными правилами наследования в Kotlin.
Наследники изолированных классов должны иметь правильные имена. Они не могут быть локальными или анонимными объектами.
Enum
-классы не могут расширять изолированный класс (как и любой другой класс), но они могут реализовывать изолированные интерфейсы.
Эти ограничения не применяются к непрямым наследникам. Если прямой наследник изолированного класса не помечен как изолированный, он может быть расширен любыми способами, разрешенными его модификаторами.
sealed interface Error // имеет реализации только в том же пакете и модуле
sealed class IOError(): Error // расширяется только в том же пакете и модуле
open class CustomError(): Error // может быть расширен везде, где виден
Наследование в мультиплатформенных проектах
В мультиплатформенных проектах есть еще одно ограничение наследования: прямые наследники изолированных
классов должны находиться в одном модуле. Это применимо к изолированным классам без
модификаторов expect
и actual
.
Если изолированный класс объявлен как expected
в общем модуле и имеет actual
реализации в платформенном модуле, как
ожидаемая, так и актуальные версии могут иметь наследников в своих модулях. Более того, если вы используете
иерархическую структуру, вы можете создавать наследников
в любом исходном наборе между expect
и actual
объявлениями.
Узнайте больше об иерархической структуре мультиплатформенных проектов.
Изолированные классы и выражение when
Ключевое преимущество от использования изолированных классов проявляется тогда, когда вы используете их в
выражении when
. Если возможно проверить, что выражение покрывает все случаи,
то вам не нужно добавлять else
. Однако, это работает только в том случае, если вы используете when
как выражение
(используя результат), а не как оператор.
fun log(e: Error) = when(e) {
is FileReadError -> { println("Error while reading file ${e.file}") }
is DatabaseError -> { println("Error while reading from database ${e.source}") }
RuntimeError -> { println("Runtime error") }
// оператор `else` не требуется, потому что мы покрыли все возможные случаи
}
Выражение
when
вexpect
изолированных классах в общем коде многоплатформенных проектов по-прежнему требует веткиelse
. Это происходит потому, что наследники актуальных реализаций платформы не известны в общем коде.