
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()
}FirstFirstFirstSecondSecondFirstSecondSecond
Ответ
3
Спасибо за прочтение, надеюсь кому-то было интересно и полезно!
yavfast
В этом предложении очень много ложных утверждений.
Пару лет — это уже давно?
Android SDK и AndroidX уже полностью переписаны на Kotlin?
У Kotlin уже своя VM?
Sektor2350
А зачем своя VM? И зачем переписывать Android SDK и AndroidX если фишка Котлина как раз в практически полной совместимости с кодом Жабы. Котлин создан чтобы помочь в работе со старым кодом и создавать более лучший новый код, а не для того чтобы переписать его.
gevondov Автор
Говоря про основной язык, я имею ввиду, что большинство новых проектов и библиотек под андроид пишется именно на котлине, а не то, что все, что связано с андроидом переписано на котлин или должно быть переписано.
Ну google объявили котлин основным языком разработки под андроид в 2017-ом году. До этого он уже активно использовался. Мне кажется, что более 4 лет в it сфере, можно назвать словом 'давно', учитывая насколько быстро все меняется. Но естественно это субъективная оценка.
Уже ответили выше, мне также кажется, что нет никакой необходимости в переписывании всего на котлин, для того, чтобы он стал основным в моем понимании.
Если честно тоже не понял связи с VM. Думаю котлин отлично работает и с JVM.
Но спасибо за коментарий, постараюсь в дальнейшем не использовать неопределености со временем :)