Магия функций в Kotlin


Оглавление (нажмите, чтобы открыть):

Введение в Kotlin: функции, переменные, условия, циклы

Подготовка среды и решение простейшей задачи

Начать введение мы хотели бы с видео про то, как установить IntelliJ IDEA (также можно использовать Android Studio) и плагин Kotlin на ваш компьютер и решить простейшую задачу сложения двух чисел из файла и записи результата в другой файл. Видео располагается по ссылке.

Немного про магию среды IntelliJ IDEA

Как вы могли заметить в видео, набор кода происходит в некоторые моменты скачкообразно. Это совсем не эффект монтажа видео, а мощные функции среды IntelliJ IDEA:

  • Автодополнение. Чтобы набирать код быстрее, используйте сочетание Ctrl + Space после первых введенных символов.
  • Live templates. Позволяют разворачивать конструкции кода нажатием клавиши табуляции ⇥. Например, «main + tab» разворачивается в функцию main, «sout + tab» разворачивается в println().
  • Дублирование строки. Для того чтобы скопировать текущую строку в строку ниже, используйте сочетание клавиш Ctrl + D или ⌘ + D для Mac OS.
  • Инкрементальное выделение. Позволяет увеличивать зону выделения с каждым нажатием сочетания клавиш Ctrl + W или ⌘ + W для Mac OS от слова до всего файла на одну степень вложенности.

Теперь у вас установлен необходимый инструментарий, чтобы писать упражнения и оттачивать свое мастерство.

Определение функций

В общем случае у функций необходимо указывать возвращаемый тип:

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

В случае, если функция не возвращает значимый тип, то либо указывается Unit:

либо ничего не указывается:

Определение локальной переменной

Локальные переменные подразделяются на 2 категории. Те, которые определяются лишь однажды (только для чтения), объявляются следующим образом:

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

Строковые шаблоны

Вывод значений переменных в строке можно использовать с помощью конструкции «$«:

Условные выражения

Язык Kotlin позволяет писать условные выражения вида:

и писать в более компактном представлении:

Проверки на null

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

Kotlin запоминает факт проверки, если такая уже происходила:

is-проверки и автоматическое приведение типов

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

Цикл for

Цикл while

Оператор условия when

Оператор условия when предназначен для описания пространства вариантов выражения, что позволяет обходиться без сложных конструкций оператора if:

Генерация рядов и ключевое слово in

Также с помощью in можно проверить, что элемент не содержится в ряде:

Оператор in позволяет проверить принадлежность к коллекции.

И наконец, с помощью in можно делать итеративный перебор элементов ряда:

Можно также итерироваться с произвольным шагом или в обратном направлении:

Использование функциональных литералов для выполнения операций filter и map над коллекциями

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

В данном примере мы определили функцию-расширение hello() для типа String. Когда мы вызываем эту функцию на строке «world», мы используем значение строки в качестве аргумента ее функции hello().

Заключение

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

Как вернуть несколько значений из функции в Котлине, как мы делаем это быстро?

Как вернуть 3 отдельных значения данных одного и того же типа (Int) из функции в Kotlin?

Я пытаюсь вернуть время суток, мне нужно вернуть Час, Минуту и ​​Второе как отдельные целые числа, но все в одном направлении от одной и той же функции, возможно ли это?

В быстрой мы делаем это, как после,

можем ли мы добиться этого в Котлине?

PS: Я знаю, что могу использовать Array или Hashmap для этого, но я хочу знать, есть ли что-то в kotlin, как это быстро.

Вы не можете создавать произвольные кортежи в Kotlin, вместо этого вы можете использовать классы данных . Один из вариантов – использование встроенных Pair и Triple классов, которые являются общими и могут содержать два или три значения соответственно. Вы можете использовать их в сочетании с объявлениями о деструкции следующим образом:

Вы также можете разрушить List или Array , до пяти первых элементов :

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

вы можете вернуть несколько значений, как показано ниже

см. эту документацию

более подробно см. эту ссылку

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

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

Но, очевидно, недостатком является то, что вы должны использовать объявление деструкции, чтобы дать ему правильное имя свойства.

Кажется, вы осознаете очевидный ответ на создание определенного класса для обработки времени. Поэтому, я думаю, вы пытаетесь избежать небольших проблем создания класса или доступа к каждому элементу массива и т. Д. И ищете кратчайшее решение с точки зрения дополнительного кода. Я бы предложил:

и использовать его с деконструкцией:

Если вам нужны 4 или 5 возвращаемых значений (вы не можете деконструировать более 5), перейдите к Array :

PS: вы можете использовать меньше переменных, чем есть значения при деконструкции, например var (a, b) = getTime() но вы не можете использовать больше или вы получите ArrayIndexOutOfBoundsException

Общая встроенная функция


0 Cilenco [2020-09-06 19:10:00]

Скажем, у меня есть объект, который помогает мне десериализовать другие объекты из хранилища:

Методы getBookList и getPersonList — это функции расширения, которые я написал. Их логика почти одинакова, поэтому я думал, что могу объединить их в один метод. Моя проблема — общий тип возврата. Методы выглядят так:

Есть ли какая-то магия Kotlin (возможно, с встроенными функциями), которую я могу использовать для обнаружения типа List и генерировать эти методы? Я думаю, что проблема будет val item = T() которая не будет работать для общих типов, не так ли? Или это возможно с встроенными функциями?

2 ответа

3 Решение marstran [2020-09-06 19:29:00]

Вы не можете вызвать конструктор родового типа, потому что компилятор не может гарантировать, что он имеет конструктор (тип может быть из интерфейса). Что вы можете сделать, чтобы обойти это, так это передать «создателя» -function в качестве параметра вашей функции. Как это:

Теперь вы можете вызвать функцию следующим образом:

1 Zoe [2020-09-06 19:30:00]

Замечания:

Как отмечал marstran в комментарии, для этого требуется, чтобы класс имел конструктор с нулевым аргументом, или он будет генерировать исключение во время выполнения. Компилятор не предупредит вас, если конструктор не существует, поэтому, если вы выбрали этот путь, убедитесь, что вы фактически передаете класс с помощью конструктора с нулевым аргументом.

Вы не можете инициализировать общие типы, в Kotlin или Java. По крайней мере, не в «традиционном» способе. Вы не можете этого сделать:

В Java вы передадите Class и получите конструктор. Очень простой пример:

Вы можете сделать то же самое в Котлине, но у Котлина есть ключевое слово reified которое упрощает его. Для этого требуется встроенная функция, что означает, что вы измените свою функцию на:

Однако readExternal нет в Any , что будет представлять проблемы. Единственное исключение — если у вас есть функция расширения для Any или общего типа с этим именем и вводом.

Если это специфично для некоторых классов, то вы не можете сделать это так, если у вас нет общего родителя. Для примера:

Не получится. Нет общего родителя, кроме Any , и Any не имеет readExternal . Метод определяется вручную в каждом из них.

Вы можете создать общий родительский элемент в качестве интерфейса или абстрактного класса (если он еще не существует) и использовать , и у вас будет доступ к нему.

Разумеется, вы можете использовать рефлексию, но она немного сложнее и добавляет некоторые исключения, которые вам нужно обработать. Я не рекомендую это делать; Я лично использовал суперкласс.

Functions

Function declarations

Functions in Kotlin are declared using the fun keyword:

Function usage

Calling functions uses the traditional approach:

Calling member functions uses the dot notation:

Parameters

Function parameters are defined using Pascal notation, i.e. name: type. Parameters are separated using commas. Each parameter must be explicitly typed:

Default arguments

Function parameters can have default values, which are used when a corresponding argument is omitted. This allows for a reduced number of overloads compared to other languages:

Default values are defined using the = after type along with the value.

Overriding methods always use the same default parameter values as the base method. When overriding a method with default parameter values, the default parameter values must be omitted from the signature:

If a default parameter precedes a parameter with no default value, the default value can only be used by calling the function with named arguments:

If the last argument after default parameters is a lambda, it can be passed in either as a named argument or outside the parentheses:

Named arguments

Function parameters can be named when calling functions. This is very convenient when a function has a high number of parameters or default ones.

Given the following function:

We could call this using default arguments:

However, when calling it with non-default, the call would look something like:

With named arguments we can make the code much more readable:

and if we do not need all arguments:

When a function is called with both positional and named arguments, all the positional arguments should be placed before the first named one. For example, the call f(1, y = 2) is allowed, but f(x = 1, 2) is not.

Variable number of arguments ( vararg) can be passed in the named form by using the spread operator:

On the JVM: the named argument syntax cannot be used when calling Java functions because Java bytecode does not always preserve names of function parameters.

Unit-returning functions

If a function does not return any useful value, its return type is Unit . Unit is a type with only one value — Unit . This value does not have to be returned explicitly:

The Unit return type declaration is also optional. The above code is equivalent to:

Single-expression functions

When a function returns a single expression, the curly braces can be omitted and the body is specified after a = symbol:

Explicitly declaring the return type is optional when this can be inferred by the compiler:

Explicit return types

Functions with block body must always specify return types explicitly, unless it’s intended for them to return Unit , in which case it is optional. Kotlin does not infer return types for functions with block bodies because such functions may have complex control flow in the body, and the return type will be non-obvious to the reader (and sometimes even for the compiler).

Variable number of arguments (Varargs)

A parameter of a function (normally the last one) may be marked with vararg modifier:

allowing a variable number of arguments to be passed to the function:

Inside a function a vararg -parameter of type T is visible as an array of T , i.e. the ts variable in the example above has type Array .

