Запечатанные классы (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 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 >)
Теперь названия выводятся нормально.
Установим зависимость валют от рубля. Создадим в запечатанном классе абстрактное свойство 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 долларов за котёнка (что-то мы продешевили) и сразу видим, сколько заработаем в рублях.
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 с суперсилой.
- У изолированного класса могут быть наследники, но все они должны находиться в одном файле с изолированным классом. Классы, которые расширяют наследников изолированного класса могут находиться где угодно.
- Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты.
- Конструктор изолированного класса всегда приватен, и это нельзя изменить.
- Изолированные классы нельзя инициализировать.
- Наследники изолированного класса могут быть классами любого типа: классом данных, объектом, обычным классом или даже другим изолированным классом.
Полезные ссылки
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:
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.