Kotlin operator fun invoke

Как себе выстрелить в ногу в Kotlin

Совсем недавно вышел релиз Kotlin, а его команда разработчиков предлагала задавать вопросы про язык. Он сейчас на слуху и, возможно, многим хочется его попробовать.
Пару недель назад тимлид сделал для компании презентацию о том, что в Котлине хорошо. Одним из самых интересных вопросов был «А как в Котлине выстрелить себе в ногу?» Так получилось, что ответил на этот вопрос я.

Disclaimer:
Не стоит воспринимать эту статью как «Kotlin — отстой». Хотя я отношусь скорее к категории тех, кому и со Scala хорошо, я считаю, что язык неплохой.
Все пункты спорные, но раз в год и палка стреляет. Когда-то вы себе прострелите заодно и башку, а когда-то у вас получится выстрелить только в полночь полнолуния, если вы предварительно совершите черный ритуал создания плохого кода.

Наша команда недавно закончила большой проект на Scala, сейчас делаем проект помельче на Kotlin, поэтому в спойлерах будет сравнение со Scala. Я буду считать, что Nullable в Kotlin — это эквивалент Option, хотя это совсем не так, но, скорее всего, большинство из тех, кто работал с Option, будут вместо него использовать Nullable.

1. Пост-инкремент и преинкремент как выражения

Цитирую вопрошавшего: «Фу, это ж баян, скучно». Столько копий сломано, миллион вопросов на собеседованиях C++… Если есть привычка, то можно было его оставить инструкцией (statement’ом). Справедливости ради, другие операторы, вроде +=, являются инструкциями.
Цитирую одного из разработчиков, abreslav:

Смотрели на юзкейсы, увидели, что поломается, решили оставить.

Замечу, что у нас тут не С++, и на собеседовании про инкремент спросить особо нечего. Разве что разницу между префиксным и постфиксным.

 var i = 5 i = i++ + i++ println(i) 
 var a = 5 a = ++a + ++a println(a) 

Источник

Kotlin operator fun invoke

Kotlin позволяет определить для типов ряд встроенных операторов. Для определения оператора для типа определяется функция с ключевым словом operator :

operator fun название_оператора([параметры_оператора]) : возвращаемый_тип< // действия функции оператора >

После ключевого слова fun идет название оператора и далее скобки. Если оператор бинарный, то в скобках указывается параметр оператора. После скобок через двоеточие указывается возвращаемый тип.

Рассмотрим простейший пример. Допустим, у нас есть класс Counter, который представляет некоторый счетчик:

class Counter(var value: Int)

Свойство value собственно хранит значение счетчика.

И допустим, у нас есть два объекта класса Counter — два счетчика, которые мы хотим сравнивать или складывать на основании их свойства value, используя стандартные операции сравнения и сложения:

val counter1 = Counter(5) val counter2 = Counter(7) val result = counter1 > counter2; val counter3: Counter = counter1 + counter2;

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

fun main() < val counter1 = Counter(5) val counter2 = Counter(7) val counter1IsGreater = counter1 >counter2 val counter3: Counter = counter1 + counter2 println(counter1IsGreater) // false println(counter3.value) // 12 > class Counter(var value: Int) < operator fun compareTo(counter: Counter) : Int< return this.value - counter.value >operator fun plus(counter: Counter): Counter < return Counter(this.value + counter.value) >>

Переопределение операторов предполагает переопределение соответствующих этим операторам функций. Например, операция сравнения

counter1.compareTo(counter2) > 0

То есть, если левый операнд (counter1) операции больше чем правый операнд (counter2), то функция оператора должна возвращать число больше 0. И в данном случае мы можем просто вычесть из counter1.value значение counter2.value , чтобы определить, больше ли counter1 чем counter2:

Читайте также:  How to use python script in windows

operator fun compareTo(counter: Counter) : Int

Оператор сложения + транслируется в функцию plus() . Параметр этой функции представляет правый операнд операции. Левый операнд доступен через ключевое слово this :

operator fun plus(counter: Counter): Counter

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

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