Only one parameter may be marked as vararg . If a vararg parameter is not the last one in the list, values for the following parameters can be passed using the named argument syntax, or, if the parameter has a function type, by passing a lambda outside parentheses.

When we call a vararg -function, we can pass arguments one-by-one, e.g. asList(1, 2, 3) , or, if we already have an array and want to pass its contents to the function, we use the spread operator (prefix the array with * ):

Infix notation

Functions marked with the infix keyword can also be called using the infix notation (omitting the dot and the parentheses for the call). Infix functions must satisfy the following requirements:


  • They must be member functions or extension functions;
  • They must have a single parameter;
  • The parameter must not accept variable number of arguments and must have no default value.

Infix function calls have lower precedence than the arithmetic operators, type casts, and the rangeTo operator. The following expressions are equivalent:

  • 1 shl 2 + 3 is equivalent to 1 shl (2 + 3)
  • 0 until n * 2 is equivalent to 0 until (n * 2)
  • xs union ys as Set is equivalent to xs union (ys as Set )

On the other hand, infix function call’s precedence is higher than that of the boolean operators && and || , is — and in -checks, and some other operators. These expressions are equivalent as well:

  • a && b xor c is equivalent to a && (b xor c)
  • a xor b in c is equivalent to (a xor b) in c

See the Grammar reference for the complete operators precedence hierarchy.

Note that infix functions always require both the receiver and the parameter to be specified. When you’re calling a method on the current receiver using the infix notation, you need to use this explicitly; unlike regular method calls, it cannot be omitted. This is required to ensure unambiguous parsing.

Function scope

In Kotlin functions can be declared at top level in a file, meaning you do not need to create a class to hold a function, which you are required to do in languages such as Java, C# or Scala. In addition to top level functions, Kotlin functions can also be declared local, as member functions and extension functions.

Local functions

Kotlin supports local functions, i.e. a function inside another function:

Local function can access local variables of outer functions (i.e. the closure), so in the case above, the visited can be a local variable:

Member functions

A member function is a function that is defined inside a class or object:

Member functions are called with dot notation:

For more information on classes and overriding members see Classes and Inheritance.

Generic functions

Functions can have generic parameters which are specified using angle brackets before the function name:

For more information on generic functions see Generics.

Inline functions

Inline functions are explained here.

Extension functions

Extension functions are explained in their own section.

Higher-order functions and lambdas

Higher-Order functions and Lambdas are explained in their own section.

Tail recursive functions

Kotlin supports a style of functional programming known as tail recursion. This allows some algorithms that would normally be written using loops to instead be written using a recursive function, but without the risk of stack overflow. When a function is marked with the tailrec modifier and meets the required form, the compiler optimises out the recursion, leaving behind a fast and efficient loop based version instead:

This code calculates the fixpoint of cosine, which is a mathematical constant. It simply calls Math.cos repeatedly starting at 1.0 until the result doesn’t change any more, yielding a result of 0.7390851332151611 for the specified eps precision. The resulting code is equivalent to this more traditional style:

To be eligible for the tailrec modifier, a function must call itself as the last operation it performs. You cannot use tail recursion when there is more code after the recursive call, and you cannot use it within try/catch/finally blocks. Currently, tail recursion is supported by Kotlin for JVM and Kotlin/Native.

Знакомство с Kotlin для Android за один день

Имея за плечами опыт с Java, я понял, что синтаксис Kotlin похож на Java, но в то же время может сильно отличаться. Kotlin — очень мощный язык, в котором много синтаксического сахара, что может немного напрягать.

В этой статье мы рассмотрим необходимый минимум, который поможет начать работать с Kotlin. Давайте сразу к делу.

1. Переменные

Чтобы объявить переменную, используйте var.

Чтобы объявить константу, используйте val. val в Kotlin — это как final в Java.

Чтобы инициализировать переменную как null, используйте оператор “?”. Если оператор “?” не указан, то компилятор не позволит присвоить переменной значение null.

Используйте companion object для определения статической переменной.

Используйте lateInit для отложенной инициализации переменной.

2. Функции

Простая функция в Kotlin выглядит следующим образом:

Здесь функция getNumber() имеет область видимости public, не имеет параметров и возвращает Int.

Попробуем создать private функцию с несколькими параметрами.

Функция getStringLength() имеет область видимости private и два параметра, возвращает Int.

Как насчёт статической функции?

getOddLengthString() — статическая функция, которая принимает параметр и возвращает строку. Тип String указан с символом “?”. Это означает, что функция может возвращать значение NULL.

3. Циклы for, while, when

В цикле for, в Kotlin, используется ключевое слово “in” для доступа к элементам коллекции.

Можно также получить доступ к индексам элементов в цикле for.

Цикл while в Kotlin такой же, как и в Java. Тут ничего нового 🙂

А вот оператор switch в Java был создан проще. Kotlin использует ключ слово when для переключения между условиями, и это гораздо более мощный и краткий способ.

4. Null Safety (Null безопасность)

В Java, чтобы избежать исключения NullPointerException, мы можем использовать блок “if”. Например:

Но в Kotlin мы можем пропустить блок “if” и переписать наш код следующим образом:

“?.”известен как оператор безопасного вызова, и приведённый выше пример показывает, что его можно использовать в цепочках. Это помогает нам писать чистый, простой код и в то же время избегать NPE.

В случае, если вы хотите обработать сценарий, где объект равен null, вы можете использовать “?:”. Этот оператор, называют — Elvis operator.

5. Классы (конструкторы, методы, наследования)

Ключевое слово “open” указывает, что класс может быть унаследован. Класс, который не является “open”, такой же, как класс final в Java. Простой пример класса в Kotlin выглядит так:


Ниже приведён более сложный пример класса и наследования в Kotlin:

Обратите внимание, что в наших классах нет методов getter и setter. Вместо этого мы обращаемся к свойствам объекта следующим образом:

6. Singleton (синглтоны)

Синглтоны в Kotlin реализуются с помощью ключевого слова “object”. Имея опыт с Java, использование “object” вместо “class”, кажется немного странным. Подробнее об этом можно прочитать в официальных документах Kotlin:

7. Интерфейсы

Базовый интерфейс в Kotlin выглядит следующим образом:

Наследование интерфейса довольно просто. Но передача интерфейса функции немного отличается. Посмотрите на этот пример:

8. Type Casts (приведение типов)

Чтобы проверить, является ли объект экземпляром определённого класса, мы используем операторы “is” и “!is”.

Чтобы предотвратить выбрасывание исключений, можно использовать оператор безопасного приведения “as?”, который возвращает null при сбое. Это называется safe typecast в Kotlin.

В этом случае при сбое приведения типов, возвращается значение null, что предотвращает выбрасывание исключений.

9. Обработка исключений

Выбрасывание и обработка исключений практически такие же, как и в Java.

Введение в Котлин

Введение

С тех пор, как Apple выпустила язык программирования Swift для iOS, многие разработчики Android хотели использовать аналогичный язык для разработки Android. Если вы один из тех разработчиков, вам понравится Kotlin, язык JVM, который очень похож на Swift.

Мастер Йода рекомендует:  Спецификация RSS 2.0

Kotlin — это статически типизированный язык, разработанный JetBrains, синтаксис которого более выразителен и краток, чем язык Java. С такими возможностями, как функции более высокого порядка, лямбда выражения, перегрузка операторов, строковые шаблоны и т.д., у Kotlin есть намного больше возможностей, чем Java. Поскольку Java и Kotlin очень совместимы, их можно использовать вместе в одном проекте.

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

Предпосылки

Чтобы следовать за мной, вам понадобятся:

  • хорошее понимание Java.
  • последняя версия компилятора Kotlin. Кроме того, вы можете использовать онлайн-playground.

1. Классы

Чтобы создать класс в Kotlin, вам нужно использовать ключевое слово class . Например, вот как вы создаете пустой класс Person:

Добавление свойств

Класс обычно имеет свойства и функции-члены (также называемые методами). Давайте добавим два свойства в классе Person , name типа String и age типа Int .

Как вы можете видеть, синтаксис для создания переменных несколько отличается от синтаксиса Java. Чтобы создать переменную в Kotlin, вы должны использовать ключевое слово var . Однако, если вы хотите, чтобы ваша переменная была переменной только для чтения/назначения-единожды, используйте вместо этого ключевое слово val .

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

Чтобы создать переменную, которая может содержать null , вам нужно добавить ? после типа переменной. Например:

Теперь, когда у нас есть класс, можно легко создать его экземпляр:

Нет, у Kotlin нет ключевого слова new . Когда экземпляр создан, вы можете получить доступ к его свойствам так же, как и в Java:

Использование конструкторов

Инициализация отдельных свойств нашего экземпляра, как мы только что сделали, не является хорошей практикой кодирования. Лучший способ сделать это — использовать конструктор. Синтаксис Kotlin’s для создания такого конструктора очень компактен:

Фактически, если вам нечего добавить в свой класс, вам не нужны фигурные скобки. Следующий код работает очень хорошо:

Возможно, вам уже очевидно, что вы не можете добавить к этому конструктору собственный код. Этот конструктор, который является частью заголовка класса, называется основным конструктором.

Чтобы добавить к вашему классу дополнительные конструкторы, известные как вторичные конструкторы, вы должны использовать ключевое слово constructor . Вторичные конструкторы должны делегировать основному конструктору, используя ключевое слово this . Давайте добавим вторичный конструктор в наш класс, который инициализирует значение свойства с именем email:

