Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,504 @@
---
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)