Рубрики
Без рубрики

Котлин Из Окопов

В последние несколько лет рост шумихи вокруг языка программирования Kotlin был примерно таким же… С тегами devexperts, java, kotlin, программное обеспечение.

В последние несколько лет рост шумихи вокруг языка программирования Kotlin был примерно таким же, как и рост курса Биткоина. Это пристальное внимание было усилено еще больше в мае 2017 года, когда Google объявила об официальной поддержке Kotlin для разработки для Android. Конечно, мы не могли не присоединиться к изучению этой темы и решили поэкспериментировать с Kotlin, используя его в нашем новом проекте на базе Android.

Kotlin – это статически типизированный язык программирования, который работает поверх JVM; он разработан компанией JetBrains. Kotlin сочетает в себе принципы объектно-ориентированного и функционального языка программирования. По словам разработчиков, он прагматичен, лаконичен и совместим. Приложения, написанные на нем, могут быть запущены на JVM или скомпилированы на JavaScript, а поддержка встроенной компиляции не за горами. Важно отметить, что язык был создан одновременно с инструментами разработки и настроен для использования с ними.

К настоящему времени Котлину было посвящено много статей и сделано много докладов о нем. Мы постараемся сосредоточиться не столько на отличительных особенностях языка, восхваляя его или ругая, сколько на нашем практическом опыте использования его особенностей.

Итак, давайте рассмотрим каждый из упомянутых аспектов…

Лучший инструмент

Разработчиком языка Kotlin является JetBrains, компания по разработке программного обеспечения, которая создала, вероятно, лучшую IDE для Java и многих других языков программирования. Несмотря на всю многословность языка Java, скорость записи остается очень высокой: среда “пишет код за вас”.

С Kotlin возникает ощущение, что вы купили новую клавиатуру и все еще не можете привыкнуть к ней, и вы не можете печатать сенсорным способом, несмотря на попытки. IntelliSense часто просто не успевает за скоростью набора текста; там, где IDE сгенерирует целый класс для Java, для Kotlin вы будете смотреть на индикатор выполнения. И проблема не только в новых файлах: активная навигация по проекту может привести к зависанию IDEA, поэтому вам поможет только его перезапуск.

Огорчает, что многие трюки, к которым вы привыкли, просто перестают работать. Например, Живые шаблоны. Android Studio – (версия IntelliJ IDEA для разработки Android) – поставляется с набором удобных шаблонов для часто используемых операций, таких как ведение журнала. Комбинация логотип + Вкладка вставит код, который запишет в журнал сообщение о том, какой метод с какими параметрами был вызван:

Log.d(TAG, "method() called with: param = [" + param + "]");

При этом этот шаблон “знает, как правильно определить метод и параметры, в зависимости от того, где вы его применили.

Однако в Kotlin это не работает. Более того, вам придется создать отдельный шаблон (например, klogd + Tab) и использовать его на основе языка программирования. Причина, по которой 100% совместимые с IDE языки требуют управления настройками дважды, остается для нас загадкой.

Легко ли этому научиться?

Kotlin, несмотря на возможность компиляции в JavaScript и, возможно, в машинный код (с использованием Kotlin. Native), в первую очередь является языком для JVM, и его цель – избавить разработчиков Java от ненужного и потенциально опасного (подверженного ошибкам) шаблона. Однако ошибочно предполагать, что написание на Kotlin с самого начала вообще будет Kotlin. Чтобы провести аналогию с языками, сначала вы будете писать на “испанском” с сильным акцентом Java. Мы убедились в этом, просматривая ваш собственный код через некоторое время, а также наблюдая за кодом коллег, которые только начинали изучать язык. Больше всего это проявлялось в работе с типами null и NonNull, а также в чрезмерной “многословности выражений”, привычке, от которой трудно избавиться. Кроме того, огромное количество новых функций, например, методов расширения, открывает Ящик Пандоры для написания заклинаний черной магии, добавляя чрезмерную сложность там, где это не требовалось, и делая код более запутанным и трудным для просмотра. Наглядным примером является перегрузка метода invoke (), которая позволяет маскировать его вызов под вызов конструктора, так что визуально создавая объект типа Dog, вы можете получить практически все, что угодно:

class Dog private constructor(){
  companion object{
      operator fun invoke(): String = "MAGIC"
  }
}

