Изолированные классы

Изолированные классы и интерфейсы позволяют выразить ограниченные иерархии классов, которые обеспечивают больший контроль над наследованием. Во время компиляции известны все прямые наследники изолированного класса. Никакие другие наследники не могут появиться после компиляции модуля с изолированным классом. Например, сторонние клиенты не могут расширить ваш изолированный класс в своем коде. Таким образом, каждый экземпляр изолированного класса имеет тип из ограниченного набора, который известен при компиляции этого класса.

То же самое справедливо для изолированных интерфейсов и их реализаций: новые реализации не могут появиться после компиляции модуля с изолированным интерфейсом.

Изолированные классы похожи на enum-классы: набор значений enum типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса может иметь несколько экземпляров, которые могут нести в себе какое-то состояние.

В качестве примера рассмотрим API библиотеки. Вероятно, он будет содержать классы ошибок, чтобы пользователи библиотеки могли обрабатывать возникающие ошибки. Если иерархия таких классов ошибок включает интерфейсы или абстрактные классы, видимые в общедоступном API, то ничто не препятствует их реализации или расширению в клиентском коде. Однако библиотека не знает об ошибках, объявленных за её пределами, поэтому не может обрабатывать их согласованно с помощью собственных классов. Благодаря изолированной иерархии классов ошибок авторы библиотек могут быть уверены, что им известны все возможные типы ошибок, и никакие другие не могут появиться позже.

Чтобы описать изолированный класс или интерфейс, укажите модификатор sealed перед его именем.

sealed interface Error

sealed class IOError(): Error

class FileReadError(val f: 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 реализации в платформенном модуле, как ожидаемая, так и актуальные версии могут иметь наследников в своих модулях.

Узнайте больше о иерархической структуре мультиплатформенных проектов.

Изолированные классы и выражение 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 в ожидаемых изолированных классах в общем коде многоплатформенных проектов по-прежнему требует ветки else. Это происходит потому, что наследники актуальных реализаций платформы не известны в общем коде.