Чтобы создать экземпляр с использованием вторичного конструктора, вы пишете что-то вроде этого:

Добавление функций-членов

В Kotlin функции создаются с использованием ключевого слова fun . Давайте добавим простую функцию-член с именем isEligibleToVote, которая возвращает значение Boolean :

Обратите внимание, что тип возвращаемого значения указывается в конце заголовка функции. Вы можете вызвать этот метод так же, как и в Java:

Создание расширений

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

Например, чтобы добавить расширение с именем isTeenager в класс Person , вы напишите следующий код вне класса:

Эта функция особенно полезна, когда вы хотите расширить классы, не принадлежащие кодовой базе вашего проекта. Например, следующий фрагмент кода добавляет расширение containsSpaces в класс String :

Создание производных классов

При создании производного класса важно помнить следующее:

  • Вы должны использовать : вместо ключевого слова Java extends .
  • Заголовок базового класса должен иметь аннотацию open .
  • Если ваш базовый класс имеет конструктор, который принимает параметры, ваш производный класс должен инициализировать эти параметры в самом заголовке.

Давайте создадим класс с именем Employee, который происходит от Person :

Переопределение функций-членов

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

Например, чтобы переопределить метод isEligibleToVote , вы добавляете следующий фрагмент кода в класс Employee :

Создание статических методов

Kotlin не позволяет создавать статические методы. Тем не менее, это позволяет вам создавать функции уровня пакета, которые не принадлежат ни одному классу.

Метод main — это, пожалуй, самый известный статический метод. Если вы хотите добавить метод main в пакет с именем com.tutsplus.code.tutorial, тогда ваш код будет выглядеть так:

2. Функции

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

Создание функций единичного выражения

Если функция возвращает значение одного выражения, вы можете использовать оператор = после заголовка функции, за которым следует выражение для определения функции.


Например, чтобы добавить метод к классу Person , который возвращает true , если человек является восьмидесятилетним, вы должны написать:

Как вы можете видеть, этот сокращенный синтаксис более компактен и читабелен.

Функции более высокого порядка и лямбда-выражения

Функции более высокого порядка — это функции, которые могут возвращать функции или принимать функции в качестве параметров. С другой стороны, выражение лямбда — это просто функция, которая не имеет имени. Обычно лямбда-выражения и функции более высокого порядка используются вместе.

Рассмотрим следующий пример, демонстрирующий синтаксис лямбда-выражения:

Это простое лямбда-выражение, которое принимает два параметра: x и y и возвращает их сумму. Как вы могли догадаться, параметры функции перечислены перед оператором -> , а тело функции начинается после оператора -> . Это лямбда-выражение может быть присвоено переменной и использовано следующим образом:

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

Создание функции более высокого порядка, которая может принимать приведенное выше лямбда-выражение в качестве параметра, так же просто. Например, чтобы создать функцию, которая удваивает результат выражения лямбда, вы должны написать:

Вы можете вызвать эту функцию следующим образом:

Кроме того, вы можете передать выражение лямбда прямо в функцию более высокого порядка:

Лямбда-выражения часто используются с массивами. Например, рассмотрим следующий массив значений Int :

Если вы хотите, получить квадрат для значения каждого элемента в массиве, вы можете использовать функцию map вместе с выражением лямбда следующим образом:

3. Диапазоны

Выражения диапазона очень часто используются в Kotlin. Вы уже использовали их при создании методов isTeenager и isOctogenarian .

To create a range, all you need is the .. operator.

Чтобы создать диапазон в порядке убывания, используйте функцию downTo .

Если вы не хотите, чтобы шаг был 1, вы можете указать пользовательское значение, используя функцию step .

В этом уроке вы увидите больше выражений диапазона.

4. Условные конструкции

В Kotlin if — выражение, которое возвращает разные значения в зависимости от того, выполнено ли условие. Следующий пример иллюстрирует, как это работает.

when выражение эквивалентно Java switch . Тем не менее, оно намного более универсально. Например, рассмотрим следующий пример.

Как вы можете видеть, when в качестве условий могут принимать не только одиночные значения, но и выражения.

5. Конструкции циклов

for..in

В Kotlin вы можете использовать цикл for..in для циклического преобразования массивов, коллекций и всего остального, который предоставляет итератор. Его синтаксис почти идентичен синтаксису Java, за исключением использования оператора in вместо оператора Java : . В следующем примере показано, как выполнить цикл массива объектов String .

С помощью выражений диапазона вы можете заставить этот цикл вести себя как традиционный цикл for C-стиля.

while и do..while

Синтаксис while и do . while циклов в Kotlin идентичен синтаксису, используемому в Java. Например, следующий код Kotlin перебирает массив объектов String с использованием цикла while :

6. Шаблоны строк

Kotlin позволяет вставлять переменные и выражения в строки, обертывая их парой фигурных скобок с префиксом символа $ . Например:

Заключение

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

Если вы хотите узнать, как использовать Kotlin в Android Studio, взгляните на статью Как использовать Kotlin в своих проектах Android. Чтобы узнать больше о языке Котлин, я рекомендую посетить документацию Kotlin.

Записки программера

Страницы

четверг, 27 июля 2020 г.

Kotlin. Часть 4. Неловкие моменты

По заголовку становится ясно, что мы проделали уже очень большой путь. В серии Kotlin вышло целых три статьи из пяти (Введение, Незнакомые конструкции, Мигрируем из Java) и это четвертая в которой я хотел бы рассказать вам о том, как не выстрелить себе в ногу, применяя этот язык и как понять причину странного поведения программы. В следующей заключительной статье мы будем обсуждать как реализовать свой DSL (проблемно-ориентированный язык) с помощью Kotlin на примере библиотеки для создания Telegram ботов.

Если вдруг вам не знакомо что-то, что я использую без объяснений, то рекомендую прочитать предыдущие статьи цикла

Если последним параметром функции в Kotlin передается лямбда, то её можно вынести за скобки, если это единственный параметр, то скобки можно не писать, очевидный пример — forEach

Начнем мы с самого популярного места: inline функции.

Inline функции с return

inline функция — функция код которой будет встроен в место вызова, особенно полезна при передаче в неё лямбд, которые тоже встраиваются. Специальный модификатор для функции — inline

Inline (встраиваемые) функции таят в себе небольшие, но лакомые для ошибок моменты о которых должен знать каждый уважающий себя Kotlin разработчик. Такие функции будут встроены в место вызова и код лямбд, передаваемых в них, будет встроен тоже. В такой ситуации нужно понимать как правильно манипулировать ключевым словом return. Давайте рассмотрим следующий пример:

Как вы думаете что увидит пользователь?

Ответ тру Java разработчика — «three», «two», «boom!»

Ответ тру Kotlin разработчика — «three», «two»

И действительно, при запуске такого метода в Kotlin последняя строчка метода выполнена не будет. Причина этому функция forEach, а вернее её модификатор — inline.

Что делает return в Kotlin? Простой вопрос и ответ соответствующий: Return либо возвращает из функции значение, либо как в этом примере прерывает её выполнение. Только вот если встроить функцию, то формально никакого вызова в этом месте уже не будет. Почему в Java не так? Как вы знаете в Java каждая лямбда это инстанс анонимного класса, у которого определен метод, в Kotlin это не всегда так. Естественно, так как мы работаем в рамках JVM, то другой реализации добиться довольно трудно, да и лично я никогда не понимал «а как иначе?». Есть кусок кода и его нужно хранить. Также мы видели альтернативный вариант это передача ссылки на метод, который в себе содержит нужный код, однако, я решил заглянуть «под капот», подготовил вот такой пример:

Для передачи ссылки на first-class функции используется «::»

И что я вижу в байт коде Kotlin?

Если верить спецификации виртуальной машины, то мы с вами являемся свидетелями единственного инстанса (ExampleKotlinKt$example$1.INSTANCE) анонимного класса, а значит и здесь без них никак.

Давайте вернемся к первому примеру. Функция forEach — это встраиваемая функция. Существует понятие, которое применимо в Kotlin — non-local return. Именно его мы и наблюдаем. Простыми словами, non-local return, это такой return, который способен прервать выполнение функции, которая окружает встраиваемую. Для того чтобы получить ожидаемое поведение нам следует воспользоваться return к метке вот так

noinline

Иногда не нужно встраивать все параметры, в такой ситуации помогает модификатор на параметре функции noinline. Рассмотрим следующий пример:

Путь изначально у нас есть inline функция someFun у которой все параметры встраиваются (по умолчанию). Что если мы захотим не встраивать передаваемую лямбду? Пример ниже не компилируется, т.к. non-local возврат из лямбды, которая точно не будет встроена невозможен

Как исправить? Очень просто! Добавляет return к метке. Это мы уже умеем. В такой ситуации не получится сделать non-local return, пожалуй, это и хорошо

Рассмотрим еще один модификатор, который используется для параметров inline функций — crossinline

crossinline

Представим что мы передали в inline функцию лямбду, которая по умолчанию тоже inline, а значит в ней может быть вызван non-local return. Если мы захотим использовать эту лямбду в другом контексте, как в примере ниже, то это не скомпилируется. Повторюсь, пример ниже не компилируется из-за попытки использовать лямбду внутри Store.


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

Для того чтобы компиляция заработала нужно воспользоваться модификатором crossinline. В этом случае компилятор будет запрещать non-local return в передаваемых лямбдах и при этом инлайнить эти лямбды там, где это возможно внутри someFun. Теперь мы будем использовать только return к метке, внутри передаваемых crossinline параметров. Код ниже становится компилируемым, при этом лямбда будет встроена в контексте функции, которая, в свою очередь, тоже будет встроена.