fun main() < val counter1 = Counter(5) val counter2 = Counter(3) val counter3: Counter = counter1 + counter2 val counter4: Counter = 33 + counter1 println(counter3.value) // 8 println(counter4.value) // 38 >class Counter(val value: Int) operator fun Counter.plus(counter: Counter): Counter < return Counter(this.value + counter.value) >operator fun Int.plus(counter: Counter): Counter

Здесь для класса Counter определена опрерация сложения с помощью функции расширения. Но кроме того, здесь также определен оператор сложения и для встроенного типа Int — в данном случае в качестве правого операнда будет передаваться объект Counter и результатом операции также будет объект Counter:

operator fun Int.plus(counter: Counter): Counter

Благодаря этому мы также сможем складывать объекты Int и Counter:

val counter4: Counter = 33 + counter1

Рассмотрим, какие операторы мы можем переопределить

Источник

Operator overloading

Kotlin allows you to provide custom implementations for the predefined set of operators on types. These operators have predefined symbolic representation (like + or * ) and precedence. To implement an operator, provide a member function or an extension function with a specific name for the corresponding type. This type becomes the left-hand side type for binary operations and the argument type for the unary ones.

To overload an operator, mark the corresponding function with the operator modifier:

When overriding your operator overloads, you can omit operator :

Unary operations

Unary prefix operators

This table says that when the compiler processes, for example, an expression +a , it performs the following steps:

  • Determines the type of a , let it be T .
  • Looks up a function unaryPlus() with the operator modifier and no parameters for the receiver T , that means a member function or an extension function.
  • If the function is absent or ambiguous, it is a compilation error.
  • If the function is present and its return type is R , the expression +a has type R .
Читайте также:  Java для samsung note

These operations, as well as all the others, are optimized for basic types and do not introduce overhead of function calls for them.

As an example, here’s how you can overload the unary minus operator:

data class Point(val x: Int, val y: Int) operator fun Point.unaryMinus() = Point(-x, -y) val point = Point(10, 20) fun main() < println(-point) // prints "Point(x=-10, y=-20)" >

Increments and decrements

The inc() and dec() functions must return a value, which will be assigned to the variable on which the ++ or — operation was used. They shouldn’t mutate the object on which the inc or dec was invoked.

The compiler performs the following steps for resolution of an operator in the postfix form, for example a++ :

  • Determines the type of a , let it be T .
  • Looks up a function inc() with the operator modifier and no parameters, applicable to the receiver of type T .
  • Checks that the return type of the function is a subtype of T .

The effect of computing the expression is:

  • Store the initial value of a to a temporary storage a0 .
  • Assign the result of a0.inc() to a .
  • Return a0 as the result of the expression.

For a— the steps are completely analogous.

For the prefix forms ++a and —a resolution works the same way, and the effect is:

  • Assign the result of a.inc() to a .
  • Return the new value of a as a result of the expression.

Источник

Перегрузка операторов

Kotlin позволяет реализовывать предопределённый набор операторов для ваших типов. Эти операторы имеют фиксированное символическое представление (вроде + или * ) и фиксированные приоритеты. Для реализации оператора предоставьте функцию-член или функцию-расширение с фиксированным именем и с соответствующим типом, т. е. левосторонним типом для бинарных операций или типом аргумента для унарных операций.

Функции, которые перегружают операторы, должны быть отмечены модификатором operator .

interface IndexedContainer

При переопределении перегрузок оператора вы можете опускать operator .

class OrdersList: IndexedContainer < override fun get(index: Int) < /*. */ >> 

Унарные операторы

Унарные префиксные операторы

Под транслированием, понимается представление вашего кода после компиляции.

Эта таблица демонстрирует, что компилятор при обрабатывании, к примеру, выражения +a , осуществляет следующие действия:

  • Определяет тип выражения a , пусть это будет T ;
  • Ищет функцию unaryPlus() с модификатором operator без параметров для приёмника типа Т , т. е. функцию-член или функцию-расширение;
  • Если функция отсутствует или неоднозначная, возвращается ошибка компиляции;
  • Если функция присутствует и R — её возвращаемый тип, выражение +a имеет Тип R .

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

Например, вы можете перегрузить оператор унарного минуса:

