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

И вот однажды мы знакомимся с новой тенденцией, которая набирает обороты.

Предпочитайте композицию наследованию.

Некоторые из вас могут относиться к этому, в то время как другие могут быть уже хорошо осведомлены в продолжающихся спорах между наследованием и композицией. По правде говоря, в композиции нет ничего нового, и она существует столько же, сколько и наследование. Мы не будем здесь обсуждать этот вопрос, кроме краткого упоминания того, как Kotlin может на что-то намекать, делая свои классы закрытыми (или окончательными) по умолчанию; и далее упоминание о том, как другие успешные языки устроены таким образом.

Шаблон делегирования - это популярный шаблон проектирования, в котором родительский объект передает запросы дочернему объекту Делегировать объект. Это обеспечивает повторное использование кода, которое аналогично достигается через наследование, а также обеспечивает соблюдение «принципа единой ответственности», который позволяет родителю не зависеть от того, как выполняется запрос.

Состав и создание делегатов могут быть выполнены на любом языке, но Kotlin упрощает это с помощью ключевого слова by. Его можно использовать для делегирования свойств или реализации путем делегирования. Мы рассмотрим и то, и другое в этой статье.

Делегированные свойства

Прежде чем изучать делегатов и все, что Kotlin может предложить, мы постараемся научить их ценить их, создав сценарий использования с помощью альтернативных решений.

Жизнь до делегированных свойств

Представьте себе сценарий, который требует от нас шифрования строкового типа данных во время каждого доступа для чтения. Предположим, у нас есть var planetName = “Earth”, где planetName - строка, которую нужно зашифровать. Если мы используем println(planetName) в качестве примера для чтения его значения, тогда будет возвращена зашифрованная версия строки. Он может выглядеть как “eraht”, “reaht” или “treah”. Второй вызов println(planetName) выведет новую скремблированную версию, которая может быть “ahert”, “hreat” или “ather”, и это повторяется для каждого выполнения println(planetName) или любого другого метода, который мы используем. для чтения planetName.

Чтобы решить эту проблему, мы, вероятно, реализуем что-то вроде следующего:

var planetName = "Earth"
planetName = planetName
    .toList()
    .shuffled()
    .joinToString(separator = "")
println("My Scrambled Planet: $planetName")

planetName.toList().shuffled().joinToString(separator = "") Самый оптимальный способ зашифровать строку?
Наверное, нет.

Есть ли более эффективный способ, который не обязательно является самым оптимальным?
Вероятно, есть.

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

fun String.scramble() : String {
    return this.toList().shuffled().joinToString(separator = "")
}

-

var planetName = "Earth"
planetName = planetName.scramble()
println("My Scrambled Planet: $planetName")

Все идет нормально.

Но не совсем так.

Наше требование - шифровать строку при каждом доступе для чтения. Второй вызов println(planetName) вернет только предыдущую скремблированную версию planetName. Сначала мы должны выполнить planetName = planetName.scramble(), чтобы не только скремблировать planetName, но и перезаписать его предыдущее значение новой версией строки. Нам нужно отслеживать состояние planetName в дополнение к выполнению скремблирования. Было бы неплохо, если бы мы могли сделать все это в одном операторе.

Решить эту проблему несложно.

class StringScrambler(private var value: String) {
    fun nextString(): String {
        value = value.scramble()
        return value
    }
}
fun String.scramble(): String {
    return this.toList().shuffled().joinToString(separator = "")
}

-

val planetName = StringScrambler("Earth")
println("My Scrambled Planet: ${planetName.nextString()}")
println("My Scrambled Planet: ${planetName.nextString()}")
println("My Scrambled Planet: ${planetName.nextString()}")

Наш StringScrambler прекрасно справляется с этим. Обертывание нашей строки внутри этого класса помогает решить нашу проблему управления состоянием при инкапсуляции логики. Все самодостаточно.

StringScrambler - это пример делегата. Это классическое ручное делегирование. И чтобы продемонстрировать его расширяемость, мы расширим наш вариант использования, чтобы отслеживать счетчик каждого скремблирования и доступа для чтения.

Зачем нам нужен счетчик зашифрованного доступа для чтения?
Потому что он будет регистрироваться с зашифрованным значением.

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

class StringScrambler(private var value: String) {
    private var count = 0
    fun nextString(): String {
        value = value.scramble()
        count = count.inc()
        println("$count: Reading new scrambled value '${value}'")
        return value
    }
}
fun String.scramble(): String {
    return this.toList().shuffled().joinToString(separator = "")
}

