Освой Kotlin играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

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

Seals - морские котики (англ.).

Добавление модификатора 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<AcceptedCurrency>(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<String>(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() {
    override val valueInRubels = 5.0
}

Добавим в класс ещё одну переменную 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 {
        return amount * valueInRubels
    }
}

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


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 долларов за котёнка (что-то мы продешевили) и сразу видим, сколько заработаем в рублях.

Sealed class

Дополнительное чтение

Классы

Реклама