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()
}
  1. FirstFirst

  2. FirstSecond

  3. SecondFirst

  4. SecondSecond

Ответ

3

Спасибо за прочтение, надеюсь кому-то было интересно и полезно!