object DogPrinter {
  @JvmStatic
  fun main(args: Array) {
      println(Dog()) // MAGIC
  }

Таким образом, несмотря на то, что вам не потребуется больше недели, чтобы освоить синтаксис, может потребоваться несколько месяцев, чтобы научиться правильно применять возможности языка. В некоторых случаях потребуется более детальное изучение принципов работы определенного синтаксического сахара, включая изучение полученного байт-кода. При использовании Java вы всегда можете обратиться к таким источникам, как Эффективная Java , чтобы избежать многих неприятностей. Несмотря на то, что Kotlin был разработан для устранения “проблем, вызванных Java”, проблемы, вызванные Kotlin, еще предстоит раскрыть.

Нулевая безопасность

Язык Kotlin имеет сложную систему типов. Это позволяет вам, в большинстве случаев, избежать самой популярной проблемы в Java – исключения NullPointerException. Каждый тип имеет два параметра, в зависимости от того, может ли переменная этого типа быть null. Если вы можете присвоить переменной значение null, к типу добавляется знак вопроса. Пример:

val nullable: String? = null
val notNull: String = ""

Методы с обнуляемыми переменными вызываются с помощью .? оператор. Если такой метод вызывается для переменной, имеющей значение null, результат всего выражения также будет равен null; однако метод не будет вызван, и исключение NullPointerException не возникнет. Конечно, разработчики языка оставили способ вызвать метод для переменной с нулевым значением, несмотря ни на что, и получить исключение NullPointerException. Чтобы сделать это, вместо того чтобы? вам придется писать!!:

nullable!!.subSequence(start, end)

Эта строка вызывает раздражение, и код теряет свою аккуратность. Два последовательных восклицания, скорее всего, указывают на то, что такой код был написан намеренно. С другой стороны, трудно представить ситуацию, когда с помощью !! оператор был бы необходим.

Все выглядит хорошо до тех пор, пока весь код не будет написан на Kotlin. Однако, если Kotlin используется в существующем проекте на базе Java, все становится намного сложнее. Компилятор не может отследить, какие переменные будут иметь значение null, поэтому правильное определение типа практически невозможно. Для переменных из Java нулевые тесты недоступны во время компиляции. Таким образом, разработчик возьмет на себя ответственность за выбор правильного типа. Кроме того, для корректной работы автоматического преобразования с Java на Kotlin код на Java должен содержать аннотации @Nullable/@Nonnull. Нажмите на ссылку для получения полного списка поддерживаемых аннотаций.

Если, однако, значение null попадает из кода Java в Kotlin, сбой с исключением следующего вида неизбежен:

FATAL EXCEPTION: main
Process: com.devexperts.dxmobile.global, PID: 16773
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.devexperts.dxmobile.global/com.devexperts.dxmarket.client.ui.generic.activity.GlbSideNavigationActivity}: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter savedInstanceState

Разобрав байт-код, мы можем найти место, из которого было выдано исключение:

ALOAD 1
LDC "param"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull(Ljava/lang/Object;Ljava/lang/String;)V

Дело в том, что для всех параметров непубличных методов компилятор добавляет специальную проверку: вызывается стандартный библиотечный метод

kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, "param")

При необходимости вы можете отключить его с помощью директивы компилятора

-Xno-param-assertions

Эта директива должна быть зарезервирована для крайних случаев, поскольку она обеспечивает лишь незначительное повышение производительности за счет вероятной потери надежности.

Для всех классов, у которых есть метод get (), вы можете использовать оператор [] в Kotlin. Это очень удобно. Например:

val str = "my string"
val ch = str[2]

Однако оператор доступа к индексу может использоваться только с ненулевыми типами. Версия с возможностью обнуления не существует, и в подобном случае вам придется явно вызвать метод get ():

var str: String? = null
val ch = str?.get(2)

Свойства

Kotlin упрощает работу с полями классов. Вы можете получить доступ к полям как к обычным переменным, и будет вызван получатель или установщик нужного поля.

// Java code
public class IndicationViewController extends ViewController {
    private IndicationHelper indicationHelper;
    protected IndicationHelper getIndicationHelper() {
        return indicationHelper;
    }
}
// Kotlin code
val indicationViewController = IndicationViewController()
val indicationHelper = indicationViewController.indicationHelper

Все усложняется, если вам нужно переопределить средство получения класса Java в классе Kotlin. На первый взгляд кажется, что помощник по индикации – это полноценное свойство, совместимое с Kotlin. На самом деле это не так. Если мы попытаемся переопределить его “в лоб”, мы определенно получим ошибку компиляции:

class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
    protected open val indicationHelper = IndicationHelper(context, this)
}

Все сделано правильно: объявленное в подклассе свойство, получатель которого имеет сигнатуру, абсолютно идентичную получателю суперкласса. Что же тогда не так? Компилятор заботится о нас, предполагая, что переопределение произошло по ошибке. Есть даже дискуссия на эту тему на форуме Kotlin/|. Там мы можем узнать две важные вещи:

  1. ” Java-геттеры не рассматриваются как средства доступа к свойствам из Kotlin”
  2. ” Однако в будущем это может быть улучшено”

И, похоже, есть только один правильный способ достичь нашей цели (также с форума): создать закрытую переменную и в то же время переопределить геттер.

