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

Котлин – Хорошее, Плохое и Уродливое

Обсуждение крутых и не очень крутых функций языка программирования Kotlin. Помечен как kotlin, java.

Привет, ребята!

Это статья, которую я хотел написать уже довольно давно. Я видел свою изрядную долю Kotlin , а также использовал его в производстве. Kotlin – это язык на основе JVM, разработанный компанией Jetbrains, известной своими идеями и инструментами для разработчиков. Котлин очень интересен с точки зрения языка, у него много достоинств, но также есть некоторые подводные камни и неудобный выбор дизайна. Давайте сразу перейдем к делу!

Я мог бы написать целые книги о том, что мне нравится в Котлине. Вот краткое изложение основных моментов.

Точки с запятой необязательны

Не “необязательно”, как в “ну, большую часть времени это работает без них, просто иногда это имеет значение”, а как в “используйте их или отбросьте, программа не изменится”. Меньше беспорядка в коде, и мое запястье действительно этому радуется.

NULL – это отдельный тип

Строка (не- нулевая Строка) и Строка? (строка с нулевым значением) – это два разных типа в Kotlin. Если вы придерживаетесь методологии Kotlin, ваши программы не смогут выдать страшное исключение NullPointerException

Функции расширения

Вы когда-нибудь хотели нулевой безопасный вариант myStringVariable.IsNullOrEmpty() в Java? Что ж, Котлин тебя прикроет! Вы можете писать статические методы (с определенными сигнатурами) и вызывать их так, как если бы они были обычными методами-членами. Это также знаменует смерть страшного StringUtils класс, полный статических методов. Все это теперь может быть методами расширения!

Ввод потока и вывод типа

Для всех локальных переменных вы просто используете var , и компилятор сам определит тип. В Java 10 это тоже есть, но с более сильной системой типов Kotlin это еще более ценно. Kotlin также не требует оператора приведения. Если вы проверяете, что объект является экземпляром некоторый класс, затем внутри этого блока if переменная будет обрабатываться как таковая, без необходимости опускать ее вручную. Котлин называет это “умными слепками”.

Финал стал легким

В Java модификатор final необходим для того, чтобы локальная переменная не могла быть назначена. Это очень полезное свойство, потому что оно устраняет большую группу потенциальных ошибок. Однако последняя строка myString – это полный рот. Вряд ли кто-то захочет иметь с этим дело. В Kotlin количество символов для конечных и не конечных переменных равно точно то же самое: var myString для изменяемых и val myString для неизменяемых переменных.

Интеллектуальная стандартная библиотека

Стандартная библиотека Kotlin – это сокровищница, полная приятных сюрпризов. Вас когда-нибудь беспокоило, что в Java нет элегантного способа использовать блокировку с помощью try-with-resources ? Ну, в Котлине у вас есть блокировка.с блокировкой{ DoWork() } . Это мелочь, но она действительно помогает. Как часто вы забывали снять блокировку в предложении наконец ? Или разблокировал блокировку чтения вместо блокировки записи? Стандартная библиотека Kotlin полна таких маленьких помощников.

Вот еще один: изменяемый список(T... элементы) . Компилятор Kotlin достаточно умен, чтобы вычислить T если вы предоставите элементы. Таким образом, вызовы этого метода будут иметь одну из двух форм:

// empty list, need to specify the type argument explicitly
val myList = mutableListOf()

// pre-filled list, no need for type argument
val yourList = mutableListOf("Hello", "World"!)

Можем ли мы остановиться здесь на мгновение и оценить, как этот код читается как проза в обоих случаях? Кроме того, Kotlin различает изменяемые коллекции и коллекции, доступные только для чтения, оставаясь при этом совместимыми с Java. Это само по себе является настоящим подвигом и делает код Kotlin намного более устойчивым к незначительным ошибкам (эй, кто изменил эту коллекцию!?).

Потоки, обтекаемый

Если вы хотите отфильтровать Список в Java, вам нужно сделать это:

List newList = oldList.stream()
    .filter( s -> s.startsWith("a") )
    .collect(Collectors.toList());

Это довольно много текста, если все, что мы действительно хотели сделать, это:

List newList = oldList.filter(s -> s.startsWith("a"));

Тогда в чем смысл использования stream() ? Окружив ваши фильтры (и операции с картами и…) потоком() и сбором (...) , потоки Java могут быть ленивыми . Однако вы можете возразить, что в лени нет смысла, если все, что вы хотите сделать, – это один фильтр. И вы правы, для одной операции фильтрации поток является излишним, как синтаксически, так и концептуально. Но в Java Список #фильтр(...) просто не существует (вы можете использовать статический служебный класс, но да ладно, это не одно и то же).

Котлин решает эту проблему очень элегантным способом:

// single filter, quick and easy
val newList = oldList.filter { it.startsWith("a") }

// streams for multiple filters (same syntax!)
val newList = oldList.asSequence().filter { it.startsWith("a") }.toList()