-

val planetName = StringScrambler("Earth")
println("My Scrambled Planet: ${planetName.nextString()}")
println("My Scrambled Planet: ${planetName.nextString()}")
println("My Scrambled Planet: ${planetName.nextString()}")

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

Реализация настраиваемого делегированного свойства

Ключевое слово by в Kotlin позволяет нам делегировать доступ для чтения объекта другому классу. В этом случае оно будет называться делегированным свойством. Синтаксис для этого следующий:

class MyDelegate {
    operator fun getValue(ref: Any?, kProp: KProperty<*>): String {
        return "My name is '${kProp.name}' and '$ref' is my parent."
    }
}

Его создание начинается с реализации метода getValue().
Обратите внимание на ключевое слово оператор.

  • ref: Any? - это ссылка на родительский объект свойства.
  • kProp: KProperty<*> - ссылка на отражение для получения метаданных свойства.

Затем его можно использовать как любое другое свойство.

class MyParent {
    val myDelegate: String by MyDelegate()
}

-

val myParent = MyParent()
println(myParent.myDelegate)

Или просто как переменную.

val myDelegate: String by MyDelegate()
println(myDelegate)

Последний заслуживает особого упоминания, поскольку делегированные свойства local доступны только с Kotlin 1.1. Сегодня мы далеки от версии 1.1, но ее стоит упомянуть, если она когда-нибудь возникнет во время собеседования или если вы когда-нибудь окажетесь вовлеченными в игру в мелочи.

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

Теперь, когда вы получили базовое представление о делегированных свойствах Kotlin, как это преобразует наш StringScrambler?

Это будет выглядеть так:

class StringScramblerDelegate(private var value: String) {
    private var count = 0
    operator fun getValue(ref: Any?, kProp: KProperty<*>): String {
        value = value.scramble()
        count = count.inc()
        println("$count: Reading '$value' from '${kProp.name}'")
        return value
    }
}
fun String.scramble(): String {
    return this.toList().shuffled().joinToString(separator = "")
}

-

val planetName: String by StringScramblerDelegate("Earth")
println("My Scrambled Planet: $planetName")
println("My Scrambled Planet: $planetName")
println("My Scrambled Planet: $planetName")

Вместо ${planetName.nextString()} для шифрования, подсчета, регистрации и последующего чтения нашего следующего строкового значения мы можем просто использовать $planetName.

До сих пор мы имели дело только с делегатами только для чтения, но возможность записи значения также важна. Ссылаясь на наше предыдущее руководство StringScrambler, предоставление доступа для записи может быть таким же простым, как добавление функции для обновления строкового значения. Решение для этого тривиально, поэтому мы не будем показывать пример; однако мы должны признать необходимость вызова этой функции для записи аналогично тому, как StringScrambler.nextString() мы хотели избежать чтения.

Мы можем добиться большего с Kotlin Delegated Properties.

Чтобы сделать наш StringScramblerDelegate доступным для записи, мы реализуем метод setValue().

class StringScramblerDelegate(private var value: String) {
    private var count = 0
    operator fun getValue(ref: Any?, kProp: KProperty<*>): String {
        value = value.scramble()
        count = count.inc()
        println("$count: Reading new '$value' from '${kProp.name}'")
        return value
    }
    operator fun setValue(ref: Any?, kProp: KProperty<*>, value: String) {
        count = 0   // reset count on each update
        println("Setting value '$value' on '${kProp.name}'")
        this.value = value
    }
}

-

var planetName: String by StringScramblerDelegate("Earth")
println("My Planet: $planetName")
println("My Planet: $planetName")
println("My Planet: $planetName")
planetName = "Mercury"
println("My Planet: $planetName")
println("My Planet: $planetName")
println("My Planet: $planetName")
planetName = "Venus"
println("My Planet: $planetName")
println("My Planet: $planetName")
println("My Planet: $planetName")

Писать в нашу собственность так же легко, как читать.

Давайте сделаем еще один шаг вперед на примере записи данных в постоянное хранилище. У Android есть различные решения для этого: SharedPreferences, База данных, Файловая система и т. Д. Я уверен, что у вас есть аналогичные варианты для любого фреймворка, с которым вы работаете. В этом примере мы воспользуемся преимуществом принципа инверсии зависимостей SOLID, чтобы наша реализация была независимой, абстрагируя зависимость от интерфейса; и, чтобы этот пример оставался общим, мы реализуем конкретный класс как простую заглушку.

