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?.

Данный код не будет работать аж с целыми двумя ошибками: «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), то оно равно нулю.

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

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

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

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

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

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

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

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