Для целостного понимания, давайте сравним все три вида лямбд передаваемых в одну функцию с разными модификаторами. Код ниже вы можете использовать как справку по inline модификаторам:

Вот ссылка на Gist, чтобы сохранить себе.

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

Наиболее насыщенная часть на этом заканчивается и «неловкие моменты», которые вы встретите дальше будут значительно проще в понимании.

Переопределенные операторы

В Kotlin вы можете переопределить операторы. Отнеситесь к этому внимательно, т.к. по ошибке можно не правильно понять принцип действия того или иного оператора, например «==». В Java «==» означает ссылочное равенство, в Kotlin всё чуть более сложнее. Оператор «==» соответствует методу equals, т.е. проверяет структурное равенство, соответственно в примере выше (с forEach) «==» для сравнения строк — правильный синтаксис. Для того, чтобы получить аналогичную Java проверку на ссылочное равенство в Kotlin используют «===». Посмотрите полный перечень операторов Kotlin и вы обнаружите, что проверка на вхождение в коллекцию (in) это тоже оператор связанный с методом contains. Вы можете перейти к реализации in и убедиться в этом сами.

Вложенные лямбды

Иногда, например при построении DSL, когда вы используете вложенные лямбды может сложиться неловкая ситуация. Очень важно следить за контекстом, т.к. this это не всегда то, что вы можете подумать. Давайте посмотрим на пример:

В примере есть функция main и она создает контекст и у него вызывает функцию apply. Всё что вам нужно знать сейчас об этой функции, так это то, что внутри неё this — это тот объект на котором она вызвана. Как этого достичь я расскажу в следующей заключительной статье основного цикла Kotlin.

Итак, мы видим, что внутри apply вызывается функция innerContext в которую передается лямбда с обработчиком (также работает apply, но об этом в 5й части). Фактически, метод служит для запуска лямбды и всё. Как вы видите в метод main мы передали лямбду и что же вернет println? Не буду томить, внутри этой функции он напечатает совсем другой this. Давайте посмотрим на вывод:

Как видно из вывода, что при смене контекста меняется не только this, но и вызываемые методы (doSomething). Это очень важно, по этому следите за контекстом, чтобы не допускать таких ошибок.

Метод который возвращает лямбду

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

В этом коде есть одна большая проблема, он возвращает лямбду, а не выполняет печать, т.е. всё что после «=» — лямбда, а «=» это присваивание её как возвращаемого значения функции. Следовательно печать может быть достигнута только так printA()(). Жутко, не правда ли? Не возвращайте лямбды в совокупности с сокращенной записью метода.

Smart cast

Не забывайте про smart cast. Компилятор после вашей проверки на null внутри if конструкции приводит String? к String, то есть внутри стейтмента if он не требует от вас явных проверок и в этом примере «!!» не нужно.

Методы вызываемые на null

Удивительно, но в Kotlin сделали методы, которые можно вызывать даже при null по ссылке у которой вызывают метод, такой метод вы можете объявить для на nullable типа в качестве extention функции (о них поговорим в последней статье). Давайте посмотрим на следующий пример.

Не смотря на то, что метод вызывается на строке в которой может быть null, компилятор разрешает это делать, т.к. в декларации метода прописано, что для этого метода this может быть равен null и безопасность nullability обеспечивается уже на другом уровне — внутри метода. Декларируется метод, например так:

Отладка Kotlin + Java

На данный момент известно одно, отладка Kotlin + Java совсем немного хромает, не значительно. В принципе на уровне приложения всё ок, но вот я захотел поставить точку останова внутри println и этого сделать уже не смогу, вернее смогу, но она будет проигнорирована, хотя глубже в System.out.println (который вызывается в println) уже работает. Неведомые вещи творятся, еще в интернете можно встретить цитирование вот этих слов:

Сам глубоко не копал, об этом можно будет поговорить отдельно, но будьте бдительны.

«Переопределяющая» extention function

В Kotlin есть понятие extention функции. Это функция которая может быть добавлена в тот или иной класс без изменения кода этого класса. В Java такая функция будет выглядеть как статический метод, а в Kotlin работает как часть синтаксиса. Важно понимать, что такие функции не «встраиваются» в класс буквально, они лишь работают с его публичным API. Так например есть много функций для обработки Java коллекций при этом сами классы коллекций не тронуты. Но что произойдет, если такая функция перекроет существующую? Давайте взглянем на код.

fun Abc.someFun — так выглядит определение extention функции. Мы указываем на какой класс она нацелена (здесь может быть даже дженерик тип), а затем на этом классе можем её вызывать. Обращайте внимание на подсветку среды разработки. Extention функции ниже по приоритету, чем существующие в классе функции, по этому при вызове функции с таким названием extention функция будет проигнорирована.

Платформенные типы

Как я писал в статье о миграции из Java — в Kotlin есть понятие платформенного типа. Для того, чтобы не получить исключение из-за null в value проверяйте и обрабатывайте код заранее. При взаимодействии с Java из Kotlin, старайтесь указать типы изначально и явно, либо сразу их обработать. Пример ниже

Здесь падает ошибка

Перед тем как выполнить toLowerCase в runtime производится приведение value к типу String, а если вы изначально не записали тип у value, то в ваше приложение может просочиться платформенный тип и создать некоторые проблемы, хотя и при первой же передаче в функцию где определён тип входного параметра это обнаружится.

Правильно было бы либо обработать value как nullable, либо, если ситуация с null не допустима, то явно указать тип и тогда ошибка упадёт сразу на стыке взаимодействия с Java

Рекурсивный аксессор get property

В Kotlin’e нет Getters и Setters в виде методов, но есть аксессоры для property. Те же яйца только в профиль и читается лучше, а так же я слышал (из публичных источников), что проперти инлайнятся (как нибудь мы это проверим), т.е. вызов во время выполнения происходит напрямую к полю снимая оверхед от посредников типа методов.

Чем опасны аксессоры? Давайте взглянем на код ниже

С первого взгляда всё в порядке, если в поле name был «Uber», то возвращаем Yandex, иначе то значение, которое находится в филде. Однако, попытка выполнить получение name завершится с

Но почему? Дело в том, что компилятор считает, что мы пытаемся рекурсивно получить name у объекта и уходит в бесконечность. Здесь мы хотели сказать, что нам нужен так называемый backing field для name и сделать это можно очевидным путём. Используем слово field! Небольшие изменения и вуаля!

Теперь всё работает отлично. И, как всегда, будьте внимательны.

Unit можно сложить в переменную

Как вы знаете, если не написать возвращаемый тип у функции, то он будет по умолчанию Unit. Это аналог vo >

Заключение

Мы рассмотрели несколько интересных ситуаций, где Kotlin может вести себя не очевидно. Основной тезис, который я хотел бы донести этой статьёй, не бойтесь языков программирования, всё можно объяснить или найти причины странного поведения, более того — это довольно интересно. В следующей, заключительной статье мы обсудим DSL и еще пару важных конструкций языка, которых нет в Java, но в Kotlin они делают вашу жизнь слаще 🙂

Functions

Function declarations

Functions in Kotlin are declared using the fun keyword:

Function usage

Calling functions uses the traditional approach:

Calling member functions uses the dot notation:

Parameters

Function parameters are defined using Pascal notation, i.e. name: type. Parameters are separated using commas. Each parameter must be explicitly typed:

Мастер Йода рекомендует:  Построение дерева иерархии с помощью PHP MySQL PHP

Default arguments

Function parameters can have default values, which are used when a corresponding argument is omitted. This allows for a reduced number of overloads compared to other languages:

Default values are defined using the = after type along with the value.

Overriding methods always use the same default parameter values as the base method. When overriding a method with default parameter values, the default parameter values must be omitted from the signature:

If a default parameter precedes a parameter with no default value, the default value can only be used by calling the function with named arguments:

If the last argument after default parameters is a lambda, it can be passed in either as a named argument or outside the parentheses:

Named arguments

Function parameters can be named when calling functions. This is very convenient when a function has a high number of parameters or default ones.

Given the following function:


We could call this using default arguments:

However, when calling it with non-default, the call would look something like:

With named arguments we can make the code much more readable:

and if we do not need all arguments:

When a function is called with both positional and named arguments, all the positional arguments should be placed before the first named one. For example, the call f(1, y = 2) is allowed, but f(x = 1, 2) is not.

Variable number of arguments ( vararg) can be passed in the named form by using the spread operator:

On the JVM: the named argument syntax cannot be used when calling Java functions because Java bytecode does not always preserve names of function parameters.

Unit-returning functions

If a function does not return any useful value, its return type is Unit . Unit is a type with only one value — Unit . This value does not have to be returned explicitly:

The Unit return type declaration is also optional. The above code is equivalent to:

Single-expression functions

When a function returns a single expression, the curly braces can be omitted and the body is specified after a = symbol:

Explicitly declaring the return type is optional when this can be inferred by the compiler:

Explicit return types

Functions with block body must always specify return types explicitly, unless it’s intended for them to return Unit , in which case it is optional. Kotlin does not infer return types for functions with block bodies because such functions may have complex control flow in the body, and the return type will be non-obvious to the reader (and sometimes even for the compiler).

Variable number of arguments (Varargs)

A parameter of a function (normally the last one) may be marked with vararg modifier:

allowing a variable number of arguments to be passed to the function:

