505 lines
21 KiB
Markdown
505 lines
21 KiB
Markdown
---
|
||
title: Инфиксные и операторные функции
|
||
description: infix-нотация, перегрузка операторов, invoke, деструктуризация и делегирование свойств
|
||
tags:
|
||
- kotlin
|
||
- functional
|
||
---
|
||
|
||
Kotlin позволяет задать для своих типов тот же синтаксис, что у встроенных: `a + b`, `a[i]`, `a()`, `a in b`. Делается это обычными функциями с модификатором `operator` или `infix`.
|
||
|
||
## infix
|
||
```kotlin
|
||
infix fun Int.упаковатьВ(unit: String): String = "$this $unit"
|
||
|
||
data class Money(val amount: Long, val currency: String)
|
||
|
||
infix fun Long.of(currency: String) = Money(this, currency)
|
||
|
||
fun main() {
|
||
println(5 упаковатьВ "кг") // 5 кг
|
||
println(100L of "RUB") // Money(amount=100, currency=RUB)
|
||
println(100L.of("RUB")) // Money(amount=100, currency=RUB) — обычный вызов тоже работает
|
||
}
|
||
```
|
||
Требования к `infix`: ровно один параметр, не `vararg`, без значения по умолчанию, и функция должна быть методом или расширением.
|
||
|
||
В stdlib `infix` используется для операций, читающихся как фраза:
|
||
```kotlin
|
||
fun main() {
|
||
println(setOf(1, 2) union setOf(2, 3)) // [1, 2, 3]
|
||
println(setOf(1, 2) intersect setOf(2, 3)) // [2]
|
||
println("a" to 1) // (a, 1)
|
||
println(5 shl 2) // 20
|
||
println(5 and 3) // 1
|
||
println(1 until 5) // 1..4
|
||
println(5 downTo 1) // 5 downTo 1 step 1
|
||
println(1..10 step 2) // 1..9 step 2
|
||
}
|
||
```
|
||
|
||
Приоритет инфиксных функций ниже арифметики, но выше сравнений - это источник ошибок:
|
||
```kotlin
|
||
infix fun Int.plusTen(x: Int) = this + x + 10
|
||
|
||
fun main() {
|
||
println(1 plusTen 2 * 3) // 17 — это 1 plusTen (2*3)
|
||
println((1 plusTen 2) * 3) // 39
|
||
}
|
||
```
|
||
При сомнении ставьте скобки.
|
||
|
||
## Перегрузка операторов
|
||
Каждый оператор отображается в функцию с фиксированным именем:
|
||
|
||
| Оператор | Функция |
|
||
|---|---|
|
||
| `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` / `-a` | `a.unaryPlus()` / `a.unaryMinus()` |
|
||
| `!a` | `a.not()` |
|
||
| `a++` / `a--` | `a.inc()` / `a.dec()` |
|
||
| `a += b` | `a.plusAssign(b)` или `a = a.plus(b)` |
|
||
| `a == b` | `a?.equals(b) ?: (b === null)` |
|
||
| `a > b` | `a.compareTo(b) > 0` |
|
||
| `a[i]` | `a.get(i)` |
|
||
| `a[i] = v` | `a.set(i, v)` |
|
||
| `a()` | `a.invoke()` |
|
||
| `a in b` | `b.contains(a)` |
|
||
| `a..b` | `a.rangeTo(b)` |
|
||
| `a..<b` | `a.rangeUntil(b)` |
|
||
| `for (x in a)` | `a.iterator()` |
|
||
| `val (x, y) = a` | `a.component1()`, `a.component2()` |
|
||
|
||
Полный пример - вектор:
|
||
```kotlin
|
||
data class Vec(val x: Double, val y: Double) {
|
||
operator fun plus(other: Vec) = Vec(x + other.x, y + other.y)
|
||
operator fun minus(other: Vec) = Vec(x - other.x, y - other.y)
|
||
operator fun times(scalar: Double) = Vec(x * scalar, y * scalar)
|
||
operator fun unaryMinus() = Vec(-x, -y)
|
||
operator fun get(index: Int): Double = when (index) {
|
||
0 -> x
|
||
1 -> y
|
||
else -> throw IndexOutOfBoundsException("индекс $index")
|
||
}
|
||
|
||
val length: Double get() = kotlin.math.sqrt(x * x + y * y)
|
||
}
|
||
|
||
fun main() {
|
||
val a = Vec(1.0, 2.0)
|
||
val b = Vec(3.0, 4.0)
|
||
|
||
println(a + b) // Vec(x=4.0, y=6.0)
|
||
println(b - a) // Vec(x=2.0, y=2.0)
|
||
println(a * 2.0) // Vec(x=2.0, y=4.0)
|
||
println(-a) // Vec(x=-1.0, y=-2.0)
|
||
println(b[0]) // 3.0
|
||
println(b.length) // 5.0
|
||
}
|
||
```
|
||
Умножение на скаляр справа - метод класса; чтобы работало и слева, нужно расширение:
|
||
```kotlin
|
||
data class Vec2(val x: Double, val y: Double) {
|
||
operator fun times(scalar: Double) = Vec2(x * scalar, y * scalar)
|
||
}
|
||
|
||
operator fun Double.times(v: Vec2) = v * this
|
||
|
||
fun main() {
|
||
val v = Vec2(1.0, 2.0)
|
||
println(v * 3.0) // Vec2(x=3.0, y=6.0)
|
||
println(3.0 * v) // Vec2(x=3.0, y=6.0)
|
||
}
|
||
```
|
||
|
||
## Операторы как расширения
|
||
Оператор можно добавить к чужому типу:
|
||
```kotlin
|
||
operator fun String.times(n: Int) = repeat(n)
|
||
|
||
operator fun <T> List<T>.get(range: IntRange): List<T> = subList(range.first, range.last + 1)
|
||
|
||
fun main() {
|
||
println("ab" * 3) // ababab
|
||
println(listOf(1, 2, 3, 4, 5)[1..3]) // [2, 3, 4]
|
||
}
|
||
```
|
||
Так в stdlib добавлены `Map.get` через `[]`, `List.plus` и операции над множествами.
|
||
|
||
## invoke
|
||
`operator fun invoke` позволяет вызывать объект как функцию:
|
||
```kotlin
|
||
class Multiplier(private val factor: Int) {
|
||
operator fun invoke(x: Int) = x * factor
|
||
}
|
||
|
||
fun main() {
|
||
val triple = Multiplier(3)
|
||
println(triple(5)) // 15
|
||
println(triple.invoke(5)) // 15 — то же самое
|
||
}
|
||
```
|
||
Практическое применение - объект с состоянием, который ведёт себя как функция:
|
||
```kotlin
|
||
class RateLimiter(private val maxCalls: Int) {
|
||
private var calls = 0
|
||
operator fun invoke(): Boolean {
|
||
calls++
|
||
return calls <= maxCalls
|
||
}
|
||
}
|
||
|
||
fun main() {
|
||
val limiter = RateLimiter(2)
|
||
println("${limiter()} ${limiter()} ${limiter()}") // true true false
|
||
}
|
||
```
|
||
`invoke` на `companion object` даёт фабрику с синтаксисом конструктора:
|
||
```kotlin
|
||
class Email private constructor(val value: String) {
|
||
companion object {
|
||
operator fun invoke(raw: String): Email? =
|
||
if (raw.contains("@")) Email(raw.trim().lowercase()) else null
|
||
}
|
||
override fun toString() = value
|
||
}
|
||
|
||
fun main() {
|
||
println(Email(" Ivan@Example.COM ")) // ivan@example.com
|
||
println(Email("не почта")) // null
|
||
}
|
||
```
|
||
Это способ сделать «конструктор, возвращающий null» - настоящий конструктор так не может.
|
||
|
||
## Сравнение: compareTo
|
||
```kotlin
|
||
class Version(val major: Int, val minor: Int) : Comparable<Version> {
|
||
override fun compareTo(other: Version): Int =
|
||
compareValuesBy(this, other, { it.major }, { it.minor })
|
||
|
||
override fun toString() = "$major.$minor"
|
||
}
|
||
|
||
fun main() {
|
||
val a = Version(1, 5)
|
||
val b = Version(2, 0)
|
||
|
||
println(a < b) // true
|
||
println(a >= Version(1, 5)) // true
|
||
println(listOf(b, a, Version(1, 9)).sorted()) // [1.5, 1.9, 2.0]
|
||
println(maxOf(a, b)) // 2.0
|
||
println(a in Version(1, 0)..Version(1, 9)) // true
|
||
}
|
||
```
|
||
Одна функция `compareTo` даёт `<`, `>`, `<=`, `>=`, сортировку, `maxOf`/`minOf` и диапазоны. `compareValuesBy` сравнивает по нескольким ключам по порядку.
|
||
|
||
## in: contains
|
||
```kotlin
|
||
data class DateRange(val from: Int, val to: Int) {
|
||
operator fun contains(day: Int) = day in from..to
|
||
}
|
||
|
||
fun main() {
|
||
val week = DateRange(1, 7)
|
||
println(3 in week) // true
|
||
println(10 in week) // false
|
||
println(10 !in week) // true — !in получается автоматически
|
||
}
|
||
```
|
||
|
||
## Итератор
|
||
```kotlin
|
||
class Countdown(private val from: Int) {
|
||
operator fun iterator(): Iterator<Int> = object : Iterator<Int> {
|
||
private var current = from
|
||
override fun hasNext() = current > 0
|
||
override fun next() = current--
|
||
}
|
||
}
|
||
|
||
fun main() {
|
||
for (i in Countdown(3)) print("$i ")
|
||
println() // 3 2 1
|
||
}
|
||
```
|
||
`iterator` можно добавить и расширением - тогда `for` заработает на чужом типе:
|
||
```kotlin
|
||
operator fun ClosedRange<java.time.LocalDate>.iterator(): Iterator<java.time.LocalDate> =
|
||
object : Iterator<java.time.LocalDate> {
|
||
private var current = start
|
||
override fun hasNext() = current <= endInclusive
|
||
override fun next() = current.also { current = current.plusDays(1) }
|
||
}
|
||
|
||
fun main() {
|
||
val from = java.time.LocalDate.of(2026, 1, 1)
|
||
val to = java.time.LocalDate.of(2026, 1, 3)
|
||
for (d in from..to) print("${d.dayOfMonth} ")
|
||
println() // 1 2 3
|
||
}
|
||
```
|
||
|
||
## get и set
|
||
```kotlin
|
||
class Matrix(private val rows: Int, private val cols: Int) {
|
||
private val data = DoubleArray(rows * cols)
|
||
|
||
operator fun get(i: Int, j: Int): Double = data[i * cols + j]
|
||
operator fun set(i: Int, j: Int, value: Double) { data[i * cols + j] = value }
|
||
|
||
override fun toString() = (0 until rows).joinToString("\n") { i ->
|
||
(0 until cols).joinToString(" ") { j -> this[i, j].toString() }
|
||
}
|
||
}
|
||
|
||
fun main() {
|
||
val m = Matrix(2, 2)
|
||
m[0, 0] = 1.0
|
||
m[1, 1] = 2.0
|
||
println(m[0, 0]) // 1.0
|
||
println(m)
|
||
// 1.0 0.0
|
||
// 0.0 2.0
|
||
}
|
||
```
|
||
`get` и `set` принимают любое число аргументов - это даёт многомерную индексацию, которой нет в Java.
|
||
|
||
## plusAssign против plus
|
||
```kotlin
|
||
class Basket {
|
||
private val items = mutableListOf<String>()
|
||
operator fun plusAssign(item: String) { items.add(item) }
|
||
override fun toString() = items.toString()
|
||
}
|
||
|
||
fun main() {
|
||
val basket = Basket()
|
||
basket += "хлеб"
|
||
basket += "молоко"
|
||
println(basket) // [хлеб, молоко]
|
||
}
|
||
```
|
||
Если объявлены оба (`plus` и `plusAssign`) и переменная `var` - компилятор сообщит об неоднозначности. Правило: `plusAssign` для изменяемых коллекций-подобных типов, `plus` для значений.
|
||
|
||
## Деструктуризация
|
||
```kotlin
|
||
class Pair3(val first: Int, val second: String) {
|
||
operator fun component1() = first
|
||
operator fun component2() = second
|
||
}
|
||
|
||
fun main() {
|
||
val (num, text) = Pair3(1, "один")
|
||
println("$num = $text") // 1 = один
|
||
}
|
||
```
|
||
`data class` объявляет `componentN` автоматически. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
|
||
|
||
## Делегирование свойств
|
||
Операторы `getValue`/`setValue` позволяют подменить реализацию свойства:
|
||
```kotlin
|
||
import kotlin.reflect.KProperty
|
||
|
||
class Trimmed {
|
||
private var value: String = ""
|
||
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
|
||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
|
||
value = newValue.trim()
|
||
}
|
||
}
|
||
|
||
class Form {
|
||
var login: String by Trimmed()
|
||
}
|
||
|
||
fun main() {
|
||
val f = Form()
|
||
f.login = " ivan "
|
||
println("[${f.login}]") // [ivan]
|
||
}
|
||
```
|
||
Подробнее о делегатах и готовых реализациях (`lazy`, `observable`, `map`): [Делегирование](../ООП/Делегирование.md)
|
||
|
||
## Когда перегружать оператор
|
||
| Ситуация | Стоит ли |
|
||
|---|---|
|
||
| математический тип (вектор, матрица, деньги, дата) | да |
|
||
| коллекция-подобный тип (`[]`, `in`, `for`) | да |
|
||
| сравнение по естественному порядку | да, через `Comparable` |
|
||
| «красивее выглядит» | нет |
|
||
| смысл оператора неочевиден (`user + role`) | нет |
|
||
| операция может бросить исключение неожиданно | нет |
|
||
|
||
Критерий простой: оператор должен означать то же, что для встроенных типов. `+` обязан быть коммутативным по смыслу и не иметь побочных эффектов; `==` - соответствовать `hashCode`.
|
||
|
||
> [!info] Отличия от Java
|
||
> | Java | Kotlin |
|
||
> |---|---|
|
||
> | перегрузки операторов нет | `operator fun` |
|
||
> | `a.add(b)` | `a + b` |
|
||
> | `list.get(i)` | `list[i]` |
|
||
> | `map.put(k, v)` | `map[k] = v` |
|
||
> | `a.compareTo(b) < 0` | `a < b` |
|
||
> | `a.equals(b)` | `a == b` |
|
||
> | `BigDecimal.add().multiply()` | `a + b * c` |
|
||
> | инфиксной нотации нет | `infix fun` |
|
||
>
|
||
> **Java не поддерживает перегрузку операторов принципиально** - кроме `+` для строк, который встроен в компилятор. Это сознательное решение дизайнеров Java: операторы с произвольным смыслом усложняют чтение. Kotlin выбрал компромисс: перегрузка есть, но набор операторов фиксирован и имена функций заданы жёстко, так что `plus` всегда означает `+` и нельзя придумать свой оператор.
|
||
>
|
||
> **Разница заметнее всего на числовых типах.** `BigDecimal` в Java:
|
||
> ```java
|
||
> BigDecimal result = a.multiply(b).add(c).divide(d, RoundingMode.HALF_UP);
|
||
> ```
|
||
> В Kotlin те же операторы уже объявлены в stdlib как расширения:
|
||
> ```kotlin
|
||
> import java.math.BigDecimal
|
||
>
|
||
> fun main() {
|
||
> val a = BigDecimal("10.5")
|
||
> val b = BigDecimal("2")
|
||
> println(a + b) // 12.5
|
||
> println(a * b) // 21.0
|
||
> println(a > b) // true
|
||
> }
|
||
> ```
|
||
> Для денежных расчётов это главный практический выигрыш: формула в коде выглядит как формула.
|
||
>
|
||
> **`==` в Kotlin - это `equals`, а не сравнение ссылок.** Самое важное отличие на практике: в Java `a == b` для объектов сравнивает ссылки, и забытый `.equals()` - классическая ошибка со строками. В Kotlin `==` вызывает `equals` (с корректной обработкой null), а для ссылок есть отдельный `===`:
|
||
> ```kotlin
|
||
> fun main() {
|
||
> val a = StringBuilder("x").toString()
|
||
> val b = StringBuilder("x").toString()
|
||
> println(a == b) // true — по значению
|
||
> println(a === b) // false — разные объекты
|
||
> }
|
||
> ```
|
||
>
|
||
> **`infix` даёт читаемость без DSL-библиотек.** В Java тестовые фреймворки имитируют инфиксную запись цепочкой методов: `assertThat(x).isEqualTo(y)`. В Kotlin это записывается как `x shouldBe y` - подробнее: [Kotest](../Тестирование/Kotest.md)
|
||
>
|
||
> **`invoke` аналога не имеет.** В Java объект нельзя вызвать как функцию; ближайшее - интерфейс с методом `apply`, вызываемый явно.
|
||
>
|
||
> Сравнить: [Операторы и выражения в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Операторы%20и%20выражения.md), [Класс Object](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Класс%20Object.md)
|
||
|
||
> [!warning] Подводные камни
|
||
> **Приоритет `infix` ниже арифметики:**
|
||
> ```kotlin
|
||
> infix fun Int.append(x: Int) = "$this$x"
|
||
>
|
||
> fun main() {
|
||
> println(1 append 2 + 3) // 15 — это 1 append (2+3)
|
||
> println((1 append 2) + 3) // 123
|
||
> }
|
||
> ```
|
||
> Выражение выглядит как последовательность слева направо, но арифметика вычисляется первой.
|
||
>
|
||
> **`infix` нельзя использовать без получателя и с `this` неявно:**
|
||
> ```kotlin
|
||
> class Calc {
|
||
> infix fun add(x: Int) = x + 1
|
||
> fun demo(): Int {
|
||
> // return add 5 // ошибка
|
||
> return this add 5 // так можно
|
||
> }
|
||
> }
|
||
>
|
||
> fun main() { println(Calc().demo()) } // 6
|
||
> ```
|
||
>
|
||
> **`plus` не меняет объект, `plusAssign` меняет - и перепутать легко:**
|
||
> ```kotlin
|
||
> fun main() {
|
||
> var list1 = listOf(1, 2)
|
||
> list1 += 3 // новый список: plus + присваивание
|
||
> println(list1) // [1, 2, 3]
|
||
>
|
||
> val list2 = mutableListOf(1, 2)
|
||
> list2 += 3 // изменение на месте: plusAssign
|
||
> println(list2) // [1, 2, 3]
|
||
> }
|
||
> ```
|
||
> Результат одинаковый, стоимость разная: первый вариант копирует весь список.
|
||
>
|
||
> **Объявление и `plus`, и `plusAssign` даёт ошибку компиляции при `var`:**
|
||
> ```kotlin
|
||
> class Both(val v: Int) {
|
||
> operator fun plus(x: Int) = Both(v + x)
|
||
> operator fun plusAssign(x: Int) { }
|
||
> }
|
||
>
|
||
> fun main() {
|
||
> val b = Both(1)
|
||
> b += 1 // выбран plusAssign — для val это единственный вариант
|
||
> println(b.v) // 1
|
||
> }
|
||
> ```
|
||
> Для `var`-переменной компилятор не сможет выбрать между `b = b + 1` и `b.plusAssign(1)`.
|
||
>
|
||
> **`inc`/`dec` должны возвращать новый объект:**
|
||
> ```kotlin
|
||
> class Counter4(val value: Int) {
|
||
> operator fun inc() = Counter4(value + 1)
|
||
> }
|
||
>
|
||
> fun main() {
|
||
> var c = Counter4(0)
|
||
> c++
|
||
> println(c.value) // 1
|
||
> }
|
||
> ```
|
||
> Если `inc` изменит объект и вернёт `this`, постфиксный `c++` сломается: он должен вернуть **старое** значение, а вернёт изменённое.
|
||
>
|
||
> **`equals` нельзя объявить расширением:**
|
||
> ```kotlin
|
||
> class NoEquals(val v: Int)
|
||
>
|
||
> // operator fun NoEquals.equals(other: Any?) = ... // не заработает
|
||
>
|
||
> fun main() {
|
||
> println(NoEquals(1) == NoEquals(1)) // false — сравнение ссылок из Any
|
||
> }
|
||
> ```
|
||
> `equals` должен быть `override` в классе: он объявлен в `Any`, а расширение не переопределяет член. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
|
||
>
|
||
> **`compareTo` обязан быть согласован с `equals`.** Если `a.compareTo(b) == 0`, но `a != b`, то `TreeSet` и `sortedSetOf` начнут терять элементы - они используют сравнение, а не равенство.
|
||
>
|
||
> **`get` без `set` создаёт асимметричный API:**
|
||
> ```kotlin
|
||
> class ReadOnlyBox(private val items: List<Int>) {
|
||
> operator fun get(i: Int) = items[i]
|
||
> }
|
||
>
|
||
> fun main() {
|
||
> val b = ReadOnlyBox(listOf(1, 2))
|
||
> println(b[0]) // 1
|
||
> // b[0] = 5 // ошибка: нет set
|
||
> }
|
||
> ```
|
||
> Это нормально (так работает `List`), но пользователь увидит `[]` и может ожидать присваивания.
|
||
>
|
||
> **Оператор на расширении не виден, если есть член класса** - действует то же правило затенения, что у обычных расширений.
|
||
>
|
||
> **Операторы с неочевидным смыслом ухудшают код:**
|
||
> ```kotlin
|
||
> data class User13(val name: String, val roles: List<String>)
|
||
>
|
||
> // operator fun User13.plus(role: String) = copy(roles = roles + role)
|
||
> fun User13.withRole(role: String) = copy(roles = roles + role)
|
||
>
|
||
> fun main() {
|
||
> println(User13("Иван", emptyList()).withRole("админ"))
|
||
> // User13(name=Иван, roles=[админ])
|
||
> }
|
||
> ```
|
||
> `user + "админ"` короче, но непонятно: что значит «сложить пользователя со строкой»? Именованный метод честнее.
|
||
>
|
||
> **`invoke` на обычном классе запутывает:** `service()` не говорит, что делает объект. Оправдано для функциональных обёрток (`UseCase`, `Validator`), где у объекта ровно одна операция.
|
||
>
|
||
> **Нельзя придумать новый оператор.** Набор фиксирован: `+`, `-`, `*`, `/`, `%`, `[]`, `()`, `in`, `..`, сравнения, `++`/`--`, унарные. Произвольные символы вроде `<*>` или `|>` недоступны - только `infix`-функция со словесным именем.
|
||
|
||
Далее: [Receivers и context parameters](Receivers%20и%20context%20parameters.md)
|