data class Point(val x: Int, val y: Int) operator fun Point.unaryMinus() = Point(-x, -y) fun main() < val point = Point(10, 20) println(-point) // выведет "Point(x=-10, y=-20)" >

Инкремент и декремент

Функции inc() и dec() должны возвращать значение, которое будет присвоено переменной, к которой была применена операция ++ или — . Они не должны изменять сам объект, для которого были вызваны эти функции.

Читайте также:  Sort files by date python

Компилятор осуществляет следующие шаги, обрабатывая операторы в постфиксной форме, например a++ :

  • Определяет тип переменной a , пусть это будет T ;
  • Ищет функцию inc() с модификатором operator без параметров, применимую для приёмника типа Т .
  • Проверяет, что возвращаемый тип такой функции является подтипом T .

Результатами вычисления выражения является:

  • Сохранение инициализирующего значения a во временную переменную a0 ,
  • Сохранение результата выполнения a0.inc() в a ,
  • Возвращение a0 как результата вычисления выражения (т.е. значения до инкремента).

Для a— шаги выполнения полностью аналогичные.

Для префиксной формы ++a или —a действует также, но результатом будет:

  • Присвоение результата вычисления a.inc() непосредственно a ,
  • Возвращение нового значения a как результата вычисления выражения.

Бинарные операции

Арифметические операции

Выражение Транслируется в
a + b a.plus(b)
a — b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)

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

Ниже приведен пример класса Counter (счетчик), начинающего счёт с заданного значения, которое может быть увеличено с помощью перегруженного оператора + :

data class Counter(val dayIndex: Int) < operator fun plus(increment: Int): Counter < return Counter(dayIndex + increment) >> 

Оператор in

Для in и !in используется одна и та же процедура, только возвращаемый результат инвертируется.

Оператор доступа по индексу

Выражение Транслируется в
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, . i_n] a.get(i_1, . i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, . i_n] = b a.set(i_1, . i_n, b)

Квадратные скобки транслируются в вызов get или set с соответствующим числом аргументов.

Оператор вызова

Оператор вызова (функции, метода) в круглых скобках транслируется в invoke с соответствующим числом аргументов.

Присвоения с накоплением

Выражение Транслируется в
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.modAssign(b)

Для присваивающих операций, таких как a += b , компилятор осуществляет следующие шаги:

  • Если функция из правой колонки таблицы доступна:
    • Если соответствующая бинарная функция (например plus() для plusAssign() ) также доступна, a — изменяемая переменная и возвращаемый тип plus является подтипом типа a , то фиксируется ошибка (неоднозначность);
    • Проверяется, что возвращаемое значение функции Unit , в противном случае фиксируется ошибка;
    • Генерируется код для a.plusAssign(b) .

    Присвоение НЕ ЯВЛЯЕТСЯ выражением в Kotlin.

    Операторы равенства и неравенства

    Эти операторы работают только с функцией equals(other: Any?): Boolean , которая может быть переопределена для обеспечения пользовательской реализации проверки равенства. Любая другая функция с тем же именем (например, equals(other: Foo) ) вызываться не будет.

    `===` and `!==` (identity checks) are not overloadable, so no conventions exist for them. —>

    Операции === и !== (проверка идентичности) являются неперегружаемыми, поэтому никакие соглашения для них не приводятся.

    Операция == имеет специальный смысл: она транслируется в составное выражение, в котором экранируются значения null . null == null — это всегда истина, а x == null для non-null значений x — всегда ложь, и не будет расширяться в x.equals() .

    Операторы сравнений

    Выражение Транслируется в
    a > b a.compareTo(b) > 0
    a < b a.compareTo(b) < 0
    a >= b a.compareTo(b) >= 0
    a a.compareTo(b)

    Все сравнения транслируются в вызовы compareTo , от которых требуется возврат значения типа Int .

    Операторы делегирования свойств

    Операторы provideDelegate , getValue и setValue описаны в Делегированные свойства.

    Инфиксные вызовы именованных функций

    Вы можете имитировать инфиксные операции, используя инфиксную запись.

    © 2015—2023 Open Source Community

    Источник

Оцените статью