Kotlin уже давно стал основным языком программирования на Android. Одна из причин, почему мне нравится этот язык, это то, что функции в нем являются объектами первого класса. То есть функцию можно передать как параметр, использовать как возвращаемое значение и присвоить переменной. Также вместо функции можно передать так называемую лямбду. И недавно у меня возникла интересная проблема, связанная с заменой лямбды ссылкой на функцию.
Представим, что у нас есть класс Button
, который в
конструкторе получает как параметр функцию onClick
class Button( private val onClick: () -> Unit) { fun performClick() = onClick()}
И есть класс ButtonClickListener
, который реализует
логику нажатий на кнопку
class ButtonClickListener { fun onClick() { print("Кнопка нажата") }}
В классе ScreenView
у нас хранится переменная
lateinit var listener: ButtonClickListener
и создается
кнопка, которой передается лямбда, внутри которой вызывается метод
ButtonClickListener.onClick
class ScreenView { lateinit var listener: ButtonClickListener val button = Button { listener.onClick() }}
В методе main
создаем объект
ScreenView
, инициализируем переменную
listener
и имитируем нажатие по кнопке
fun main() { val screenView = ScreenView() screenView.listener = ButtonClickListener() screenView.button.performClick()}
После запуска приложения, все нормально отрабатывает и выводится строка "Кнопка нажата".
А теперь давайте вернемся в класс ScreenView
и
посмотрим на строку, где создается кнопка - val button =
Button { listener.onClick() }
. Вы могли заметить, что метод
ButtonClickListener.onClick
по сигнатуре схож с
функцией onClick: () -> Unit
, которую принимает
конструктор нашей кнопки, а это значит, что мы можем заменить
лямбда выражение ссылкой на функцию. В итоге получим
class ScreenView { lateinit var listener: ButtonClickListener val button = Button(listener::onClick)}
Но при запуске программа вылетает со следующей ошибкой - поле listener не инициализированно
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property listener has not been initializedat lambdas.ScreenView.<init>(ScreenView.kt:6)at lambdas.ScreenViewKt.main(ScreenView.kt:10)at lambdas.ScreenViewKt.main(ScreenView.kt)
Чтобы понять в чем проблема, посмотрим чем отличается полученный Java код в обоих случаях. Опущу детали и покажу основную разницу.
При использовании лямбды создается анонимный класс
Function0
и в методе invoke
вызывается
код, который мы передали в нашу лямбду. В нашем случае -
listener.onClick()
private final Button button = new Button((Function0)(new Function0() { public final void invoke() { ScreenView.this.getListener().onClick(); }}));
То есть если мы передаем лямбду, наша переменная
listener
будет использована после имитации нажатия и
она уже будет инициализирована.
А вот что происходит при использовании ссылки на функцию. Тут
также создается анонимный класс Function0
, но если
посмотреть на метод invoke()
, то мы заметим, что метод
onClick
вызывается на переменной
this.receiver
. Поле receiver
принадлежит
классу Function0
и должно проинициализироваться
переменной listener
, но так как переменная
listener
является lateinit
переменной, то
перед инициализацией receiver
-а происходит проверка
переменной listener
на null
и выброс
ошибки, так как она пока не инициализирована. Поэтому наша
программа завершается с ошибкой.
Button var10001 = new Button;Function0 var10003 = new Function0() { public final void invoke() { ((ButtonClickListener)this.receiver).onClick(); }};ButtonClickListener var10005 = this.listener;if (var10005 == null) { Intrinsics.throwUninitializedPropertyAccessException("listener");}var10003.<init>(var10005);var10001.<init>((Function0)var10003);this.button = var10001;
То есть разница между лямбдой и ссылкой на функцию заключается в том, что при передаче ссылки на функцию, переменная, на метод которой мы ссылаемся, фиксируется при создании, а не при выполнении, как это происходит при передаче лямбды.
Отсюда вытекает следующая интересная задача: Что напечатается после запуска программы?
class Button( private val onClick: () -> Unit) { fun performClick() = onClick()}class ButtonClickListener( private val name: String) { fun onClick() { print(name) }}class ScreenView { var listener = ButtonClickListener("First") val buttonLambda = Button { listener.onClick() } val buttonReference = Button(listener::onClick)}fun main() { val screenView = ScreenView() screenView.listener = ButtonClickListener("Second") screenView.buttonLambda.performClick() screenView.buttonReference.performClick()}
-
FirstFirst
-
FirstSecond
-
SecondFirst
-
SecondSecond
3
Спасибо за прочтение, надеюсь кому-то было интересно и полезно!