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

Буквально на днях я работал над системой кэширования и нашел отличный способ использовать (возможно, злоупотреблять) еще одну функцию Kotlin, известную как Расширения, для вызова функции по нулевой ссылке без сбоев. Типичный способ использования расширения - это добавить функциональность к классу, которым вы не владеете, но они позволяют вам вызывать функцию непосредственно в этом классе. За кулисами расширение разрешается статически. Если вы когда-либо писали класс Util, то вы хорошо знаете, как это выглядит. Поскольку они разрешаются статически, хотя они дают нам еще одно преимущество, мы можем передать пустую ссылку и обработать ее без сбоев; однако на сайте вызова мы просто вызываем нулевую безопасную функцию для объекта.

Загружать или не загружать?

Допустим, мы хотим сделать сетевой вызов, чтобы загрузить аватар, который может время от времени меняться. У нас есть несколько вариантов:

  1. Мы всегда можем запросить изображение и потратить лишнюю полосу пропускания.
  2. Мы можем запросить его только один раз и рискуем показать устаревшие данные.
  3. Мы можем встретиться где-то посередине и загрузить изображение первый раз и снова только по прошествии некоторого времени.

В этом случае давайте спустимся по маршруту встреча посередине. Давайте рассмотрим базовую реализацию этого:

data class ImageCache(
    val timeRetrieved: Long,
    val image: Image, 
    val cacheExpiration: Long = 300000
)
object ImageLoader {
    
    val cacheMap = mutableMapOf<String, ImageCache>()
fun load(resource: String): Image {
        val previousAttempt = cacheMap[resource]
        return previousAttempt?.run {
            if (timeRetrieved > (currentTime + cacheExpiration) {
                loadImage(resource)
            } else {
                image
            }
        } ?: loadImage(resource)
    }
}

Эта реализация должна работать, но на самом деле это не самое чистое решение. Сначала мы должны обработать случай, когда previousAttempt имеет значение null. Во-вторых, поскольку мы делаем нулевой безопасный вызов для previousAttempt, мы должны обрабатывать ситуацию, когда он равен нулю, что заставляет нас вызывать loadImage второй раз после оператора elvis. Давайте очистим это с помощью функции расширения.

data class ImageCache(
    val timeRetrieved: Long,
    val image: Image, 
    val cacheExpiration: Long = 300000
)
// Extension function added...
fun ImageCache?.isInvalidated(): Boolean {
    return this == null || 
        timeRetrieved > (currentTime + cacheExpiration)
}
object ImageLoader {
    
    val cacheMap = mutableMapOf<String, ImageCache>()
fun load(resource: String): Image {
        val previousAttempt = cacheMap[resource]
        return if (previousAttempt.isInvalidated()) {
            loadImage(resource)
        } else {
            previousAttempt.image
        } // No need for Elvis now :D
    }
}

Это гораздо лучшее решение. Для начала нам не нужно беспокоиться о том, что previousAttempt допускает значение NULL, мы можем вызвать функцию isInvalidated так же, как мы вызываем ненулевой тип для ImageCache. Он также включает в себя функции определения того, действителен ли кеш в самом ImageCache, избавляя нас от загромождения кода, который просто хочет знать, действителен ли кеш. Поскольку это решение также устраняет необходимость делать нулевой безопасный вызов, оно избавляет нас от оператора elvis, который дублировал наш loadImage вызов. Наконец, мы вызываем функцию, как если бы она была частью ImageCache, что помогает улучшить читаемость.

Когда мы должны это использовать?

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

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

val ImageCache?.isInvalidated: Boolean 
    get {
        return this == null || 
            timeRetrieved > (currentTime + cacheExpiration)
    }

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

Первоначально опубликовано на www.activecampaign.com.