interface StorageWrapper {
    fun readValue(): String
    fun writeValue(value: String)
}
class StubStorageWrapper : StorageWrapper {
    private var value: String = ""
    override fun readValue(): String {
        return value
    }
    override fun writeValue(value: String) {
        this.value = value
    }
}

Вот рабочий пример делегата.

class StorageDelegate(private val storageWrapper: StorageWrapper) {
    operator fun getValue(ref: Any?, kProp: KProperty<*>): String {
        return storageWrapper.readValue()
    }
    operator fun setValue(ref: Any?, kProp: KProperty<*>, value: String) {
        this.storageWrapper.writeValue(value)
    }
}

-

var planetName: String by StorageDelegate(StubStorageWrapper())
planetName = "Mercury"
println("My Planet: $planetName")

Kotlin предоставляет следующие интерфейсы для делегирования свойств.

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(ref: R, kProp: KProperty<*>): T
}
interface ReadWriteProperty<in R, T> {
    operator fun getValue(ref: R, kProp: KProperty<*>): T
    operator fun setValue(ref: R, kProp: KProperty<*>, value: T)
}

Хотя это и не обязательно, интерфейсы рекомендуются для удобства чтения и обслуживания. Вот как это выглядит с нашим StorageDelegate.

class StorageDelegate(
    private val storageWrapper: StorageWrapper
) : ReadWriteProperty<Any?, String> {
    override fun getValue(ref: Any?, kProp: KProperty<*>): String {
        return storageWrapper.readValue()
    }
    override fun setValue(ref: Any?, kProp: KProperty<*>, value: String) {
        this.storageWrapper.writeValue(value)
    }
}

Наш StorageDelegate основан на типе String, но мы можем определить параметр типа и работать с ним как с универсальным типом.

Предоставить делегата

Создание нашего StorageDelegate и зависимого от него StubStorageWrapper тривиально, потому что оно было разработано так, чтобы быть простым для целей этого примера. Реалистичный сценарий потребует создания логики, основанной на ключах предпочтений, идентификаторах базы данных, путях к файлам и т. Д. Распространенным вариантом использования будет скрытие этой логики. Это может быть достигнуто путем реализации оператора provideDelegate, который позволяет нам инкапсулировать эту логику создания, продолжая использовать ключевое слово by.

class StorageDelegateFactory {
    operator fun provideDelegate(ref: Any?, kProp: KProperty<*>): ReadWriteProperty<Any?, String> {
        println("Creating new StorageDelegate for '${kProp.name}'")
        return StorageDelegate(StubStorageWrapper())
    }
}

provideDelegate очень похож на getValue()

  • ref: Any? - это ссылка на родительский объект свойства.
  • kProp: KProperty<*> - ссылка на отражение для получения метаданных свойства.

Вот рабочий пример того, как это используется.

class StorageDelegateFactory {
    operator fun provideDelegate(ref: Any?, kProp: KProperty<*>): ReadWriteProperty<Any?, String> {
        println("Creating new StorageDelegate for '${kProp.name}'")
        return StorageDelegate(StubStorageWrapper())
    }
}

-

var planetName: String by StorageDelegateFactory()
planetName = "Mercury"
println("My Planet: $planetName")

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

Стандартные делегированные свойства

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

Ненулевой

Вы использовали модификатор Kotlin lateinit? Они удобны в ситуациях, когда свойство должно быть объявлено как не допускающее значения NULL, но не может быть инициализировано при объявлении. Обратной стороной является то, что они не разрешены для примитивных типов.

NotNull - это одно из решений, которое может нам помочь. Его фабричный метод имеет следующее определение:

fun <T : Any> notNull(): ReadWriteProperty<Any?, T>

И им легко пользоваться.

var planetName : String by notNull()
fun main() {
    planetName = "Saturn"
    println("My Planet: $planetName")
}

Исключение IllegalStateException возникает при попытке прочитать значение, которое не было установлено.

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

NotNull также не заменяет введенные в поле свойства, например, из Dagger2.

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

Ленивый

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

