5. Основы Kotlin. Ассоциативные массивы Maps и множества Sets

Распространенные операции над изменяемыми множествами

Рассмотрим основные операции, доступные над изменяемыми множествами.

  • set.add(element) добавляет элемент в множество
  • set.addAll(listOrSet) добавляет все элементы из заданного набора элементов
  • set.remove(element) удаляет элемент из множества
  • set.removeAll(listOrSet) удаляет все элементы из заданного набора элементов
  • set.retainAll(listOrSet) оставляет в множестве только элементы, которые есть в заданном наборе элементов
  • set.clear() удаляет из множества все элементы

Как и раньше, поддержание уникальности элементов выполняется автоматически.

Операции над null

Напоследок давайте чуть ближе познакомимся с объектом null — тем самым специальным значением, которое означает отсутствие чего-то в ассоциативном массиве. Данная «пустота» в Котлине не может появиться и использоваться просто так; если вы попробуете, например, присвоить null в переменную типа Int, то у вас ничего не получится. Дело в том, что значение null является допустимым только для специальных nullable типов; все обычные типы по умолчанию являются non-nullable.

Каким образом можно сделать nullable тип? Очень просто — если вы хотите сделать nullable версию Int, то нужно написать Int?. Знак вопроса, обычно выражающий сомнение, в данном контексте делает то же самое — сигнализирует, что этот тип может как иметь нормальное значение, так и значение null.

Есть ли еще какая-либо разница между типами Int и Int?, кроме того, что во втором может храниться null? Да, разница есть, и она заключается в том, что многие операции, возможные над Int, нельзя выполнить просто так над Int?. Представим, что мы хотим сложить два Int?.

fun addNullables(a: Int?, b: Int?): Int = a + b  // ERROR

Данный код не будет работать аж с целыми двумя ошибками: «Operator call corresponds to a dot-qualified call ‘a.plus(b)’ which is not allowed on a nullable receiver ‘a'» и «Type mismatch: inferred type is Int? but Int was expected». Эти ошибки вызваны как раз тем, что в переменной с типом Int? может храниться null, а как сложить что-то с тем, чего нет?

Так как операции с nullable типами являются потенциально опасными, в Котлине для работы с ними есть специальные безопасные операции и операторы, которые учитывают возможность появления null. Одним из таких операторов является элвис-оператор ?:, названный так в честь схожести с прической короля рок-н-ролла Элвиса Пресли. Рассмотрим, как он работает.

Выражение a ?: valueIfNull возвращает a в случае, если a не равно null, и valueIfNull в противном случае. Это позволяет предоставить «значение по умолчанию» для случая, когда в переменной хранится null. В нашем случае сложения двух чисел мы можем считать, что если какого-то числа нет (null), то оно равно нулю.

fun addNullables(a: Int?, b: Int?): Int = (a ?: 0) + (b ?: 0)

Еще один null-специфичный оператор — это оператор безопасного вызова ?.. Он используется в случаях, когда необходимо безопасно вызвать функцию над объектом, который может быть null. Выражение a?.foo(b, c)возвращает результат вызова функции foo с аргументами b и c над получателем a, если a не равен null; в противном случае возвращается null. Пусть нам нужно вернуть сумму элементов в nullable cписке.

fun sumOfNullableList(list: List<Int>?): Int = list?.sum()  // ERROR

Такой код не будет работать, потому что list?.sum() может вернуть null. Если подсмотреть в IntelliJ IDEA, то можно увидеть, что тип такого выражения, — Int?; чтобы исправить ситуацию с типом возвращаемого значения, можно воспользоваться элвис-оператором.

fun sumOfNullableList(list: List<Int>?): Int = list?.sum() ?: 0

Третий оператор, относящийся к null, но не являющийся безопасным, — это оператор !!. Его смысл очень прост — он делает из nullable выражения non-nullable выражение. В случае, если выражение имеет нормальное значение, эта операция завершается успешно. А вот если в выражении был null, это приводит к ошибке NullPointerException; по этой причине использовать этот оператор можно только тогда, когда вы уверены в том, что выражение не содержит null. Например, пусть вы работаете с ассоциативным массивом следующим образом.

val map = getMapOfNumbers()
if (map[key] != null) {
    val correctedNumber = map[key] + correction // ERROR
    // ...
}

Несмотря на то, что мы проверили значение в if, Котлин считает, что map[key] может вернуть null и выдает ошибку компиляции. Если мы считаем, что значение действительно не может поменяться, то можно воспользоваться !!.

val map = getMapOfNumbers()
if (map[key] != null) {
    val correctedNumber = map[key]!! + correction
    // ...
}

Кто-то может спросить: подождите, мы в самом начале этого урока делали ровно такую же операцию, и никакого оператора !! там не было. Вспомним, о чем идет речь.

fun shoppingListCost(
        shoppingList: List<String>,
        costs: Map<String, Double>): Double {
    var totalCost = 0.0

    for (item in shoppingList) {
        val itemCost = costs[item]
        if (itemCost != null) {
            totalCost += itemCost // No `!!` operator
        }
    }

    return totalCost
}

Что здесь происходит? Тут нам помогает такая вещь как «умные приведения типов» или смарт-касты. Компилятор Котлина, увидев, что неизменяемое выражение itemCost проверили на неравенство null, «стирает» с его типа знак вопроса внутри if; именно поэтому itemCost можно использовать без каких-либо безопасных операторов. Если присмотреться, то IntelliJ IDEA специальным образом подсвечивает подобные ситуации в редакторе кода.

Почему это не работает для map[key]? Именно потому что выражение map[key] не является неизменяемым, то есть результат его вычисления может быть разным в разные моменты времени; для того, чтобы сохранить безопасность кода, компилятор не делает никаких опасных предположений и отдает всю ответственность вам.

Если попробовать описать правила работы с null в компактном виде, то они могут выглядеть следующим образом.

  • Если у вас нет никакого осмысленного значения по умолчанию для объекта, проверьте на null в if или when и воспользуйтесь смарт-кастами
  • Если у вас есть какое-либо значение по умолчанию, можно применить элвис-оператор
  • Если вы хотите вызвать функции над nullable объектом, воспользуйтесь оператором безопасного вызова
  • Если вы точно-точно знаете, что nullable объект на самом деле не может содержать null, можете применить оператор !!

Этими правилами покрываются 99 из 100 ситуаций, с которыми вы можете столкнуться при программировании на Котлине. К тому моменту, как вы окажетесь в той самой «1 из 100» ситуации, вы уже будете разбираться в программировании достаточно, чтобы справиться с ней самостоятельно.

Упражнения

Откройте файл srс/lesson5/task1/Map.kt в проекте KotlinAsFirst.

Выберите любую из задач в нём. Придумайте её решение и запишите его в теле соответствующей функции.

Откройте файл test/lesson5/task1/Tests.kt, найдите в нём тестовую функцию — её название должно совпадать с названием написанной вами функции. Запустите тестирование, в случае обнаружения ошибок исправьте их и добейтесь прохождения теста. Подумайте, все ли необходимые проверки включены в состав тестовой функции, добавьте в неё недостающие проверки.

Переходите к следующему разделу.

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