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