fun <T> lazy(initializer: () -> T): Lazy<T>
fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>
  • initializer - это функция обратного вызова, вызываемая при первом доступе для чтения к свойству. Если выполнение вызывает исключение, оно повторит попытку при следующем доступе для чтения.
  • mode указывает, как должен быть синхронизирован обратный вызов инициализатора.
    LazyThreadSafetyMode.SYNCHRONIZED используется по умолчанию и, как правило, это режим, который мы будем использовать в большинстве сценариев. Этот режим гарантирует, что только один поток может инициализировать свойство.
    LazyThreadSafetyMode.PUBLICATION обеспечивает одновременный доступ к свойству во время инициализации первого чтения. Только первое возвращенное значение будет использоваться в качестве инициализированного значения свойства, остальные будут проигнорированы.
    LazyThreadSafetyMode.NONE указывает, что ничего не должно использоваться для управления синхронизацией. Это применимо для однопоточных сред, таких как основной поток Android.
fun calculateStarsInUniverse(): Long {
    println("The number of stars in the universe is estimated to equal the number of grains of sand on all the beaches on Earth.")
    println("A very big number.")
    println("We will only initialize to Long.MAX_VALUE.")
    return Long.MAX_VALUE
}

-

val numStars: Long by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    calculateStarsInUniverse() 
}
// Initializing on first read
println("First Read Access:  Count = $numStars")
// Reading initialized value
println("Second Read Access: Count = $numStars")
println("Third Read Access:  Count = $numStars")

Разрешенный

Делегат Vetoable позволяет нам перехватить доступ на запись к ресурсу. Его фабричный метод имеет следующее определение:

inline fun <T> vetoable(
    initialValue: T,
    crossinline onChange: (kProp: KProperty<*>, oldVal: T, newVal: T) -> Boolean
): ReadWriteProperty<Any?, T>
  • initialValue - начальное значение свойства.
  • onChange - это обратный вызов, вызываемый во время доступа для записи к свойству «до» его записи. Логическое возвращаемое значение указывает, разрешено ли свойству продолжить обновление newVal или будет «наложено вето», чтобы сохранить текущее oldVal.

Типичным вариантом использования Vetoable является проверка, которая принимает различные формы: ограничение целого числа определенным диапазоном, применение десятичного формата в строке для представления валюты и т. Д.

Достижения науки и астрономии за последние несколько десятилетий изменили ландшафт нашей Солнечной системы. Давайте напишем делегату, который заставляет Международный астрономический союз 24 августа 2006 г. проголосовать за изменение классификации планет.

private var planetName: String by Delegates.vetoable("Earth") { kProp, oldVal, newVal ->
    val isValid = newValue.toLowerCase() != "pluto"
    if (isValid) {
        println("Update ${kProp.name} from '$oldVal' to '$newVal'")
    } else {
        println("Veto Update on ${kProp.name}'")
    }
    isValid
}
    
planetName = "Mars"
println("My Planet: $planetName")
planetName = "Neptune"
println("My Planet: $planetName")
    
// This update will be vetoed
planetName = "Pluto"
println("My Planet: $planetName")
planetName = "55 Cancri e"
println("My Planet: $planetName")

Наблюдаемый

Делегат Observable уведомляет нас каждый раз, когда его значение изменяется. Его фабричный метод имеет следующее определение:

inline fun <T> observable(
    initialValue: T,
    crossinline onChange: (kProp: KProperty<*>, oldVal: T, newVal: T) -> Unit
): ReadWriteProperty<Any?, T>
  • initialValue - начальное значение свойства.
  • onChange - это обратный вызов, вызываемый во время доступа к свойству для записи «после» его записи.

Это возврат к классическому и повсеместному шаблону наблюдателя, используемому для управления событиями, связанными с изменением состояния объекта.

private var planetName: String by Delegates.observable("Earth") { kProp, oldVal, newVal ->
    println("${kProp.name} was updated from '$oldVal' to '$newVal'")
}
planetName = "Uranus"
planetName = "Jupiter"

Установка свойств с карты

Как бы вы преобразовали элементы карты в свойства объекта? Одно из решений предполагает перебор ключей и ручную установку значения для каждого свойства. Лучшим решением было бы использовать экземпляр карты в качестве делегата.

data class Satellite(val name: String, val discoveryDate: LocalDate)
val planetData = mapOf(
    "name" to "Jupiter",
    "adjective" to "Jovian",
    "distance" to 5.20f,
    "characteristics" to mapOf(
        "orbitalPeriod" to 11.82,
        "rotationPeriod" to 9.93,
        "volume" to 1321
    ),
    "totalSatellites" to 79,
    "galileanSatellites" to listOf(
        Satellite("Ganymede", LocalDate.of(1610, 1, 7)),
        Satellite("Callisto", LocalDate.of(1610, 1, 7)),
        Satellite("Europa", LocalDate.of(1610, 1, 8)),
        Satellite("Io", LocalDate.of(1610, 1, 8))
    )
)
val name: String by planetData
val adjective: String by planetData
val distance: Float by planetData
val characteristics: Map<String, Any> by planetData
val totalSatellites: Int by planetData
val galileanSatellites: List<Satellite> by planetData

