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