Inside a function a vararg -parameter of type T is visible as an array of T , i.e. the ts variable in the example above has type Array .

Only one parameter may be marked as vararg . If a vararg parameter is not the last one in the list, values for the following parameters can be passed using the named argument syntax, or, if the parameter has a function type, by passing a lambda outside parentheses.

When we call a vararg -function, we can pass arguments one-by-one, e.g. asList(1, 2, 3) , or, if we already have an array and want to pass its contents to the function, we use the spread operator (prefix the array with * ):

Infix notation

Functions marked with the infix keyword can also be called using the infix notation (omitting the dot and the parentheses for the call). Infix functions must satisfy the following requirements:

  • They must be member functions or extension functions;
  • They must have a single parameter;
  • The parameter must not accept variable number of arguments and must have no default value.

Infix function calls have lower precedence than the arithmetic operators, type casts, and the rangeTo operator. The following expressions are equivalent:

  • 1 shl 2 + 3 is equivalent to 1 shl (2 + 3)
  • 0 until n * 2 is equivalent to 0 until (n * 2)
  • xs union ys as Set is equivalent to xs union (ys as Set )

On the other hand, infix function call’s precedence is higher than that of the boolean operators && and || , is — and in -checks, and some other operators. These expressions are equivalent as well:

  • a && b xor c is equivalent to a && (b xor c)
  • a xor b in c is equivalent to (a xor b) in c

See the Grammar reference for the complete operators precedence hierarchy.

Note that infix functions always require both the receiver and the parameter to be specified. When you’re calling a method on the current receiver using the infix notation, you need to use this explicitly; unlike regular method calls, it cannot be omitted. This is required to ensure unambiguous parsing.

Function scope

In Kotlin functions can be declared at top level in a file, meaning you do not need to create a class to hold a function, which you are required to do in languages such as Java, C# or Scala. In addition to top level functions, Kotlin functions can also be declared local, as member functions and extension functions.

Local functions

Kotlin supports local functions, i.e. a function inside another function:

Local function can access local variables of outer functions (i.e. the closure), so in the case above, the visited can be a local variable:

Member functions

A member function is a function that is defined inside a class or object:

Member functions are called with dot notation:

For more information on classes and overriding members see Classes and Inheritance.

Generic functions

Functions can have generic parameters which are specified using angle brackets before the function name:

For more information on generic functions see Generics.

Inline functions

Inline functions are explained here.

Extension functions

Extension functions are explained in their own section.

Higher-order functions and lambdas

Higher-Order functions and Lambdas are explained in their own section.

Tail recursive functions

Kotlin supports a style of functional programming known as tail recursion. This allows some algorithms that would normally be written using loops to instead be written using a recursive function, but without the risk of stack overflow. When a function is marked with the tailrec modifier and meets the required form, the compiler optimises out the recursion, leaving behind a fast and efficient loop based version instead:

This code calculates the fixpoint of cosine, which is a mathematical constant. It simply calls Math.cos repeatedly starting at 1.0 until the result doesn’t change any more, yielding a result of 0.7390851332151611 for the specified eps precision. The resulting code is equivalent to this more traditional style:

To be eligible for the tailrec modifier, a function must call itself as the last operation it performs. You cannot use tail recursion when there is more code after the recursive call, and you cannot use it within try/catch/finally blocks. Currently, tail recursion is supported by Kotlin for JVM and Kotlin/Native.

Записки программера

Страницы

четверг, 27 июля 2020 г.

Kotlin. Часть 4. Неловкие моменты


По заголовку становится ясно, что мы проделали уже очень большой путь. В серии Kotlin вышло целых три статьи из пяти (Введение, Незнакомые конструкции, Мигрируем из Java) и это четвертая в которой я хотел бы рассказать вам о том, как не выстрелить себе в ногу, применяя этот язык и как понять причину странного поведения программы. В следующей заключительной статье мы будем обсуждать как реализовать свой DSL (проблемно-ориентированный язык) с помощью Kotlin на примере библиотеки для создания Telegram ботов.

Если вдруг вам не знакомо что-то, что я использую без объяснений, то рекомендую прочитать предыдущие статьи цикла

Если последним параметром функции в Kotlin передается лямбда, то её можно вынести за скобки, если это единственный параметр, то скобки можно не писать, очевидный пример — forEach

Начнем мы с самого популярного места: inline функции.

Inline функции с return

inline функция — функция код которой будет встроен в место вызова, особенно полезна при передаче в неё лямбд, которые тоже встраиваются. Специальный модификатор для функции — inline

Inline (встраиваемые) функции таят в себе небольшие, но лакомые для ошибок моменты о которых должен знать каждый уважающий себя Kotlin разработчик. Такие функции будут встроены в место вызова и код лямбд, передаваемых в них, будет встроен тоже. В такой ситуации нужно понимать как правильно манипулировать ключевым словом return. Давайте рассмотрим следующий пример:

Как вы думаете что увидит пользователь?

Ответ тру Java разработчика — «three», «two», «boom!»

Ответ тру Kotlin разработчика — «three», «two»

И действительно, при запуске такого метода в Kotlin последняя строчка метода выполнена не будет. Причина этому функция forEach, а вернее её модификатор — inline.

Что делает return в Kotlin? Простой вопрос и ответ соответствующий: Return либо возвращает из функции значение, либо как в этом примере прерывает её выполнение. Только вот если встроить функцию, то формально никакого вызова в этом месте уже не будет. Почему в Java не так? Как вы знаете в Java каждая лямбда это инстанс анонимного класса, у которого определен метод, в Kotlin это не всегда так. Естественно, так как мы работаем в рамках JVM, то другой реализации добиться довольно трудно, да и лично я никогда не понимал «а как иначе?». Есть кусок кода и его нужно хранить. Также мы видели альтернативный вариант это передача ссылки на метод, который в себе содержит нужный код, однако, я решил заглянуть «под капот», подготовил вот такой пример:

Для передачи ссылки на first-class функции используется «::»

И что я вижу в байт коде Kotlin?

Если верить спецификации виртуальной машины, то мы с вами являемся свидетелями единственного инстанса (ExampleKotlinKt$example$1.INSTANCE) анонимного класса, а значит и здесь без них никак.

Давайте вернемся к первому примеру. Функция forEach — это встраиваемая функция. Существует понятие, которое применимо в Kotlin — non-local return. Именно его мы и наблюдаем. Простыми словами, non-local return, это такой return, который способен прервать выполнение функции, которая окружает встраиваемую. Для того чтобы получить ожидаемое поведение нам следует воспользоваться return к метке вот так

noinline

Иногда не нужно встраивать все параметры, в такой ситуации помогает модификатор на параметре функции noinline. Рассмотрим следующий пример:

Путь изначально у нас есть inline функция someFun у которой все параметры встраиваются (по умолчанию). Что если мы захотим не встраивать передаваемую лямбду? Пример ниже не компилируется, т.к. non-local возврат из лямбды, которая точно не будет встроена невозможен

Как исправить? Очень просто! Добавляет return к метке. Это мы уже умеем. В такой ситуации не получится сделать non-local return, пожалуй, это и хорошо

Рассмотрим еще один модификатор, который используется для параметров inline функций — crossinline

crossinline

Представим что мы передали в inline функцию лямбду, которая по умолчанию тоже inline, а значит в ней может быть вызван non-local return. Если мы захотим использовать эту лямбду в другом контексте, как в примере ниже, то это не скомпилируется. Повторюсь, пример ниже не компилируется из-за попытки использовать лямбду внутри Store.

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

Для того чтобы компиляция заработала нужно воспользоваться модификатором crossinline. В этом случае компилятор будет запрещать non-local return в передаваемых лямбдах и при этом инлайнить эти лямбды там, где это возможно внутри someFun. Теперь мы будем использовать только return к метке, внутри передаваемых crossinline параметров. Код ниже становится компилируемым, при этом лямбда будет встроена в контексте функции, которая, в свою очередь, тоже будет встроена.

Для целостного понимания, давайте сравним все три вида лямбд передаваемых в одну функцию с разными модификаторами. Код ниже вы можете использовать как справку по inline модификаторам:

Вот ссылка на Gist, чтобы сохранить себе.

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

Наиболее насыщенная часть на этом заканчивается и «неловкие моменты», которые вы встретите дальше будут значительно проще в понимании.

Переопределенные операторы

В Kotlin вы можете переопределить операторы. Отнеситесь к этому внимательно, т.к. по ошибке можно не правильно понять принцип действия того или иного оператора, например «==». В Java «==» означает ссылочное равенство, в Kotlin всё чуть более сложнее. Оператор «==» соответствует методу equals, т.е. проверяет структурное равенство, соответственно в примере выше (с forEach) «==» для сравнения строк — правильный синтаксис. Для того, чтобы получить аналогичную Java проверку на ссылочное равенство в Kotlin используют «===». Посмотрите полный перечень операторов Kotlin и вы обнаружите, что проверка на вхождение в коллекцию (in) это тоже оператор связанный с методом contains. Вы можете перейти к реализации in и убедиться в этом сами.

Вложенные лямбды

Иногда, например при построении DSL, когда вы используете вложенные лямбды может сложиться неловкая ситуация. Очень важно следить за контекстом, т.к. this это не всегда то, что вы можете подумать. Давайте посмотрим на пример:

В примере есть функция main и она создает контекст и у него вызывает функцию apply. Всё что вам нужно знать сейчас об этой функции, так это то, что внутри неё this — это тот объект на котором она вызвана. Как этого достичь я расскажу в следующей заключительной статье основного цикла Kotlin.

