21 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Инфиксные и операторные функции | infix-нотация, перегрузка операторов, invoke, деструктуризация и делегирование свойств |
|
Kotlin позволяет задать для своих типов тот же синтаксис, что у встроенных: a + b, a[i], a(), a in b. Делается это обычными функциями с модификатором operator или infix.
infix
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 используется для операций, читающихся как фраза:
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
}
Приоритет инфиксных функций ниже арифметики, но выше сравнений - это источник ошибок:
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() |
Полный пример - вектор:
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
}
Умножение на скаляр справа - метод класса; чтобы работало и слева, нужно расширение:
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)
}
Операторы как расширения
Оператор можно добавить к чужому типу:
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 позволяет вызывать объект как функцию:
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 — то же самое
}
Практическое применение - объект с состоянием, который ведёт себя как функция:
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 даёт фабрику с синтаксисом конструктора:
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
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
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 получается автоматически
}
Итератор
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 заработает на чужом типе:
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
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
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 для значений.
Деструктуризация
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 автоматически. Подробнее: Деструктуризация
Делегирование свойств
Операторы getValue/setValue позволяют подменить реализацию свойства:
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): Делегирование
Когда перегружать оператор
| Ситуация | Стоит ли |
|---|---|
| математический тип (вектор, матрица, деньги, дата) | да |
коллекция-подобный тип ([], in, for) |
да |
| сравнение по естественному порядку | да, через Comparable |
| «красивее выглядит» | нет |
смысл оператора неочевиден (user + role) |
нет |
| операция может бросить исключение неожиданно | нет |
Критерий простой: оператор должен означать то же, что для встроенных типов. + обязан быть коммутативным по смыслу и не иметь побочных эффектов; == - соответствовать hashCode.
[!info] Отличия от Java
Java Kotlin перегрузки операторов нет operator funa.add(b)a + blist.get(i)list[i]map.put(k, v)map[k] = va.compareTo(b) < 0a < ba.equals(b)a == bBigDecimal.add().multiply()a + b * cинфиксной нотации нет infix funJava не поддерживает перегрузку операторов принципиально - кроме
+для строк, который встроен в компилятор. Это сознательное решение дизайнеров Java: операторы с произвольным смыслом усложняют чтение. Kotlin выбрал компромисс: перегрузка есть, но набор операторов фиксирован и имена функций заданы жёстко, так чтоplusвсегда означает+и нельзя придумать свой оператор.Разница заметнее всего на числовых типах.
BigDecimalв Java:BigDecimal result = a.multiply(b).add(c).divide(d, RoundingMode.HALF_UP);В Kotlin те же операторы уже объявлены в stdlib как расширения:
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, а не сравнение ссылок. Самое важное отличие на практике: в Javaa == bдля объектов сравнивает ссылки, и забытый.equals()- классическая ошибка со строками. В Kotlin==вызываетequals(с корректной обработкой null), а для ссылок есть отдельный===: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
invokeаналога не имеет. В Java объект нельзя вызвать как функцию; ближайшее - интерфейс с методомapply, вызываемый явно.Сравнить: Операторы и выражения в Java, Класс Object
[!warning] Подводные камни Приоритет
infixниже арифметики: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неявно: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меняет - и перепутать легко: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: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должны возвращать новый объект: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нельзя объявить расширением: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
compareToобязан быть согласован сequals. Еслиa.compareTo(b) == 0, ноa != b, тоTreeSetиsortedSetOfначнут терять элементы - они используют сравнение, а не равенство.
getбезsetсоздаёт асимметричный API: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), но пользователь увидит[]и может ожидать присваивания.Оператор на расширении не виден, если есть член класса - действует то же правило затенения, что у обычных расширений.
Операторы с неочевидным смыслом ухудшают код:
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