Invoke kotlin зачем нужен
Перейти к содержимому

Invoke kotlin зачем нужен

  • автор:

Invoke kotlin зачем нужен

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 > counter2

транслируется в функцию

counter1.compareTo(counter2) > 0

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

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

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

Лямбды

Лямбды всегда находятся внутри фигурных скобок. Слева находятся аргументы, справа — тело функции. Разделяет их специальное выражение ->. Например, создадим выражение.

  x + 5> 

Открыли фигурную скобку, записали параметр в виде числа Int, а затем указали, что с ним нужно делать в правой части — прибавить к числу 5. Параметр может быть один, как в нашем пример, несколько или вообще не быть.

Лямбда-выражение можно сохранить в переменной, а затем обращаться к ней как к обычной функции. Можно вызвать лямбда-выражение при помощи invoke().

 val lambda = < println("Hello Kitty!") >lambda.invoke() 

Лямбда-выражение может послужить удобной заменой для паттернов Listener или Callback.

Примеры

 // Нет аргументов и возвращает 1 < 1 >// () -> Int // Один аргумент в виде строки, который выводится на экран < s: String ->println(s) > // (String)->Unit // два аргумента типа Int и возвращает произведение чисел < a: Int, b: Int ->a * b > // (Int, Int)->Int 

Код с лямбдами становится короче и читабельнее.

 button.setOnClickListener < /* код для щелчка кнопки */ >
 val sum = < x: Int, y: Int ->x + y > println(sum(1, 2)) // 3 

Лямбды можно записывать в несколько строк.

 val test = < a: Int, b: Int ->println("$a + $b") a + b > 

Этот же пример можно записать одной строкой, разделяя команды точкой с запятой.

val test = < a: Int, b: Int ->println(«$a + $b»); a + b >

setOnClickListener

Рассмотрим применение лямбда-выражений на примере обработчика щелчка кнопки.

Код на Java 7 и ниже.

 button.setOnClickListener(new OnClickListener() < @Override public void onClick(View view) < /* код для щелчка кнопки */ >>); 

Если использовать такой же код на Kotlin, то получим.

 button.setOnClickListener(object : View.OnClickListener < override fun onClick(v: View?) < println("Hello Kitty") >>) 

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

 button.setOnClickListener( < v ->println("Hello Kitty") >) 

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

 button.setOnClickListener() < v ->println("Hello Kitty") > 

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

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

 button.setOnClickListener < v ->println("Hello Kitty") > 

Если в самом лямбда-выражении не используется параметр в левой части, то его можно удалить. Это справедливо для выражения с одним параметром. В нашем примере v (View) не используется, сокращаем код ещё раз.

 button.setOnClickListener

Функция с одним параметром. Ключевое слово it

Рассмотрим частный случай, когда функция только получает параметр. Мы можем не использовать левую часть, а использовать ключевое слово it. Например, мы используем v для его передачи другой функции.

 button.setOnClickListener < v ->doSomething(v) > private fun doSomething(v: View?)

Убираем левую часть и используем it.

 button.setOnClickListener

Все варианты в одном месте.

 button.setOnClickListener( < v ->println("Hello Kitty") >) button.setOnClickListener() < v ->println("Hello Kitty") > button.setOnClickListener < v ->println("Hello Kitty") > button.setOnClickListener < println("Hello Kitty") >button.setOnClickListener < v ->doSomething(v) > button.setOnClickListener

Другой пример использования — пройтись по элементам коллекции. Например, воспользуемся forEach:

 val cats = listOf("Barsik", "Murzik", "Ryzhik") cats.forEach < println(it) >// Выводит Barsik Murzik Ryzhik 

Без использования ключевого слова it пришлось бы писать длинный вариант.

 cats.forEach < cat ->println(cat) > 

Функции высшего порядка

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