Итак, Kotlin предоставляет вам как ленивые цепочки на основе потоков (называемые последовательностями ), так и одноразовые операции. Самое лучшее в этом то, что синтаксис для фильтра в списке точно то же самое, что и для фильтра в последовательности. Если вы работаете с существующим кодом и понимаете, что вам нужен другой фильтр, просто вставьте asSequence() до и to Lust() после, чтобы сделать его ленивым, и оставьте существующий фильтр на месте.

Да здравствует Итератор

Итераторы – это очень простая и мощная концепция. Они существуют в Java с версии 1.0 (что говорит о многом). Однако в Java к итераторам относятся довольно по-мачехиному. Вы не можете выполнить цикл для над итератором так же, как вы можете над Коллекция . Коллекция#addAll принимает только другие Коллекция s в качестве аргумента, а не Повторяющийся s. Черт возьми, вы даже не можете сделать iterator.stream() . Я никогда не понимал почему.

Kotlin устраняет все вышеупомянутые проблемы. Работа с итераторами в Kotlin так же приятна, как и работа с коллекциями, без потери преимуществ итераторов:

for(element in iterator) { ... } /* works! */

iterator.asSequence().filter { ... } /* works! */

myList.addAll(iterable) /* works! */

Взаимодействие с Java

… это действительно хорошо. Как в “Действительно, ОЧЕНЬ хорошо”. Вы можете смешивать исходные тексты Kotlin и Java (и двоичные файлы!) в том же проекте без проблем. Однако есть некоторые подводные камни; подробнее об этом мы расскажем позже.

Множество Платформ, один Язык

Kotlin переводит байт-код JVM, а также JavaScript. Собственные реализации также находятся в разработке. Я думаю, само собой разумеется, что это огромная сделка.

При всех своих достоинствах у Kotlin есть и некоторые недостатки. На самом деле они не являются прорывными, но, тем не менее, заслуживают внимания.

Нет статического модификатора

В Kotlin нет ключевого слова static . Это вызывает некоторое раздражение, если вы просто хотите определить этот один метод полезности. То, что вы должны сделать вместо этого, это:

class MyClass {
   companion object {
       @JvmStatic
       fun myStaticMethod(/*...*/) { /*...*/ }
   }
}

Это некрасиво. В защиту Котлина, для констант есть const (что примерно совпадает с общедоступными статическими конечными членами в Java), и вы можете объявлять функции верхнего уровня вне любого класса (что фактически делает их статическими ). Но если вы действительно хотите статический метод, способ, описанный выше, является единственным вариантом (насколько я знаю).

Объект -компаньон первоначально был введен для облегчения построения одиночных объектов. Сопутствующий объект – это одноэлементный экземпляр класса, в котором он находится. Я чувствую, что это было плохое решение. Во-первых, потому, что статические методы полезны и не должны требовать такого большого синтаксиса, а во-вторых, потому, что шаблон Singleton не настолько подробен, чтобы оправдывать эту стенографию, а также не настолько полезен, чтобы заслужить такую выдающуюся языковую функцию.

Ключевое слово “открыть”

Все классы в Kotlin по умолчанию являются окончательными . Позвольте этому немного осмыслиться.

class MyClass {

}

class SubClass : MyClass { /* NOPE: compile error! MyClass is final! */

}

Это только верхушка айсберга. Представьте себе такие фреймворки, как Spring или Hibernate, которые генерируют байт-код во время выполнения, расширяющий ваш класс. Нет, не могу этого сделать – класс окончательный . В мгновение ока все ваши классы Kotlin внезапно стали несовместимыми практически с любым широко используемым фреймворком. Есть два способа обойти эту проблему:

  • Либо объявите свой класс как открытый класс MyClass , либо
  • Добавьте плагин компилятора в компилятор Kotlin.

Ни один из вариантов не очень хорош. Разработчики языка Kotlin исходят из того, что расширяемость должна быть обязательной. Только в том случае, если ваш класс предназначен для расширения, он должен быть расширен. На мой взгляд, это рассуждение ошибочно: лучшие практики защитного программирования скажут вам, что вы должны подготовить свой код для работы в самых неожиданных средах. Если вы пишете класс, который потенциально ломается при его подклассе, вы написали плохой класс. Котлин дает здесь совершенно неправильный стимул, просто закрывая все.

Конструктор В изобилии

Котлин как язык имеет действительно странный фетиш для конструкторов. Взглянуть:

// primary constructor
class PersonA(val firstName: String, val lastName: String) { }

// regular constructor
class PersonB {

    val firstName: String
    val lastName: String

    constructor(firstName: String, lastName: String){
        this.firstName = firstName
        this.lastName = lastName
    }

}

