Что такое sealed class
Перейти к содержимому

Что такое sealed class

  • автор:

Запечатанные классы (sealed)

Добавление модификатора sealed к суперклассу ограничивает возможность создания подклассов. Все прямые подклассы должны быть вложены в суперкласс. Запечатанный класс не может иметь наследников, объявленных вне класса.

 sealed class SealedClass < class One(val value: Int) : SealedClass() class Two(val x: Int, val y: Int) : SealedClass() fun eval(e: SealedClass): Int = when (e) < is SealedClass.One ->e.value is SealedClass.Two -> e.x + e.y > > 

В методе eval() при использовании when не пришлось использовать ветку else, так как sealed позволяет указать все доступные варианты и значение по умолчанию не требуется.

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

По умолчанию запечатанный класс открыт и модификатор open не требуется. Запечатанные классы немного напоминают enum.

Пример: Продам кота дёшево

Вася Ложкин

Создадим запечатанный класс AcceptedCurrency и три подкласса на его основе. Обратите внимание, что сейчас Kotlin разрешает объявлять подклассы не внутри запечатанного класса, а на одном уровне (для сравнения смотри старые примеры выше).

 package ru.alexanderklimov.sealed sealed class AcceptedCurrency class Rubel : AcceptedCurrency() class Dollar : AcceptedCurrency() class Tugrik : AcceptedCurrency() 

В классе активности создадим список принимаемых валют для покупки котят и применим его к адаптеру выпадающего списка.

 package ru.alexanderklimov.sealed import android.os.Bundle import android.widget.ArrayAdapter import androidx.appcompat.app.AppCompatActivity import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() < private val currencies = listOf(Rubel(), Dollar(), Tugrik()) override fun onCreate(savedInstanceState: Bundle?) < super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, currencies) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) currencySpinner.adapter = adapter > > 

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

Sealed class

Внесём изменения в запечатанный класс, чтобы у него появилось новое свойство.

 sealed class AcceptedCurrency < val name: String get() = when (this) < is Rubel ->"Рубль" is Dollar -> "Доллар" is Tugrik -> "Тугрик" > > 

Если вы пропустите какой-то подкласс в выражении when, то компилятор будет ругаться. Это удобно, когда вы будете вносить изменения в код.

Поменяем код для адаптера.

 val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, currencies.map < it.name >) 

Теперь названия выводятся нормально.

Sealed class

Установим зависимость валют от рубля. Создадим в запечатанном классе абстрактное свойство valueInRubels. После этого студия потребует дополнить код у всех подклассов.

 package ru.alexanderklimov.sealed sealed class AcceptedCurrency < abstract val valueInRubels: Double var amount: Double = 0.0 val name: String get() = when (this) < is Rubel ->"Рубль" is Dollar -> "Доллар" is Tugrik -> "Тугрик" > > class Rubel : AcceptedCurrency() < override val valueInRubels = 1.00 >class Dollar : AcceptedCurrency() < override val valueInRubels = 70.0 >class Tugrik : AcceptedCurrency()

Добавим в класс ещё одну переменную ammount и функцию для подсчёта общей суммы.

 sealed class AcceptedCurrency < abstract val valueInRubels: Double var amount: Double = 0.0 val name: String get() = when (this) < is Rubel ->"Рубль" is Dollar -> "Доллар" is Tugrik -> "Тугрик" > fun totalValueInRubels(): Double > 

Напишем код для щелчка кнопки. Вам нужно ввести минимальную и максимальную цену в любой валюте для одного котёнка, а кнопка покажет цену в рублях. Если вы увидите, что покупатель из Америки, то выставляете ценник в долларах. Если покупатель из непонятной страны, то ставьте тугрики (какая вам разница?).

 convertButton.setOnClickListener < val low = currencyFromSelection() val high = currencyFromSelection() low.amount = lowAmountEditText.text.toString().toDouble() high.amount = highAmountEditText.text.toString().toDouble() lowAmountInRubelsTextView.text = String.format("%.2f руб.", low.totalValueInRubels()) highAmountInRubelsTextView.text = String.format("%.2f руб.", high.totalValueInRubels()) >private fun currencyFromSelection() = when (currencies[currencySpinner.selectedItemPosition]) < is Dollar ->Dollar() is Rubel -> Rubel() is Tugrik -> Tugrik() > 

В примере мы выставили цену от 2 до 3 долларов за котёнка (что-то мы продешевили) и сразу видим, сколько заработаем в рублях.

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

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

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

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

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

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

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

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

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

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

1 2 3 4

sealed class MessageType

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

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

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

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

1 2 3 4 5 6 7 8
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 с суперсилой.
  • У изолированного класса могут быть наследники, но все они должны находиться в одном файле с изолированным классом. Классы, которые расширяют наследников изолированного класса могут находиться где угодно.
  • Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты.
  • Конструктор изолированного класса всегда приватен, и это нельзя изменить.
  • Изолированные классы нельзя инициализировать.
  • Наследники изолированного класса могут быть классами любого типа: классом данных, объектом, обычным классом или даже другим изолированным классом.

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

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

#10 — Классы данных, изолированные классы

#10 - Классы данных, изолированные классы

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

Видеоурок

Изолированные классы в Kotlin являются продолжением enum-классов. Подобные классы удобно создавать если в будущем вам придется создавать объекты, что могут иметь разное количество параметров, разный функционал, но при этом они все будут объединены под одним общим классом.

В изолированных классах каждый элемент представляет из себя либо класс данных ( data class ), либо объект ( object ).

sealed class Bank < data class Client(val age: Int, val name: String, val isRich: Boolean): Bank() data class Manager(val age: Int, val name: String, val status: String): Bank() object Filial : Bank() < val address = "Москва, ул. Космонавтов" >>
Функции «Лямба»

В языке Kotlin можно создавать, так называемые, «Лямба»-функции, которые в качестве параметра могут принимать другую функцию.

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

fun someFunction(number: Int, numberEqual: (Int) -> String)

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

Весь код будет доступен после подписки на проект!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *