Files
obsidian/Kotlin/Функциональный Kotlin/Инфиксные и операторные функции.md
T

505 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)