Sealed classes in kotlin

Kotlin. Изолированные (запечатанные) классы (sealed classes).

Изолированный класс — это еще одно новшество в языке Kotlin, которого не было в Java. Тем не менее, само по себе понятие в программировании не является новым — Kotlin позаимствовал его у других языков.

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

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

  • В enum каждое значение — это константа, которая существует в единственном экземпляре. Значение константы нельзя подстроить под конкретную ситуацию, потому что при изменении значения в одном месте, оно изменится везде. В изолированном же классе можно создать столько подклассов, сколько необходимо для покрытия каждой ситуации. Помимо этого, каждый подкласс может иметь несколько экземпляров, каждый из которых будет нести в себе свое собственное состояние.
  • Каждое значение в enum должно содержать одинаковый набор свойств. Не получится какому-либо значению задать дополнительное свойство. Напротив, каждый подкласс изолированного класса имеет свой конструктор со своими индивидуальными свойствами.

Для определения изолированного класса используется ключевое слово sealed .

В данном примере класс MessageType является изолированным. У него есть два подкласса-наследника — Success() и Failure() , каждый из которых имеет индивидуальный набор свойств. Тут может возникнуть вопрос: как Success() и Failure() могут наследоваться от MessageType() , если он не отмечен ключевым словом open ? Всё просто: изолированный класс “открыт” для наследования по умолчанию, и дополнительно указывать слово open не требуется.

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

Помимо этого, изолированный класс абстрактен по умолчанию и может содержать в себе абстрактные компоненты.

Изолированный класс можно использовать совместно с условным выражением when , при этом указывать ветку else не требуется.

val msgSuccess = Success("Ура!") val msgFailure = Failure("Ну вот. ", Exсeption("Что-то пошло не так.")) var messageType: MessageType = msgFailure val msg = when(messageType) < is Success ->messageType.msg is Failure -> messageType.msg + " " + messageType.e.message >

На данный момент об изолированных классах сказать больше нечего. Поэтому резюмируем:

  • Изолированные классы — это enum с суперсилой.
  • У изолированного класса могут быть наследники, но все они должны находиться в одном файле с изолированным классом. Классы, которые расширяют наследников изолированного класса могут находиться где угодно.
  • Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты.
  • Конструктор изолированного класса всегда приватен, и это нельзя изменить.
  • Изолированные классы нельзя инициализировать.
  • Наследники изолированного класса могут быть классами любого типа: классом данных, объектом, обычным классом или даже другим изолированным классом.
Читайте также:  Java mockito unit test

Полезные ссылки

Sealed Classes — официальная документация.
Изолированные классы — перевод на русский.

Источник

Sealed classes and interfaces

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear outside the module and package within which the sealed class is defined. For example, third-party clients can’t extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled.

The same works for sealed interfaces and their implementations: once a module with a sealed interface is compiled, no new implementations can appear.

In some sense, sealed classes are similar to enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances, each with its own state.

As an example, consider a library’s API. It’s likely to contain error classes to let the library users handle errors that it can throw. If the hierarchy of such error classes includes interfaces or abstract classes visible in the public API, then nothing prevents implementing or extending them in the client code. However, the library doesn’t know about errors declared outside it, so it can’t treat them consistently with its own classes. With a sealed hierarchy of error classes, library authors can be sure that they know all possible error types and no other ones can appear later.

To declare a sealed class or interface, put the sealed modifier before its name:

sealed interface Error sealed class IOError(): Error class FileReadError(val file: File): IOError() class DatabaseError(val source: DataSource): IOError() object RuntimeError : Error

A sealed class is abstract by itself, it cannot be instantiated directly and can have abstract members.

Constructors of sealed classes can have one of two visibilities: protected (by default) or private :

sealed class IOError < constructor() < /*. */ >// protected by default private constructor(description: String): this() < /*. */ >// private is OK // public constructor(code: Int): this() <> // Error: public and internal are not allowed >

Location of direct subclasses

Direct subclasses of sealed classes and interfaces must be declared in the same package. They may be top-level or nested inside any number of other named classes, named interfaces, or named objects. Subclasses can have any visibility as long as they are compatible with normal inheritance rules in Kotlin.

Subclasses of sealed classes must have a proper qualified name. They can’t be local nor anonymous objects.

enum classes can’t extend a sealed class (as well as any other class), but they can implement sealed interfaces.

These restrictions don’t apply to indirect subclasses. If a direct subclass of a sealed class is not marked as sealed, it can be extended in any way that its modifiers allow:

Читайте также:  Php string starts and ends with

sealed interface Error // has implementations only in same package and module sealed class IOError(): Error // extended only in same package and module open class CustomError(): Error // can be extended wherever it’s visible

Inheritance in multiplatform projects

There is one more inheritance restriction in multiplatform projects: direct subclasses of sealed classes must reside in the same source set. It applies to sealed classes without the expect and actual modifiers.

If a sealed class is declared as expect in a common source set and have actual implementations in platform source sets, both expect and actual versions can have subclasses in their source sets. Moreover, if you use a hierarchical structure, you can create subclasses in any source set between the expect and actual declarations.

Sealed classes and when expression

The key benefit of using sealed classes comes into play when you use them in a when expression. If it’s possible to verify that the statement covers all cases, you don’t need to add an else clause to the statement:

when expressions on expect sealed classes in the common code of multiplatform projects still require an else branch. This happens because subclasses of actual platform implementations aren’t known in the common code.

Источник

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

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

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

Изолированные классы похожи на 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 

Сам по себе изолированный класс является абстрактным, он не может быть создан напрямую и может иметь абстрактные компоненты.

Читайте также:  A program on how getElementById works in JavaScript

Конструкторы изолированных классов могут иметь одну из двух видимостей: protected (по умолчанию) или private .

sealed class IOError < constructor() < /*. */ >// protected по умолчанию private constructor(description: String): this() < /*. */ >// private допускается // public constructor(code: Int): this() <> // Ошибка: public и internal не допускаются > 

Расположение прямых наследников

Прямые наследники изолированных классов и интерфейсов должны быть объявлены в том же пакете. Они могут быть верхнего уровня или вложены в любое количество других именованных классов, именованных интерфейсов или именованных объектов. Наследники могут иметь любую видимость, если они совместимы с обычными правилами наследования в Kotlin.

Наследники изолированных классов должны иметь правильные имена. Они не могут быть локальными или анонимными объектами.

`enum` classes can’t extend a sealed class (as well as any other class), but they can implement sealed interfaces. —>

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 $") > is DatabaseError -> < println("Error while reading from database $") > RuntimeError -> < println("Runtime error") >// оператор `else` не требуется, потому что мы покрыли все возможные случаи > 

`when` expressions on [`expect`](multiplatform-connect-to-apis.md) sealed classes in the common code of multiplatform projects still > require an `else` branch. This happens because subclasses of `actual` platform implementations aren’t known in the > common code. —>

Выражение when в expect изолированных классах в общем коде многоплатформенных проектов по-прежнему требует ветки else . Это происходит потому, что наследники актуальных реализаций платформы не известны в общем коде.

© 2015—2023 Open Source Community

Источник

Оцените статью