Итак, мы видим, что внутри apply вызывается функция innerContext в которую передается лямбда с обработчиком (также работает apply, но об этом в 5й части). Фактически, метод служит для запуска лямбды и всё. Как вы видите в метод main мы передали лямбду и что же вернет println? Не буду томить, внутри этой функции он напечатает совсем другой this. Давайте посмотрим на вывод:

Как видно из вывода, что при смене контекста меняется не только this, но и вызываемые методы (doSomething). Это очень важно, по этому следите за контекстом, чтобы не допускать таких ошибок.

Метод который возвращает лямбду

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

В этом коде есть одна большая проблема, он возвращает лямбду, а не выполняет печать, т.е. всё что после «=» — лямбда, а «=» это присваивание её как возвращаемого значения функции. Следовательно печать может быть достигнута только так printA()(). Жутко, не правда ли? Не возвращайте лямбды в совокупности с сокращенной записью метода.

Smart cast

Не забывайте про smart cast. Компилятор после вашей проверки на null внутри if конструкции приводит String? к String, то есть внутри стейтмента if он не требует от вас явных проверок и в этом примере «!!» не нужно.

Методы вызываемые на null

Удивительно, но в Kotlin сделали методы, которые можно вызывать даже при null по ссылке у которой вызывают метод, такой метод вы можете объявить для на nullable типа в качестве extention функции (о них поговорим в последней статье). Давайте посмотрим на следующий пример.

Не смотря на то, что метод вызывается на строке в которой может быть null, компилятор разрешает это делать, т.к. в декларации метода прописано, что для этого метода this может быть равен null и безопасность nullability обеспечивается уже на другом уровне — внутри метода. Декларируется метод, например так:

Отладка Kotlin + Java

На данный момент известно одно, отладка Kotlin + Java совсем немного хромает, не значительно. В принципе на уровне приложения всё ок, но вот я захотел поставить точку останова внутри println и этого сделать уже не смогу, вернее смогу, но она будет проигнорирована, хотя глубже в System.out.println (который вызывается в println) уже работает. Неведомые вещи творятся, еще в интернете можно встретить цитирование вот этих слов:

Сам глубоко не копал, об этом можно будет поговорить отдельно, но будьте бдительны.

«Переопределяющая» extention function

В Kotlin есть понятие extention функции. Это функция которая может быть добавлена в тот или иной класс без изменения кода этого класса. В Java такая функция будет выглядеть как статический метод, а в Kotlin работает как часть синтаксиса. Важно понимать, что такие функции не «встраиваются» в класс буквально, они лишь работают с его публичным API. Так например есть много функций для обработки Java коллекций при этом сами классы коллекций не тронуты. Но что произойдет, если такая функция перекроет существующую? Давайте взглянем на код.

fun Abc.someFun — так выглядит определение extention функции. Мы указываем на какой класс она нацелена (здесь может быть даже дженерик тип), а затем на этом классе можем её вызывать. Обращайте внимание на подсветку среды разработки. Extention функции ниже по приоритету, чем существующие в классе функции, по этому при вызове функции с таким названием extention функция будет проигнорирована.

Платформенные типы

Как я писал в статье о миграции из Java — в Kotlin есть понятие платформенного типа. Для того, чтобы не получить исключение из-за null в value проверяйте и обрабатывайте код заранее. При взаимодействии с Java из Kotlin, старайтесь указать типы изначально и явно, либо сразу их обработать. Пример ниже

Здесь падает ошибка

Перед тем как выполнить toLowerCase в runtime производится приведение value к типу String, а если вы изначально не записали тип у value, то в ваше приложение может просочиться платформенный тип и создать некоторые проблемы, хотя и при первой же передаче в функцию где определён тип входного параметра это обнаружится.

Правильно было бы либо обработать value как nullable, либо, если ситуация с null не допустима, то явно указать тип и тогда ошибка упадёт сразу на стыке взаимодействия с Java

Рекурсивный аксессор get property

В Kotlin’e нет Getters и Setters в виде методов, но есть аксессоры для property. Те же яйца только в профиль и читается лучше, а так же я слышал (из публичных источников), что проперти инлайнятся (как нибудь мы это проверим), т.е. вызов во время выполнения происходит напрямую к полю снимая оверхед от посредников типа методов.

Чем опасны аксессоры? Давайте взглянем на код ниже

С первого взгляда всё в порядке, если в поле name был «Uber», то возвращаем Yandex, иначе то значение, которое находится в филде. Однако, попытка выполнить получение name завершится с

Но почему? Дело в том, что компилятор считает, что мы пытаемся рекурсивно получить name у объекта и уходит в бесконечность. Здесь мы хотели сказать, что нам нужен так называемый backing field для name и сделать это можно очевидным путём. Используем слово field! Небольшие изменения и вуаля!


Теперь всё работает отлично. И, как всегда, будьте внимательны.

Unit можно сложить в переменную

Как вы знаете, если не написать возвращаемый тип у функции, то он будет по умолчанию Unit. Это аналог vo >

Заключение

Мы рассмотрели несколько интересных ситуаций, где Kotlin может вести себя не очевидно. Основной тезис, который я хотел бы донести этой статьёй, не бойтесь языков программирования, всё можно объяснить или найти причины странного поведения, более того — это довольно интересно. В следующей, заключительной статье мы обсудим DSL и еще пару важных конструкций языка, которых нет в Java, но в Kotlin они делают вашу жизнь слаще 🙂

Магия функций в Kotlin

Sql, RegExp, Gradle — что их объединяет? Всё это примеры использования проблемно-ориентированных языков или DSL (domain-specific language). Каждый такой язык решает свою узконаправленную задачу, например, запрос данных из БД, поиск совпадений в тексте или описание процесса сборки приложения. Язык Kotlin предоставляет большое количество возможностей для создания собственного проблемно-ориентированного языка. В ходе статьи мы разберемся, какие инструменты есть в арсенале программиста, и реализуем DSL для предложенной предметной области.

Весь синтаксис, представленный в статье, я объясню максимально просто, однако, материал рассчитан на практикующих инженеров, которые рассматривают Kotlin, как язык для построения проблемно-ориентированных языков. В конце статьи будут приведены недостатки, к которым нужно быть готовым. Используемый в статье код актуален для Kotlin версии 1.1.4-3 и доступен на GitHub.

Что такое DSL?

Языки программирования можно разделить на 2 типа: универсальные языки (general-purpose programming language) и предметно-ориентированные (domain-specific language). Популярные примеры DSL — это SQL, регулярные выражения, build.gradle. Язык уменьшает объем предоставляемой функциональности, но при этом он способен эффективно решать определенную проблему. Это способ описать программу не в императивном стиле (как нужно получить результат), а в декларативном или близком к декларативному (описать текущую задачу), в таком случае решение проблемы будет получено исходя из заданной информации.

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

Мастер Йода рекомендует:  От CREATE до JOIN введение в SQL + шпаргалка

В статье я рассмотрел построение «внутреннего» DSL на языке Kotlin. Такой вид проблемно-ориентированных языков реализуется на основе синтаксиса универсального языка. Подробнее об этом вы можете прочитать по ссылке.

Область применения

Один из лучших способов применить и продемонстрировать Kotlin DSL, на мой взгляд, это тесты.

Предположим, что вы пришли из мира Java. Часто ли вам приходилось снова и снова описывать стандартные экземпляры сущностей для довольно крупной модели данных? Вероятно, что для этого вы использовали какие-нибудь билдеры или, еще хуже, специальные утилитные классы, которые под капотом заполняли значения по умолчанию? Как много у вас перегруженных методов? Как часто вам нужно «совсем немного» отклониться от значений по умолчанию и как много работы для этого приходится делать сейчас? Если ничего, кроме негатива, у вас эти вопросы не вызывают, то вы читаете правильную статью.

Длительное время на нашем проекте, посвященном образовательной сфере, мы точно так же, с помощью билдеров и утилитных классов, покрывали тестами один из важнейших модулей системы — модуль построения учебного расписания. На смену этому подходу пришел язык Kotlin и DSL для формирования различных вариантов применения системы планирования и проверки результатов. Ниже вы увидите примеры того, как мы воспользовались возможностями языка и превратили разработку тестов подсистемы планирования из пытки в удовольствие.

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

Основные возможности

Давайте перечислим основные преимущества Kotlin, которые позволяют достаточно чисто писать на этом языке и доступны для построения собственного DSL. Ниже представлена таблица с основными улучшениями синтаксиса языка, которые стоит использовать. Просмотрите этот список внимательно. Если большая часть конструкций для вас не знакома, то желательно читать последовательно. Однако если вы не знакомы с одним или двумя пунктами, то можете перейти сразу к ним. Если всё здесь для вас знакомо, то вы можете перейти к обзору недостатков использования DSL в конце статьи. Если вы хотите дополнить этот список, то, пожалуйста, напишите свои варианты в комментариях.

Название функциональности DSL синтаксис Обычный синтаксис
Переопределение операторов collection += element collection.add(element)
Псевдонимы типа typealias Point = Pair Создание пустых классов-наследников и прочие костыли
Соглашение для get/set методов map[«key»] = «value» map.put(«key», «value»)
Мульти-декларации val (x, y) = Point(0, 0) val p = Point(0, 0); val x = p.first; val y = p.second
Лямбда за скобками list.forEach