Напишем функцию convert(), которая преобразует значение Double по формуле, передаваемой в лямбда-выражении, выводит результат и возвращает её. Например, с помощью этой функции вы сможете преобразовать температуру по Цельсию в температуру по Фаренгейту или преобразовать вес из килограммов в фунты — все зависит от формулы, которая передаётся в лямбда-выражении (аргумента). Начнём с определения параметров функции.

Для этого в функцию будут добавлены два параметра: Double и лямбда-выражение. Назовём лямбда-выражение converter, а поскольку оно будет использоваться для преобразования Double в Double, оно должно иметь тип (Double) -> Double (лямбда-выражение, которое получает параметр Double и возвращает Double).

 fun convert(x: Double, converter: (Double) -> Double) : Double < // ваш код >

Напишем какой-то для функции.

 fun convert(x: Double, converter: (Double) -> Double) : Double < val result = converter(x) println("$x is converted to $result") // выводим результат return result // вернуть результат >

Функция с лямбда-параметром вызывается точно так же, как и любая другая функция: с передачей значений всех аргументов. В данном случае Double и лямбда-выражений.

Давайте используем функцию convert() для преобразования 20 градусов по Цельсию в градусы по Фаренгейту. Для этого функции нужно передать значения 20.0 и лямбда-выражение < c: Double ->c * 1.8 + 32 >:

 convert(20.0, < c: Double ->c * 1.8 + 32 >) // 20.0 is converted to 68.0 // с присвоением возвращаемого результата переменной val fahrenheit = convert(20.0, < c: Double ->c * 1.8 + 32 >) println(fahrenheit) 

Если последний параметр вызываемой функции является лямбда-выражением, лямбда-аргумент можно вынести за круглые скобки вызова функции.

 // было convert(20.0, < c: Double ->c * 1.8 + 32 >) // стало convert(20.0) < c: Double ->c * 1.8 + 32 > 

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

Предположим, функция convertFive() преобразует Int 5 в Double по формуле преобразования, которая передаётся в виде лямбда-выражения.

 fun convertFive(converter: (Int) -> Double) : Double
 // стандартный вариант convertFive() < it * 1.8 + 32 >// компактный вариант без круглых скобок convertFive

Функция может возвращать лямбда-выражение. Например, следующий код определяет функцию с именем getConversionLambda(), которая возвращает лямбда-выражение типа (Double) -> Double. Точное лямбда-выражение, возвращаемое функцией, зависит от значения переданной строки.

 fun getConversionLambda(str: String): (Double) -> Double < if (str == "CentigradeToFahrenheit") < return < it * 1.8 + 32 >> else if (str == "KgsToPounds") < return < it * 2.204623 >> else if (str == "PoundsToUSTons") < return < it / 2000.0 >> else < return < it >> > 

Вы можете вызвать лямбда-выражение, возвращённое функцией, или использовать его в аргументе при вызове другой функции. Например, следующий код выполняет возвращаемое значение getConversionLambda для пересчета 2,5 кг в фунты и присваивает его переменной с именем pounds:

 val pounds = getConversionLambda("KgsToPounds")(2.5) println(pounds) 

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

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

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

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

interface IndexedContainer

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

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

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

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

Выражение Транслируется в
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

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

Эта таблица демонстрирует, что компилятор при обрабатывании, к примеру, выражения +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)" >

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

Выражение Транслируется в
a++ a.inc() + см. ниже
a— a.dec() + см. ниже

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

Компилятор осуществляет следующие шаги, обрабатывая операторы в постфиксной форме, например 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

Выражение Транслируется в
a in b b.contains(a)
a !in b !b.contains(a)

Для 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 с соответствующим числом аргументов.

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

Выражение Транслируется в
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, . i_n) a.invoke(i_1, . i_n)

Оператор вызова (функции, метода) в круглых скобках транслируется в 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.

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

    Выражение Транслируется в
    a == b a?.equals(b) ?: (b === null)
    a != b !(a?.equals(b) ?: (b === null))

    Эти операторы работают только с функцией 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

    Как себе выстрелить в ногу в 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) 

    Никакого undefined behaviour, результат, очевидно, 12

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

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *