Распространенные операции над изменяемыми множествами
Рассмотрим основные операции, доступные над изменяемыми множествами.
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
, найдите в нём тестовую функцию — её название должно совпадать с названием написанной вами функции. Запустите тестирование, в случае обнаружения ошибок исправьте их и добейтесь прохождения теста. Подумайте, все ли необходимые проверки включены в состав тестовой функции, добавьте в неё недостающие проверки.
Переходите к следующему разделу.