list.forEach(<. >)
Extention функции mylist.first(); // метод first() отсутствует в классе коллекции mylist Утилитные функции
Infix функции 1 to «one» 1.to(«one»)
Лямбда с обработчиком Person().apply

Нет
Контролирование контекста @DslMarker Нет

Нашли для себя что-то новое? Тогда продолжим.

В таблице намеренно пропущены делегированные свойства, так как, на мой взгляд, они бесполезны для построения DSL в том виде, который мы будем рассматривать. Благодаря указанным возможностям вы сможете писать код чище, избавиться от большого количества «шумного» синтаксиса и при этом сделать разработку еще более приятным занятием («куда уж приятнее?» — спросите вы). Мне понравилось сравнение из книги Kotlin in Action, в натуральных языках, например, в английском, предложения построены из слов и грамматические правила управляют тем, как нужно объединять слова друг с другом. Аналогично в DSL, одна операция может быть сложена из нескольких вызовов методов, а проверка типов обеспечит гарантию, что конструкция имеет смысл. Естественно, порядок вызовов может быть не всегда очевиден, но это остается на совести проектировщика DSL.

Важно понимать, что в этой статье мы будем рассматривать «внутренний DSL», т.е. проблемно-ориентированный язык базируется на универсальном языке — Kotlin.

Пример финального результата

Прежде чем мы приступим к построению нашего проблемно-ориентированного языка, я хочу продемонстрировать результат того, что вы сможете построить после прочтения статьи. Весь код вы можете найти на GitHub репозитории по ссылке. Ниже рассмотрен DSL для тестирования поиска преподавателя для студентов по интересующим их предметам. В этом примере есть фиксированная временная сетка и мы проверяем, что занятия размещены в плане преподавателя и студента в одно и то же время.

Инструменты

Полный список инструментов для построения DSL, был приведен выше. Каждый из них был использован в примере и, исследуя код по ссылке, вы можете изучить построение таких конструкций. Мы не раз будем возвращаться к этому примеру для демонстрации различных инструментов. Важно отметить, что решения по построению DSL носят демонстративный характер, хотя вы можете повторить увиденное и в собственном проекте, это не означает, что представленный вариант единственно верный. Ниже мы детально рассмотрим каждый инструмент.

Некоторые возможности языка особенно хороши в совокупности с другими и первый инструмент в этом списке — лямбда вне скобок.

Лямбда вне скобок

Лямбда-выражения или лямбды — это блоки кода, которые можно передавать в функции, сохранять или вызывать. В языке Kotlin тип лямбды обозначается следующим образом (список типов параметров) -> возвращаемый тип . Следуя этому правилу, самый примитивный вид лямбды это () -> Unit , где Unit — это аналог Void с одним исключением. В конце лямбды или функции мы не
должны писать конструкцию «return . ». Благодаря этому, мы всегда имеем возвращаемый тип, просто в Kotlin это происходит неявно.

Ниже приведен простейший пример того, как можно сохранить лямбду в переменную:

Для лямбд без параметров компилятор способен самостоятельно вывести тип из уже известных. Однако в нашем случае один параметр есть. Вызов такой лямбды выглядит следующим образом:

В примере выше лямбда принимает один параметр. Внутри лямбды этот параметр по умолчанию имеет имя «it», но если параметров несколько, то вы должны явно перечислить их имена, либо использовать знак подчеркивания «_», чтобы проигнорировать его. Пример ниже демонстрирует такое поведение.

Базовый инструмент, который вы уже могли встретить, например, в Groovy, это лямбда вне скобок. Обратите внимание на пример в самом начале статьи, практически каждое использование фигурных скобок, за исключением стандартных конструкций — это использование лямбд. Существует как минимум два способа сделать конструкцию вида x < … >:

  • объект x и его унарный оператор invoke (этот способ обсудим позже);
  • функция x, в которую передают лямбду.

Независимо от варианта, мы используем лямбды. Допустим, есть функция x() . В языке Kotlin действует следующее правило: если лямбда является последним аргументом функции, то её можно вынести за скобки, если при этом лямбда единственный параметр, то скобки можно не писать. В результате, конструкция x(<…>) может быть преобразована в x() <> , а затем, убрав скобки, мы получаем x <> . Объявление такой функции выглядит следующим образом:

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

Но что если x — это экземпляр класса, объект, а не функция? Существует другое интересное решение, которое базируется на одной из основополагающих концепций, используемой при построении проблемно-ориентированных языков, переопределение операторов. Давайте рассмотрим этот инструмент.

Переопределение операторов

Kotlin предоставляет широкий, но ограниченный спектр операторов. Модификатор operator позволяет определять функции по соглашениям, которые будут вызываться при определенных условиях. Очевидным примером является функция plus, которая будет выполнена, при использовании оператора «+» между двумя объектами. Полный перечень операторов вы найдете по ссылке выше в документации.

Рассмотрим чуть менее тривиальный оператор «invoke». Главный пример этой статьи начинается с конструкции schedule < >. Назначение конструкции — обособить блок кода, который отвечает за тестирование планирования. Для построения такой конструкции используется способ, немного отличающийся от рассмотренного выше: оператор invoke + «лямбда вне скобок». После определения оператора invoke нам становится доступна конструкция schedule(. ), при том, что schedule — это объект. Фактически, вызов schedule(. ) интерпретируется компилятором как schedule.invoke(…). Давайте посмотрим на декларацию schedule.

Нужно понимать, что идентификатор schedule отсылает нас к единственному экземпляру класса schedule (синглтону), который помечен специальным ключевым словом object (подробнее о таких объектах, можно прочитать здесь). Таким образом, мы вызываем метод invoke у экземпляра schedule и при этом единственным параметром метода определяем лямбду, которую выносим за скобки. В итоге, конструкция schedule <… >равносильна следующей:

Однако если вы посмотрите внимательнее на метод invoke, то увидите не обычную лямбду, а «лямбду с обработчиком» или «лямбду с контекстом», тип которой записывается следующим образом: SchedulingContext.() -> Unit
Пора разобраться с тем, что это такое.

Лямбда с обработчиком

Kotlin дает нам возможность установить контекст для лямбда-выражения. Контекст — это обычный объект. Тип контекста опрелеляется вместе с типом лямбда-выражения. Такая лямбда приобретает свойства нестатического метода в классе контекста, но с доступом только к публичному API этого класса.
В то время как тип обычной лямбды определяется так: () -> Unit , тип лямбды с контекстом типа Х определяется так: X.()-> Unit и, если первый тип лямбд можно запускать привычным образом:

то для лямбды с контекстом нужен контекст:

Напомню, что в объекте schedule у нас определен оператор invoke (см. предыдущий параграф), который позволяет нам использовать конструкцию:

Лямбда, которую мы используем, имеет контекст типа SchedulingContext. В этом классе определен метод data. В результате у нас получается следующая конструкция:

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

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

Как вы видите, всё предельно просто.
Давайте взглянем на реализацию оператора invoke.

Мы вызываем конструктор для контекста: SchedulingContext() , а затем на созданном объекте (контексте) вызываем лямбду с идентификатором init, которую мы передали в качестве параметра. Это очень похоже на вызов обычной функции. В результате, в одной строке SchedulingContext().init() мы создаем контекст и вызываем переданную в оператор лямбду. Если вас интересуют другие примеры, то обратите внимание на методы apply и with из стандартной библиотеки Kotlin.

В последних примерах мы рассмотрели оператор invoke и его взаимодействие с другими инструментами. Далее мы сфокусируемся на другом инструменте, который формально является оператором и делает наш код чище, а именно на соглашении для get/set методов.

Соглашение для get/set методов

При разработке DSL мы можем реализовывать синтаксис доступа к ассоциативному массиву по одному или более ключам. Взглянем на пример ниже:

Чтобы использовать квадратные скобки, необходимо реализовать методы get или set в зависимости от того, что нужно (чтение или запись) с модификатором operator. Пример реализации этого инструмента вы можете найти в классе Matrix на GitHub по ссылке. Это простейшая реализация обертки для работы с матрицами. Ниже часть кода, которая интересует нас.

Типы параметров функций get и set ограничены только вашей фантазией. Вы можете использовать как один, так и несколько параметров для get/set функций и обеспечивать комфортный синтаксис для доступа к данным. Операторы в Kotlin привносят много интересных возможностей, с которыми вы можете ознакомиться в документации.

К удивлению, в стандартной библиотеке Kotlin есть класс Pair, но почему? Большая часть сообщества считает, что класс Pair — это плохо, с ним пропадает смысл связи двух объектов и становится не очевидно, почему они в паре. Следующие два инструмента демонстрируют, как можно и осмысленность пары сохранить, и не создавать лишние классы.


Псевдонимы типа

Представим, что нам нужен класс-обертка для точки на плоскости с целочисленными координатами. В принципе, нам подходит класс Pair , но в переменной такого типа в один момент мы можем потерять понимание того, зачем мы связываем значения в пару. Очевидные пути исправления — это либо писать собственный класс, либо еще, что похуже. В Kotlin арсенал разработчика пополняется псевдонимами типа, которые записываются следующим образом:

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

Однако у класса Pair есть два свойства, first и second, и как бы нам переименовать эти свойства так, чтобы стереть всякие различия между желаемым классом Point и Pair? Сами свойства переименовать не удастся, но в нашем инструментарии есть замечательная возможность, которую народные умельцы обозначили как мульти-декларации.

Мульти-декларации (Destructuring declaration)

