Подробно разбираем популярные функции Kotlin

Продолжаем изучать полезные трюки, с которыми проще писать на языке Kotlin. В этой статье рассмотрим полезные функции Kotlin with() и withCorrectType().

Польза от использования with()

Предположим, мы не знакомы с функцией with(). Посмотрим, что написано в документации:

inline fun <T, R> with(receiver: T, block: T.() -> R): R (source)
Вызывает определенный функциональный блок с данным получателем в качестве аргумента и возвращает результат.

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

val receiver: String = "Fructos"

val block: String.() -> Unit = {
    println(toUpperCase())
    println(toLowerCase())
    println(capitalize())
}
        
val result: Unit = with<String, Unit>(receiver, block)

Первая строка описывает получатель (receiver) типа String. Внутри block можно применить любой метод на объект receiver без дополнительной классификации. И при этом в строке 9 Unit будет возвращен с типом функции block.

Таким образом работают block и receiver. Давайте на основе этого нового знания создадим более простой и нетипизированный пример:

val sugar = "Fructos"

with(sugar) {
    println(toUpperCase())
    println(toLowerCase())
    println(capitalize())
}

Функция высшего порядка block передается в качестве последнего параметра функции with(). А благодаря этому, ее можно вынести за круглые скобки.

Так чем может быть полезна функция with() в коде? Ее часто можно использовать для замены классификаторов, вроде view.show() и view.hide() на уровне представления компонентов интерфейса:

interface View {

    fun show()
    fun hide()
    fun reset()
    fun clear()
}

class Presenter(private val view: View) {

    fun present(isFructos: Boolean) = with(view) {
        if (isFructos) {
            show()
            hide()
        } else {
            hide()
            clear()
        }
    }
}

Рефакторинг с помощью функции Kotlin withCorrectType()

Если вы сталкивались с ощущением, что что-то с вашим кодом не так, но не ясно что именно – это нормально. Давайте рассмотрим такой пример:

abstract class Item

class MediaItem : Item() {
    val media = ...
}

class IconItem : Item() {
    val icon = ...
}

interface Renderer {
    fun render(view: View, item: Item)
}

class MediaItemRenderer : Renderer {

    override fun render(view: View, item: Item) {
        if (item !is MediaItem) {
            throw AssertionError("Item is not an instance of MediaItem")
        }

        view.showMedia(item.media)
        view.reset()
    }
}

class IconItemRenderer : Renderer {

    override fun render(view: View, item: Item) {
        if (item !is IconItem) {
            throw AssertionError("Item is not an instance of IconItem")
        }

        view.showIcon(item.icon)
        view.reset()
    }
}

Очевидно, что MediaItemRenderer и IconItemRenderer имеют похожую логику метода render(). К тому же теперь мы знаем как избежать использования классификатора (view).

class MediaItemRenderer : Renderer {

    override fun render(view: View, item: Item) = with(view) {
        if (item !is MediaItem) {
            throw AssertionError("Item is not an instance of MediaItem")
        }

        showMedia(item.media)
        reset()
    }
}

class IconItemRenderer : Renderer {

    override fun render(view: View, item: Item) = with(view) {
        if (item !is IconItem) {
            throw AssertionError("Item is not an instance of IconItem")
        }

        showIcon(item.icon)
        reset()
    }
}

Теперь можно попробовать создать функцию аналогичную with() и переместить все проверки типов в нее.

fun <T> withCorrectType(toBeChecked: Item, block: (T) -> Unit) {
    if (toBeChecked !is T) {
        throw IllegalArgumentException("Invalid type")
    }
    block.invoke(toBeChecked)
}

Казалось бы, самое время отрефакторить функцию render(), вот только код не компилируется.

Kotlin compile error

Эта ошибка связана со стиранием общих типов и встречается в Java, ее описание есть в документации Oracle:

Во время процесса стирания типа, компилятор Java стирает все параметры типа и заменяет каждую свою первую привязку, если параметр типа ограничен, или объект, если параметр типа неограничен.

В Kotlin возможно избежать стирания дженерика T. Используя комбинацию ключевых слов inline и reified, мы можем легко этого избежать:

class MediaItemRenderer: Renderer {

    override fun render(view: View, item: Item) = with(view) {
        withCorrectType<MediaItem>(item) {
            show { it.media() }
            reset()
        }
    }
}

class IconItemRenderer: Renderer {

    override fun render(view: View, item: Item) = with(view) {
        withCorrectType<IconItem>(item) {
            clear()
            show { it.icon() }
        }
    }
}

inline fun <reified T> withCorrectType(toBeChecked: Item, block: (T) -> Unit) {
    if (toBeChecked !is T) {
        throw IllegalArgumentException("Invalid type, 
        should be ${T::class.java.simpleName}")
    }
    block.invoke(toBeChecked)
}

Можно пойти дальше и использовать IntelliJ IDEA Kotlin Bytecode, чтобы понять, что компилятор Kotlin делает с reified и как в этом помогает inline:

public final class MediaItemRenderer {
   public final void render(@NotNull View view, @NotNull Item item) {
      if (!(item instanceof MediaItem)) {
         throw (Throwable)(new IllegalArgumentException("Invalid type, should be " 
         + MediaItem.class.getSimpleName()));
      } else {
         MediaItem it = (MediaItem)item;
         view.show((Function0)(new MediaItemRenderer$render$1$1$1(it)));
         view.reset();
      }
   }
}

public final class IconItemRenderer {
   public final void render(@NotNull View view, @NotNull Item item) {
      if (!(item instanceof IconItem)) {
         throw (Throwable)(new IllegalArgumentException("Invalid type, should be " 
         + IconItem.class.getSimpleName()));
      } else {
         IconItem it = (IconItem)item;
         view.clear();
         view.show((Function0)(new IconItemRenderer$render$1$1$1(it)));
      }
   }
}

Компилятор Kotlin оставляет ваш тип, так как он отмечен в качестве reified, что невозможно без inline-функции, так как код withCorrentType() должен вставляться точно в местах вызова.

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