Оба примера выше делают одно и то же . Вы можете возразить, что вариант с “первичным конструктором” намного короче, а значит, и лучше. Я не согласен по следующим причинам:

  • Основной конструктор всегда должен перечислять (и назначать) все поля. Это очень раздражает, когда вам требуется дополнительное временное поле для целей локального кэширования.
  • Строка объявления класса становится чрезвычайно длинной.
  • Если вы расширяетесь из базового класса, у которого есть основной конструктор, вы…
    • необходимо самостоятельно объявить первичный конструктор
    • должен вызывать основной конструктор базового класса
    • должен сделать все это в объявлении класса

Таким образом, как только вы заперты в рабочем процессе основного конструктора, вы больше не сможете вырваться на свободу. Ваши объявления классов быстро вырастают до огромных размеров. Подумайте об этом:

class Student(val firstName: String, val lastName: String, val studentId: String) : Person(firstName,lastName) {

}

Конечно, вы получаете большой пробег от такой линии. В нем объявляется подкласс, объявляются поля, объявляется конструктор, указывается базовый класс и, наконец, указывается, как должны заполняться унаследованные поля. Это очень высокая плотность информации прямо здесь. Мне все еще это не нравится. Это слишком много информации, собранной вместе, и человеку может быть очень трудно ее разобрать. Я не рекомендую использовать первичные конструкторы (за исключением классов данных, где у вас нет выбора), но иногда вы вынуждены использовать их из-за классов в стандартной библиотеке.

Классы данных – блестящая концепция с наполовину испеченным исполнением

Классы данных сами по себе и сами по себе являются блестящей идеей. Взглянуть:

data class Person(val firstName: String, val lastName: String)

Вы указываете поля класса и их типы, и вы получаете:

  • указанные поля
  • добытчики/сеттеры
  • конструктор, который делает именно то, что вы ожидаете
  • Хэш-код
  • равняется
  • К струне
  • клон
  • различные утилиты

… все бесплатно, не записывая их . Это означает, что ваш Хэш-код() никогда не будет синхронизирован с вашим equals() . Ваш toString() никогда не забудет напечатать поле, которое вы недавно добавили в класс. Все это возможно, потому что для этих вещей нет текстового представления; компилятор просто генерирует байт-код напрямую. Это действительно классная концепция и значительно превосходит создание всех вышеупомянутых вещей с помощью IDE (потому что сгенерированный код может легко выйти из синхронизации).

Проблема в том, что это не было продумано до конца. Основная проблема заключается в том, что вы не можете использовать наследование при написании классов данных. Классы данных не могут расширять какой-либо базовый класс и сами являются окончательными (нет, вы не можете использовать open здесь тоже). Причиной этого решения было то, что при попытке ввести наследование что-то может сломаться (в частности, в утилитах клонирования). На мой взгляд, это полная чушь. UML и Ecore показали, как правильно создавать классы данных с помощью утилит множественного наследования и клонирования.

Отсутствие вариантов наследования резко ограничивает применимость классов данных. Даже простые объекты передачи данных (DTO) или объекты JPA (яркие примеры, когда классы данных были бы так полезны!) Часто требуют наследования.

Обилие ключевых слов

В Kotlin очень большой набор Ключевых слов и операторов . В Kotlin (как и в C#) есть понятие “Мягких ключевых слов”, означающее, что определенное слово считается ключевым словом только в определенных контекстах, но может использоваться в качестве идентификатора (например, имени переменной) во всех других местах кода. Некоторые из них довольно… загадочны и ужасно специфичны, такие как крест в строке . Несмотря на это, это делает язык довольно трудным для изучения и еще более трудным для освоения, так как вы должны знать все эти ключевые слова. При обычном использовании вам не нужно больше, чем вам нужно было бы в Java. Но если вы хотите прочитать и понять библиотечные функции, вы будете сталкиваться с ними довольно часто. Я лично считаю, что многие из этих ключевых слов были бы лучше выражены аннотациями ( например @Фактический вместо модификатора фактический ).

Я абсолютно рекомендую любому Java-программисту хотя бы поближе познакомиться с Kotlin. В очень многих областях это гораздо лучший язык, чем Java. Он включает в себя всю инфраструктуру и библиотеки Java, а также добавляет множество разумных вариантов дизайна. Он имеет улучшенную систему типов для повышения безопасности и проверки ошибок во время компиляции. Одного того факта, что по умолчанию он нулевой -безопасный, должно быть достаточно, чтобы оправдать использование Kotlin поверх Java. Однако это не идеально. Kotlin облегчает написание короткого и лаконичного кода, но его также можно использовать для создания беспорядка, казалось бы, случайных комбинаций символов, которые никто больше не может расшифровать через два дня. Однако, несмотря на его недостатки, слабые стороны и иногда странный выбор дизайна, это отличный язык.

Есть ли у вас какой-либо опыт, которым вы могли бы поделиться с этим языком? Используете ли вы его в производстве, и довольны ли вы им? Оставьте комментарий ниже.

Оригинал: “https://dev.to/martinhaeusler/kotlin—the-good-the-bad-and-the-ugly-3jfo”