Для простоты понимания примера рассмотрим ситуацию: у нас есть объект типа Point, как мы знаем из примера выше, это всего лишь переименованный тип Pair . Как видно из реализации класса Pair стандартной библиотеки, он помечен модификатором data, а это значит, что, среди прочего, в данном классе мы получаем сгенерированные методы componentN. Давайте о них и поговорим.

Для любого класса мы можем определить оператор componentN, который будет предоставлять доступ к одному из свойств объекта. Это означает, что вызов метода point.component1 равносилен вызову point.first. Теперь разберемся, зачем нужно это дублирование.

Что такое мульти-декларации? Это способ «разложить» объект по переменным. Благодаря этой функциональности, мы можем написать следующую конструкцию:

У нас есть возможность объявить сразу несколько переменных, но что окажется в качестве значений? Именно для этого нам и нужны генерируемые методы componentN , в соответствии с порядковым номером, вместо N, начиная с 1, мы можем разложить объект на набор его свойств. Так, например, запись выше эквивалентна следующей:

что в свою очередь равносильно:

где first и second это свойства объекта Point.

Конструкция for в Kotlin имеет следующий вид, где x последовательно принимает значения 1, 2 и 3:

Обратим внимание на блок assertions в DSL из основного примера. Для удобства часть его я приведу ниже:

Теперь всё должно быть очевидно. Мы перебираем коллекцию scheduledEvents, каждый элемент которой раскладывается на 4 свойства, описывающие текущий объект.

Extension функции

Добавление собственных методов к объектам из сторонних библиотек или добавление методов в Java Collection Framework — давняя мечта многих разработчиков. И теперь у всех нас есть такая возможность. Объявление расширяющих функций выглядит следующим образом:

В отличии от обычного метода, мы добавляем название класса перед названием метода, чтобы обозначить какой именно класс мы расширяем. В примере AvailabilityTable это псевдоним для типа Matrix и, так как псевдонимы в Kotlin это только переименование, то в результате такая декларация равносильна приведенной в примере ниже, что не всегда удобно:

Но, к сожалению, ничего с этим поделать нельзя, кроме как не использовать инструмент или добавлять методы только в определенный класс контекста. Тогда магия появляется только там, где она нужна. Более того, вы можете расширять этими функциями даже интерфейсы. Хорошим примером будет метод first, расширяющий любой Iterable объект следующим образом:

В итоге, любая коллекция, основанная на интерфейсе Iterable, вне зависимости от типа элемента, получает метод first. Интересно то, что мы можем поместить extension метод в класс контекста и благодаря этому иметь доступ к расширяющему методу только в определенном контексте (см. выше лямбда с контекстом). Более того, мы можем создавать extension функции и для Nullable типов (объяснение Nullable типов выходит за рамки статьи, но при желании вы можете почитать здесь). Например, функция isNullOrEmpty из стандартной библиотеки Kotlin, которая расширяет тип CharSequence?, может быть использована следующим образом:

Сигнатура этой функции представлена ниже:

При работе из Java с такими Kotlin функциями, extension функции доступны как статические.

Infix функции

Очередной способ подсладстить синтаксис — это infix функции. Проще говоря, благодаря этому инструменту мы получили возможность избавиться от лишнего зашумления кода в простых ситуациях.
Блок assertions из основного примера статьи демонстрирует использование этого инструмента:

Такая конструкция эквивалентна следующей:

Есть ситуации, когда скобки и точки излишни. Именно на этот случай нам нужен infix модификатор для функций.
В коде выше, конструкция teacherSchedule[day, lesson] возвращает элемент расписания, а функция shouldNotEqual проверяет, что элемент не равен null.

Чтобы объявить такую функцию необходимо:

  • указать модификатор infix;
  • определить ровно один параметр.

Вы можете комбинировать два последних инструмента, как в коде ниже:

Обратите внимание, что дженерик тип по умолчанию наследник Any (не Nullable иерархии типов), однако, в таких случаях, мы не можем использовать null, по этому необходимо явно указать тип Any?

Контроль контекста

Когда мы используем много вложенных контекстов, то на самом нижнем уровне получается гремучая смесь, так, например, без какого-либо контроля может получиться следующая конструкция, не имеющая смысла:

До версии Kotlin 1.1 уже существовал способ, как этого избежать. Создание собственного метода data во вложенном контексте DataContext, а затем пометка его аннотацией Deprecated с уровнем ERROR.

Благодаря такому поодходу мы могли исключить возможность недопустимого построения DSL. Однако, при большом количестве методов в SchedulingContext, мы получали определенное количество рутинной работы, отбивающей всё желание контролировать контекст.

В Kotlin 1.1 появился новый инструмент для контроля — аннотация @DslMarker. Она применяется на ваши собственные аннотации, которые, в свою очередь, нужны для маркирования ваших контекстов. Создадим свою аннотацию, которую пометим с помощью нового инструмента в нашем арсенале:

Затем необходимо разметить контексты. В нашем основном примере это SchedulingContext и DataContext. Благодяря тому, что мы помечаем каждый из классов единым маркером DSL, происходит следующее:

Не смотря на всю восхитительность такого подхода, сокращающего кучу сил и времени, остается одна проблема. Если вы обратите внимание на наш главный пример, то увидите следующий код:

В этом примере у нас появляется третий уровень вложенности и вместе с ним новый контекст Student, который, на деле, сущностной класс, часть модели, а значит нам нужно пометить аннотацией @MyCustomDslMarker еще и сущностную модель, что, на мой взгляд, не верно.

В контексте Student вызовы data <> всё так же запрещены, т.к. внешний DataContext никуда не делся, но эти конструкции остаются валидны:
javaschedule < data < student < student < >> > >

Пытаясь решить эту проблему с помощью аннотаций, у нас смешивается код для тестирования и бизнес код, а это в большинстве случаев нам не подойдет. Решения здесь три:

Использовать дополнительный контекст для создания студента, например, StudentContext. Это похоже на безумие и перестает оправдывать преимущества @DslMarker.

Создать интерфейсы для всех сущностей, например, IStudent (наименование здесь не важно), создать контексты-пустышки, наследующие эти интерфейсы, и делегировать реализацию объектам студентов, что тоже на грани бреда.
@MyCustomDslMarker
>

Воспользоваться аннотацией @Deprecated, как в примерах выше. В данном случае, пожалуй, это лучшее решение, которым можно воспользоваться.
Просто добавляем deprecated extension метод для всех Identifiable объектов.

В итоге, комбинируя разные инструменты, мы строим комфортный DSL для решения наших задач.

Минусы использования DSL

Попытаемся быть более объективными в применении DSL на Kotlin и разберемся, какие минусы есть у использования DSL в вашем проекте.

Переиспользование части DSL

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

Возможно, вы подскажете интересные варианты, но сейчас мне известно два решения этой проблемы: добавлять «именованные callback’и», как составляющую DSL, или плодить лямбды. Второй вариант проще, но его последствия могут превратиться в самый настоящий ад, когда вы пытаетесь отследить последовательность вызовов. Естественно, когда у нас появляется много императивного поведения подход с DSL начинает от этого страдать, отсюда и эта проблема.

This, it!?

Крайне легко потерять смысл текущего this и it в ходе взаимодействия со своим DSL. Если вы где-то используете it, как название параметра по умолчанию, и осознаете, что осмысленное название для этого параметра будет лучше, то просто сделайте это. Лучше немного очевидного кода, чем много неочевидных багов.

Наличие контекста может сбить с толку человека, который с ними никогда не работал. Однако теперь в вашем арсенале есть «лямбда с контекстом» и вас стало еще труднее поставить в тупик появлянием странных методов внутри DSL. Помните, что на крайний случай вы можете присвоить контекст переменной, например, val mainContext = this

Вложенность

Эта проблема тесно переплетена с первым в нашем списке минусом. Использование вложенных во вложенные во вложенных конструкций двигает весь ваш осмысленный код вправо. Если это терпимо, то пусть так и остается, но в тот момент, когда вы сдвинулись «слишком сильно», разумно применить лямбды. Естественно, такой подход ухудшает читаемость DSL, но это некоторый компромисс, в том случае, когда DSL подразумевает не только создание структур, но и какую-то логику. При создании тестов на DSL (кейс, который мы разбирали в ходе статьи), этой проблемы нет, т.к. данные описываются компактными структурами.

Где доки, Зин?

Если вы когда-либо подступались к чужому DSL, то у вас наверняка вставал вопрос: «Где документация?». На этот счет у меня есть свое мнение. Если вы пишете DSL, который будет использован не только вами, то лучшей документацией будут примеры использования. Сама по себе документация важна, но скорее в качестве дополнительной справки. Смотреть её довольно неудобно, т.к. наблюдатель проблемно-ориентированного языка задается естественным вопросом: «Что мне нужно вызвать, чтобы получить результат?» и, по моему опыту, здесь эффективнее всего себя показывают примеры использования для схожих ситуаций.

Заключение

В статье мы рассмотрели инструменты, благодаря которым вы с легкостью построите собственный проблемно-ориентированный язык. Теперь у вас не должно возникать сомнений о том, как это работает.

Возможно, я что-то ненамеренно пропустил, пожалуйста, напишите об этом в комментариях и статья будет дополнена. Важно помнить, что DSL не панацея. Когда получаешь такой мощный молоток, то всё подряд представляется гвоздём, но это не так.

Потренируйтесь «на кошках», как герой одного известного фильма, сделайте DSL для тестов, а затем, сделав множество ошибок, и после появления опыта, рассмотрите и другие применения.
Желаю успехов в разработке проблемно-ориентированных языков!

Добавить комментарий