Этот пост является копией предыдущих постов на Medium ( начальный , последующие ) Но так как я планирую удалить свою учетную запись Medium, я переместил их сюда.
Котлин – замечательный язык программирования. Примерно после 12 лет программирования на Java, работая с Kotlin, мне захотелось надеть очки после долгих лет прищуривания: здесь так много всего, что можно любить.
Но, как и в любых отношениях, некоторые причуды вы обнаруживаете только позже в вашей совместной жизни. После переноса все большего и большего количества кода Java в код Kotlin я заметил кое-что довольно странное и, откровенно говоря, немного раздражающее.
Именно так Котлин обрабатывает функциональные интерфейсы .
Давайте вернемся в прошлое, в мир без лямбд. Это было ужасно многословно!
interface JavaInterface { String doSomething(Item item); } String delegateWork(JavaInterface f) { return f.doSomething(item); } void doWork() { delegateWork(new JavaInterface() { @Override public String doSomething(Item item) { return "Item = " + item; } }); }
Наконец-то Java 8 дала нам лямбды, и мы смогли избавиться от большого количества кода и сосредоточиться на том, что важно. Кроме того, нас не заставляли писать собственный функциональный интерфейс для каждой простой функции, мы могли бы просто использовать некоторые из предоставленных oracle, такие как: java.util.function. Функция R> R>
@FunctionalInterface interface JavaInterface { String doSomething(Item item); } String delegateWork(JavaInterface f) { return f.doSomething(item); } String delegateOtherWork(Function- f) { return f.apply(item); } void doWork() { delegateWork(item -> "Item = " + item); delegateOtherWork(item -> "Item = " + item); }
Все было хорошо, пока вы не поняли, что, хотя у вас теперь есть типы функций, они все еще не являются первоклассными гражданами языка. Хотите доказательств? Угадайте, сколько “Типов функций” пришлось ввести в Java? Один? Три? Пять?
Не верите мне, посмотрите сами: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
И если вам этого недостаточно, добавьте joomla в микс, и у вас будет доступ еще к 35: https://github.com/jOOQ/jOOL/tree/master/jOOL/src/main/java/org/jooq/lambda/function Потому что кому бы не понравилось встретить сигнатуру метода, которая выглядит так:
Function5> higherOrder(Function12 , String, Integer, Long, String, Double, Optional >>)
😜 примечание: jOOL на самом деле довольно аккуратная библиотека, и ее стоит проверить.
Теперь давайте добавим Kotlin в смесь. В Котлине функции выполняют первоклассные граждане. Так что нет необходимости запоминать десятки слегка отличающихся типов функций. Вам просто нужно запомнить синтаксис типа функции Kotlin:
(Parameter1Type, Parameter2Type, ParameterNType) -> ReturnType
Вот и все, вот и все, что нужно сделать.
Итак, хорошо, почему мы здесь, в чем проблема?
Как упоминалось ранее, по мере того, как я переносил все больше и больше кода с Java на Kotlin. Я столкнулся с некоторыми проблемами при работе с пользовательскими функциональными интерфейсами. Потому что иногда вам нужна эта дополнительная описательность.
Давайте вернемся к нашему примеру Java 8
@FunctionalInterface interface JavaInterface { String doSomething(Item item); } class JavaComponent { private Item item = new Item(); String delegateWork(JavaInterface f) { return f.doSomething(item); } String delegateOtherWork(Function- f) { return f.apply(item); } }
Теперь давайте используем его из кода Kotlin
delegateWork { "Print $it" } delegateOtherWork { "Print $it" }
Хорошо, это здорово, именно то, что мы ожидали! Хорошо, теперь давайте перенесем это Компонент Java
класс для Kotlin. Обратите внимание, что мы изменили java.util.функцию. Функция<Элемент, строка>
к типу функции Kotlin (Предмет) -> Строка
class KotlinComponent(private val item: Item = Item()) { fun delegateWork(f: JavaInterface): String { return f.doSomething(item) } fun delegateOtherWork(f: (Item) -> String): String { return f.invoke(item) } }
Давайте посмотрим, что происходит, когда мы используем эти функции более высокого порядка из кода Java.
delegateWork(item -> "Print: " + item); delegateOtherWork(item -> "Print: " + item);
Ничего необычного, как и ожидалось, мы можем использовать одну и ту же лямбду для обоих методов. Давайте посмотрим, что произойдет, когда мы сделаем то, что мы ожидаем в Котлине:
delegateWork { "Print $it" } Error: Kotlin: Type mismatch: inferred type is () -> String but JavaInterface was expected
Что случилось? Похоже, компилятор не может понять, что сигнатура лямбды совпадает с методом функционального интерфейса. Похоже, компилятор не может понять, что сигнатура лямбды совпадает с методом функционального интерфейса.
Поэтому мы должны четко сказать, чего мы ожидаем:
delegateWork(JavaInterface { "Print $it" })
Я думаю, что это довольно разочаровывает но это не так уж плохо. Теперь давайте посмотрим, что произойдет, когда мы также перенесем интерфейс на Kotlin:
interface KotlinInterface { fun doSomething(item: Item): String } class KotlinComponent(private val item: Item = Item()) { fun delegateWork(f: KotlinInterface): String { return f.doSomething(item) } fun delegateOtherWork(f: (Item) -> String): String { return f.invoke(item) } }
Когда мы используем компонент Kotlin
класс из Java, как и ожидалось, ничего не меняется, лямбды остаются точно такими же. Что, если мы используем его из кода Котлина:
delegateWork { "Print $it" } Error: Kotlin: Type mismatch: inferred type is () -> String but KotlinInterface was expected
Похоже, то ЖЕ самое преобразование снова завершается неудачей. Теперь, что, если мы просто прямо упомянем интерфейс, как мы делали раньше?
delegateWork(KotlinInterface { "Print $it" }) Error: Kotlin: Interface KotlinInterface does not have constructors
Это тоже не помогло. Нам нужно создать анонимный объект, чтобы он работал:
delegateWork(object : KotlinInterface { override fun doSomething(item: Item): String { return "Print $item" } })
Черт возьми! Это похоже на повторную работу с Java 7. К сожалению, это связано с тем, что Kotlin еще не поддерживает преобразование SAM для интерфейсов Kotlin, поэтому нам приходится создавать этот анонимный объект. Смотрите также: https://youtrack.jetbrains.com/issue/KT-7770 https://stackoverflow.com/a/43737962/611032
Итак, как мы можем избежать этих подробных анонимных объектов и по-прежнему использовать пользовательское имя для функции? Мы используем псевдоним типа:
/** * Very helpful comment. */ typealias KotlinFunctionAlias = (Item) -> String fun delegateAliasWork(f: KotlinFunctionAlias): String { return f.invoke(item) }
Итак, теперь мы можем передать лямбду так, как мы ожидали, и у нас все еще есть преимущество пользовательского имени для функции.
delegateAliasWork { "Print $it" }
Значит, все хорошо, дело закрыто, пора идти домой. К сожалению, не совсем.
Потерялся в переводе
Одна незначительная проблема с псевдонимами типов заключается в том, что, хотя вы можете назвать тип функции, вы не можете назвать имя метода:
val iface: JavaInterface = JavaInterface { "Print $it" } iface.doSomething(item) val alias: KotlinFunctionalAlias = { item -> "Print $item" } alias.invoke(item) alias(item)
Выбор подходящих имен для псевдонима типа и переменной может смягчить проблему. К счастью, мы, разработчики, отлично разбираемся в именовании вещей 😜
Безопасность типа
Более серьезная проблема заключается в том, что, хотя псевдоним типа дает нам другое имя, на самом деле они не являются разными типами, поэтому на самом деле мы не безопасны для типов.
Давайте рассмотрим пример Java с двумя функциональными интерфейсами, которые имеют одинаковую сигнатуру метода.
JavaInterface1 f1 = item -> "Print " + item; JavaInterface2 f2 = item -> "Print " + item; f1 = f2; Error: java: incompatible types: JavaInterface2 cannot be converted to JavaInterface1
Это то, чего мы ожидали бы, мы не хотим смешивать здесь яблоки и апельсины.
Что произойдет, если мы сделаем то же самое с нашими псевдонимами типа Kotlin? (Я думаю, вы знаете, к чему я клоню)
var f1: KotlinFunctionAlias1 = { item -> "Print $item" } var f2: KotlinFunctionAlias2 = { item -> "Print $item" } var f3: (Item) -> String = { item -> "Print $item" } f1 = f2 f2 = f3 f1 = f3
Это прекрасно работает, компилятор не жалуется, потому что, как я уже упоминал, на самом деле они не являются разными типами. Все они просто: (Элемент) -> Строка
Итак, давайте быстро рассмотрим различные способы, которыми мы можем справиться с отсутствующим преобразованием SAM в Kotlin для интерфейсов Kotlin, а также их плюсы и минусы
Оставьте функциональные интерфейсы в качестве интерфейсов Java
; + Хорошая совместимость с Java + Поддержка пользовательского имени метода + Безопасность типов
– Необходимо добавить префикс Kotlin lambda к имени интерфейса – Необходимы дополнительные круглые скобки – Необходимо поддерживать код Java
Используйте псевдоним типа для типов функций Kotlin
+ Хорошая совместимость с Java + Простота использовать
– Небезопасно для ввода – Нет имени пользовательского метода
Используйте онлайн-классы
Еще один вариант, который мы еще не обсуждали, – это использование экспериментальных встроенных классов Kotlin. Вы могли бы “обернуть” функцию Kotlin встроенным классом.
inline class KotlinInlineInterface(val doSomething: (Item) -> String) fun delegateInlineWork(f: KotlinInlineInterface): String { return f.doSomething.invoke(item) } delegateInlineWork(KotlinInlineInterface { "Print $it" })
Несмотря на то, что это работает, я не думаю, что это подходящий способ использования онлайн-классов. Также совместимость Java в настоящее время не поддерживается: Также совместимость Java в настоящее время не поддерживается:
Всегда используйте типы функций Kotlin
Да, вы могли бы просто использовать ((Параметр) -> Возвращает
типы везде. Часто этого бывает достаточно, но по мере роста вашего приложения его может становиться все труднее читать и поддерживать, а также он может быть более подвержен ошибкам.
Жить с анонимными объектами
Конечно, если вы не возражаете, вы можете просто жить с анонимными объектами, надеясь, что когда-нибудь Kotlin будет поддерживать полное преобразование SAM и использовать замечательную интеграцию IDE для переноса ваших анонимных объектов в лямбды
\ (ツ) /
На Reddit состоялась короткая дискуссия: На Reddit состоялась короткая дискуссия:
С тех пор я получил ответ от Романа Елизарова на эту тему
Я попробовал упомянутый вариант компилятора Kotlin:
// Gradle Kotlin DSL tasks.withType{ kotlinOptions.freeCompilerArgs += "-XXLanguage:+NewInference" } // Gradle Groovy DSL compileKotlin { kotlinOptions { freeCompilerArgs += "-XXLanguage:+NewInference" } }
Если вы больше интересуетесь другими системами сборки, обратитесь к документации Kotlin ( Maven / Ant ), чтобы узнать, как передавать аргументы компилятора Kotlin.
Проблема решена?
Сначала давайте посмотрим, что происходит, когда мы используем функциональный интерфейс Kotlin в коде Kotlin:
fun delegateWork(f: KotlinInterface): String { return f.doSomething(item) } delegateWork { item -> "Print: $item" } Error: Type mismatch: inferred type is (Nothing) -> TypeVariable(_L) but KotlinInterface was expected
Как насчет явного указания интерфейса?
delegateWork(KotlinInterface { item -> "Print $item" } Error: Interface KotlinInterface does not have constructors
Облом! Нам все еще нужен анонимный объект.
Как насчет использования функционального интерфейса Java в Kotlin код?
fun javaInterface(f: JavaInterface) { val res = f.doSomething(item) output(res) } javaInterface { item -> "Print: $item" }
Наконец: именно то, что мы ожидали . Все хорошо, пиво заслужено!
Терпение юный джедай
Если вы наблюдательны, вы увидите это во время сборки:
w: ATTENTION! This build uses unsafe internal compiler arguments: -XXLanguage:+NewInference This mode is not recommended for production use, as no stability/compatibility guarantees are given on compiler or generated code. Use it at your own risk!
Так что же это значит? Это означает то, что здесь сказано: это еще не совсем безопасно в использовании. Но, зная, что JetBrains работает в этом направлении, я бы предложил, чтобы мы сейчас действовали следующим образом (от наиболее благоприятного к наименее благоприятному)
- Сохраняйте функциональные интерфейсы в виде кода Java
- Используйте псевдонимы типов для типов функций Kotlin (если вы можете смириться с возможным смешиванием яблок и апельсинов)
- Жить с анонимными объектами
Спасибо за чтение. Как всегда, я открыт для критики и отзывов.
Оригинал: “https://dev.to/ranilch/functional-interfaces-self-loathing-in-kotlin-4bce”