Мир сходит с ума. Говорят, все новые мобильные проекты на Андроиде пишут исключительно на Котлине. В наше время очень опасно не учиться новым технологиям. Вначале твои знания устаревают, ты вылетаешь с работы, живешь у теплотрассы, дерёшься с бомжами за еду и умираешь в безвестности, так и не выучив функционального программирования. Поэтому я отправился на Курсеру изучать курс Kotlin for Java Developers и начал читать книжку (привет, abreslav, yole), поспрашивал друзей сами знаете откуда и вернулся назад с некой пустотой в душе. Помогите Олегу-путешественнику найти смысл в Котлине!
● В Java ты чаще понимаешь по узкому контексту, что происходит. a = b
— запись в поле или локал, a[1] = 2
— запись в массив. В Котлине за любым простым выражением может стоять сколь угодно сложный код из-за всяких умностей вроде перегрузки. Без IDE ничего не поймёшь. А IDE плохо, когда ты едешь в поезде и видишь, что свинговый жабоинтерфейс высасывает из ноутбука батарейку как вампир.
● Котлин даёт одинаковый API для коллекций и сиквенсов, из-за чего люди злоупотребляют цепочками map/filter на коллекциях, создавая кучу промежуточных неленивых копий. Стримы в джаве специально введены для различия между ленивой и неленивой коллекцией. Да, есть инспекция в IDE для этого — потому что инспекции призваны исправлять недостатки языков.
● Кстати, об IDE. Насколько хороша поддержка Kotlin в IntelliJ IDEA? Она действительно лучше, чем для Java? Есть большие сомнения. Может быть, кому-то из JB хватит духу проадвокатировать по данному вопросу.
● Котлин форсит использование it
, что приводит к нечитаемому коду. Что-нибудь типа seq.map { it -> foo(it, 1); }.map { it -> bar(it, 2); }.filter { it -> it.getBaz() > 0; }
. Что это вообще было? Имена переменным даны не зря! А тут получается монолог вроде «Возьмём это, прикрутим к нему то, потом его закрутим и если оно стало больше того, то наденем сверху шарнир».
● Цепочки вроде ?.let { foo(it); }?.let { bar(it); }
— это вообще ад и должны быть запрещены в декларации о правах человека. И это считается идиоматично, Карл. В отличие от нормального if. Читать такой код невозможно.
● От интеропа с джавой кровь идёт из глаз. А тут всякие JvmStatic и JvmName, и код превращается в цирк с конями.
Например, вот у нас есть такое:
class C {
companion object {
@JvmStatic fun foo() {}
fun bar() {}
}
}
Относительно помеченного аннотацией метода компилятор сгенерит и статический метод во внешнем по отношению к объекту классе, и метод экземпляра в самом объекте. Возможные варианты:
C.foo();
— работаетC.bar();
— синтаксическая ошибка, ибо метод не статическийC.Companion.foo()
; — остается метод экземпляраC.Companion.bar();
— единственный правильный способ
Оправились от красоты решения? Окей, пошли дальше. Теперь вы готовы понять и принять тот факт, что, например, нельзя одновременно объявить два таких метода:
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
Ведь их сигнатуры на уровне JVM совпадают: filterValid(Ljava/util/List;)Ljava/util/List;
Поэтому нужно подпихнуть специальный костыль:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
А как вам такое: в Kotlin нет checked exceptions. А в JVM-реальности они есть. Отряд специального назначения «Боевые протезы» имеет честь представить новый самоходный костыль @Throws
:
@Throws(IOException::class)
fun foo() {
throw IOException()
}
Можно долго рассуждать, что «джависты постоянно ноют, что тут всё не как в джаве». Но если вот это красиво, то что тогда ужасно?
В общем, рекомендуется открыть статью Java-to-Kotlin Interop и своими глазами посмотреть, как это выглядит.
● Автоматические геттеры/сеттеры с добавлением английского слова get и первой буквой проперти в большом регистре (видимо, в локали ENGLISH? Ведь регистр букв системно-зависим) — это страшно.
import java.util.Calendar
fun calendarDemo() {
val calendar = Calendar.getInstance()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // call getFirstDayOfWeek()
calendar.firstDayOfWeek = Calendar.MONDAY // call setFirstDayOfWeek()
}
if (!calendar.isLenient) { // call isLenient()
calendar.isLenient = true // call setLenient()
}
}
● Экстеншн-методы загрязняют публичный интерфейс такими вещами, о которых автор и подумать боялся.
Так как этой фичи нет в джаве, поясню. Можно написать любой метод, слева поставить имя «принимающего класса», и всё — он расширен. Давайте расширим MutableList
функцией swap
:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' относится к листу
this[index1] = this[index2]
this[index2] = tmp
}
val lst = mutableListOf(1, 2, 3)
lst.swap(0, 2) // 'this' внтури 'swap()' будет иметь значение 'lst'
Работа экстеншн-методов возможна, даже если автор специально сделал финальный класс, явно показав, что не хочет сторонних расширений. Получается что-то вроде изнасилования с особым цинизмом. И конечно, они ломают совместимость: что будет, если в следующей версии библиотеки автор добавит методы с теми же именами, но с другим возвращаемым типом? Он должен думать обо всех экстеншн-методах, которые любые люди могут добавить в тот же класс?
Кроме того, невозможно сделать оптимизированные реализации экстеншн-методов в конкретных подклассах. Хотя, казалось бы, вот эта фича могла бы произвести вау-эффект.
● Библиотека местами не продумана. Например, reduce.
Вот как выглядит reduce:
listOf(1, 2, 3).reduce { sum, element -> sum + element } == 6
Там есть только форма с identity (fold), но она не всегда применима.
listOf(1, 2, 3).fold(0) { sum, element -> sum + element } == 6
Кстати, почему Хабр подсвечивает эти две строчки по-разному? Аааа, уже неважно.
По сути, fold и reduce делают одно и то же, но fold требует определённого начального значения, а reduce использует в качестве этого начального значения первый элемент списка. Соответственно, форма без identity кидает исключение для пустой коллекции.
Всегда ли такое поведение всем нужно? Почему не вернуть какое-нибудь Optional
и дать пользователю самому решить, что делать в случае пустой коллекции? Да или хоть null вернуть, раз уж это null-friendly язык.
● Давайте ещё навалим про библиотеку. Нафига в стандартную библиотеку языка, который поддерживает дата-классы, включили пары? Это ж прямое поощрение плохого кода.
Напоминаю, дата-классы выглядят так:
data class User(val name: String, val age: Int)
val duncan = User("Duncan MacLeod", 426)
val (name, age) = duncan
println("$name, $age years of age") // печатает "JaDuncan MacLeodne, 426 years of age"
Пара выглядит вот так:
val (name, age) = Pair("Java", 23)
println("$name, $age years of age") // тоже печатает "Java, 23 years of age"
А все потому, что внутри:
public data class Pair<out A, out B>(
public val first: A,
public val second: B
)
Совершенно очевидно, что среднестатистический быдлокодер забьёт писать свои классы на второй день использования, и код превратится в кошмарную пародию на лисп. Складываем огуречные жопки с сотнями нефти, пишем фильтры-франкенштейны и в продакшен. Быстро, просто, нечитаемо.
● Очень странный момент — возможность не указывать возвращаемый тип метода (особенно публичного).
Совсем недавно был случай в C++, от которого меня чуть не разорвало от злости. Программа падала в произвольном месте, а я не понимал — почему. Оказалось, в C++ можно не писать return в методе, который согласно сигнатуре должен что-то возвращать. Это не синтаксическая ошибка согласно стандарту, а undefined behavior. Соответсвенно, программа в рантайме падает с произвольной ошибкой. Чудесный язык — в нем есть специальный синтаксис для неработающих методов. С тех пор я очень аккуратно проверяю, что мы обещали вернуть из метода и что отдали на выходе. Эдакая параноидальная привычка.
И вот теперь, в лучшем в мире языке Kotlin мы можем вообще не указывать возвращаемый тип. Это провоцирует людей писать нечленораздельную лапшу, в которой и ничего не понятно. Если метод a
вызывает метод b
, а тот метод c
, а тот содержит в теле выражение when
, в котором в ветках ещё три метода вызываются d
, e
и f
, попробуй пойми тип метода а
!
fun a(check: Int) = b(check)
fun b(check: Int) = c(check)
fun c(check: Int) =
when (check) {
1 -> d()
2 -> e()
else -> f()
}
fun d() = "result 1";
fun e() = "result 2";
fun f() = "result 3";
fun main(args: Array<String>) {
println(::a.returnType)
for (i in 1..3) println(a(i).javaClass.name)
Причём вначале вроде всё было просто и понятно, а в процессе эволюции поменялось, и капец. Меняешь возвращаемый тип метода f
, и у тебя автоматом меняется возвращаемый тип метода а
совсем в другом пакете, и ты не понимаешь, что происходит.
Изначально в нашем примере выхлоп выглядел вот так:
kotlin.String
java.lang.String
java.lang.String
java.lang.String
Но стоит только поменять определения функций на вот такие:
fun d() = "1";
fun e() = 100500;
fun f() = listOf<String>();
И результат тут же изменится на
kotlin.Any
java.lang.String
java.lang.Integer
kotlin.collections.EmptyList
Никакой кристаллизации API. Для публичных методов явная спецификация API должна быть священной коровой, а Kotlin её не требует.
Пожалуй, для начала достаточно. Из этой статьи может показаться, что все в Котлине плохо, но это очевидно, не так. Как минимум, зарплата Kotlin-разработчика обычно неплоха :-)
В недавнем докладе на Joker 2018 (есть слайды), Паша (asm0dey) Финкельштейн отмечал, что на бэкенде Kotlin помогает писать более красивый и лаконичный код (но не всегда это получается), на нем получаются более выразительные тесты, с ним работает GraalVM, и всё это с примерами для Spring, Spring Security, Spring Transactions, jOOQ, и т.п.
Стоит ли переходить на Kotlin с Java для мобильных приложений? Неясно. В любом случае, Kotlin интересный. Давайте в нём покопаемся!
Минутка рекламы. Уже на этой неделе, 8-9 декабря 2018, пройдет конференция Mobius. На ней, скорей всего, можно будет пересечься со множеством людей, реально использующих Kotlin, и узнать, зачем и как они это делают. Места все еще есть, а вот времени уже почти не осталось, так что, если хотите прийти, сейчас у вас последний шанс. Билеты можно приобрести на официальном сайте.
Комментариев нет:
Отправить комментарий