class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
    private val indicationHelper = IndicationHelper(context, this)
    override fun getIndicationHelper() = indicationHelper
}

100% Java-взаимодействие

Возможно, нам следовало поместить этот абзац первым, потому что именно Java-interop позволил новому языку так быстро завоевать такую популярность, что даже Google объявил об официальной поддержке языка для разработки для Android. К сожалению, и здесь мы не избежали сюрпризов.

Давайте рассмотрим простую вещь, известную всем разработчикам Java: модификаторы доступа или модификаторы видимости. В Java их четыре: общедоступный, частный, защищенный и закрытый для пакетов. Package-private используется по умолчанию, если вы не укажете иное. В Kotlin модификатор, используемый по умолчанию, является общедоступным, и он (как и защищенный, так и закрытый) вызывается и работает точно так же, как в Java. Однако модификатор package-private в Kotlin называется “internal” и работает несколько иначе.

Разработчики языка хотели решить проблему с потенциальной возможностью нарушения инкапсуляции при применении модификатора package-private. Решение состояло в том, чтобы создать пакет в клиентском коде с тем же именем, что и в коде библиотеки, и в предварительном определении метода. Такой трюк часто используется при написании модульных тестов, чтобы “не открывать” извне метод, созданный для целей тестирования. Так появился внутренний модификатор, который делает объект видимым внутри модуля.

Модуль имени применяется к следующему:

  • Модуль в проекте IntelliJ Idea
  • Проект в Maven
  • Исходный набор в Gradle
  • Набор исходных кодов, скомпилированных с помощью одного запуска ant-скрипта

Проблема в том, что internal на самом деле является публичным финалом. Таким образом, при компиляции на уровне байт-кода вы можете случайно переопределить метод, который вы не хотели переопределять. Из-за этого компилятор переименует ваш метод, чтобы такого не произошло, что, в свою очередь, сделает невозможным вызов этого метода из кода Java. Даже если файл с этим кодом находится в том же модуле, в том же пакете.

class SomeClass {
   internal fun someMethod() {
       println("")
   }
}

public final someMethod$production_sources_for_module_test()V

Вы можете скомпилировать свой код Kotlin с помощью внутреннего модификатора и добавить его в качестве зависимости в свой Java-проект. В этом случае вы можете вызвать этот метод там, где защищенный модификатор не позволит вам этого сделать, т.Е. вы получите доступ к частному API вне пакета (поскольку метод де-факто является общедоступным), хотя вы не сможете переопределить. Может возникнуть ощущение, что внутренний модификатор был разработан не как часть “Прагматического языка”, а скорее как функция IDE. И такое поведение могло быть достигнуто с помощью аннотаций, например, несмотря на многие утверждения о том, что в Kotlin зарезервировано очень мало ключевых слов, например, для сопрограмм, internal фактически привязывает ваш проект на основе Kotlin к IDE JetBrains. Если вы разрабатываете сложный проект, состоящий из большого количества модулей, и некоторые из этих модулей могут использоваться коллегами в качестве зависимостей в проекте на основе чистой Java, тщательно подумайте, прежде чем писать общие части в Kotlin.

Классы данных

Следующая, вероятно, одна из самых известных особенностей языка – это классы данных. Классы данных позволяют вам быстро и легко писать POJO-объекты, equals, hashCode, toString и другие методы, для которых компилятор напишет за вас.

Это действительно удобная вещь, однако остерегайтесь ловушек в совместимости с библиотеками, используемыми в проекте. В одном из наших проектов мы использовали Jackson для сериализации/десериализации JSON. Когда мы решили переписать некоторые POJOs в Kotlin, оказалось, что аннотации Jackson некорректно работают с Kotlin и необходимо было дополнительно подключить отдельный jackson-module-kotlin для совместимости.

В заключение

Несмотря на то, что эта статья может показаться критической, нам действительно нравится Kotlin! Особенно на Android, где Java застряла на версии 1.6, это стало настоящим спасением. Мы понимаем, что Котлин. Родной язык, сопрограммы и другие новые функции языка – очень важные и правильные вещи, но они просто могут быть полезны не всем. В то же время поддержка IDE – это то, что использует каждый разработчик, а медленная работа IDE сводит на нет все преимущества скорости, которые мы получаем, переходя на Kotlin с многословной Java. Хотя решение о переходе на Kotlin или о том, чтобы пока остаться на Java, должна принимать каждая отдельная команда, все, что мы хотим, – это поделиться проблемами, с которыми нам пришлось столкнуться, что, надеюсь, сэкономит вам драгоценное время.

Тимур Валеев, инженер-программист Devexperts

Александр Верещагин, инженер-программист Devexperts

Оригинал: “https://dev.to/devexperts/kotlin-from-the-trenches-da1”