Kotlin. Лямбда vs Ссылка на функцию
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 initialized
at 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
Спасибо за прочтение, надеюсь кому-то было интересно и полезно!