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