-

println("$name is $distance AU from the Sun.")
    
println("A $adjective day is ${characteristics["rotationPeriod"]} hours")
    
println("A $adjective year is ${characteristics["orbitalPeriod"]} Earth years.")
    
println("You can fit ${characteristics["volume"]} Earths inside $name.")
println("There are $totalSatellites $adjective satellites.")
    
val period = Period.between(galileanSatellites[0].discoveryDate, LocalDate.now())
println(
"""The Galilean Satellites: ${galileanSatellites.map { it.name }}
  |were discovered by Galileo Galilei ${period.years} years ago
  |and were recognized as the first objects to orbit another planet.
""".trimMargin())
}

В дополнение к отображению примитивов мы можем отображать списки, сложные типы и вложенные карты.

Делегирование класса

Ключевое слово by можно использовать для делегирования реализации интерфейса другому классу. Посмотрим, насколько это полезно.

Представьте, что у нас есть следующий интерфейс

data class PlanetData(val name: String, val distance: Float)
interface PlanetDatabase {
    fun getFirstPlanet(): PlanetData
    fun getLastPlanet(): PlanetData
    fun randomPlanet(): PlanetData
}

С этим как его реализация

class PlanetDatabaseImpl : PlanetDatabase {
    private val planets = listOf(
        PlanetData("Mercury", 0.39f),
        PlanetData("Venus", 0.72f),
        PlanetData("Earth", 1.00f),
        PlanetData("Mars", 1.52f),
        PlanetData("Jupiter", 5.20f),
        PlanetData("Saturn", 9.31f),
        PlanetData("Uranus", 19.22f),
        PlanetData("Neptune", 30.07f)
    )
    override fun getFirstPlanet() = planets.first()
    override fun getLastPlanet() = planets.last()
    override fun randomPlanet() = planets.random()
}

Теперь у нас есть база данных, готовая к использованию. Но эта база данных будет реализована как часть SolarSystemRepository и будет существовать среди набора других подобных баз данных. Как нам включить PlanetDatabase?

Классическое наследование вроде

class SolarSystemRepository : PlanetDatabaseImpl()

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

Другое решение - прямая композиция.

class SolarSystemRepository {
    val planetDatabase : PlanetDatabase = PlanetDatabaseImpl()
}

Рассмотрение его как вложенного объекта будет работать, но мы можем сделать лучше.

Давайте воспользуемся ключевым словом by и делегируем реализацию PlanetDatabase PlanetDatabaseImpl.

Синтаксис для этого будет

class SolarSystemRepository : PlanetDatabase by PlanetDatabaseImpl()

Вот рабочий пример.

private const val ASTRONOMICAL_UNIT_TO_LIGHT_MINUTES = 8.32167464f
class repository : PlanetDatabase by PlanetDatabaseImpl()
// Note the different ways we can declare our references
val repository = SolarSystemRepository()
val database : PlanetDatabase = SolarSystemRepository()
// This will not compile
//  val databaseImpl : PlanetDatabaseImpl = SolarSystemRepository()

-

val firstPlanet = repository.getFirstPlanet()
println("${firstPlanet.name} is the closest planet to the sun at ${firstPlanet.distance} AU")

-

val lastPlanet = repository.getLastPlanet()
println("${lastPlanet.name} is the farthest planet from the sun at ${lastPlanet.distance} AU")

-

val randomPlanet = database.randomPlanet()
val lightTime = randomPlanet
    .distance
    .times(ASTRONOMICAL_UNIT_TO_LIGHT_MINUTES)
println("The Planet of the day is ${randomPlanet.name}")
println("It takes $lightTime minutes for light to reach ${randomPlanet.name} at ${randomPlanet.distance} AU")

Это прекрасно работает, поскольку дает прямую ссылку на нашу PlanetDatabase реализацию без необходимости ссылаться на вложенный объект. Что еще более важно, это позволяет нам делегировать реализацию дополнительных интерфейсов. Фактически мы смоделировали множественное наследование.

Но мы должны помнить о следующих предостережениях:

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

Последние мысли

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

Если вы хотите узнать больше об этом для других языков, ознакомьтесь со следующей статьей о Property Wrappers, чтобы понять, как это обрабатывается в Swift.