연산자 오버로딩

연산자 오버로딩 (Operator overloading)

Kotlin은 타입에 대해 미리 정의된 연산자 집합에 사용자 정의 구현을 제공할 수 있게 해 줘요. 이 연산자들은 +* 같은 미리 정의된 기호 표현과 우선순위를 가져요. 연산자를 구현하려면 해당 타입에 대해 특정 이름을 가진 멤버 함수 또는 확장 함수를 제공해요. 이 타입이 이항 연산에서는 왼쪽 피연산자 타입이 되고, 단항 연산에서는 인자 타입이 되죠.

연산자를 오버로드하려면 해당 함수에 operator 수정자를 붙여요.

interface IndexedContainer {
    operator fun get(index: Int)
}

연산자 오버로드를 오버라이딩할 때는 operator를 생략할 수 있어요.

class OrdersList: IndexedContainer {
    override fun get(index: Int) { /*...*/ }   
}

출처: Kotlin 공식 문서

본문

단항 연산

단항 접두 연산자

표현식 번역됨
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

이 표는 컴파일러가 예를 들어 +a라는 표현식을 처리할 때 다음 단계를 거친다는 뜻이에요.

  • a의 타입을 결정한다. 그 타입을 T라고 하자.
  • 리시버 T에 대해 operator 수정자가 있고 매개변수가 없는 unaryPlus() 함수를 찾는다. 즉 멤버 함수나 확장 함수를 찾는다.
  • 함수가 없거나 모호하면 컴파일 오류가 된다.
  • 함수가 존재하고 반환 타입이 R이면, +a 표현식은 타입 R을 가진다.

이 연산들은 다른 모든 연산들과 마찬가지로 기본 타입에 최적화되어 있어, 그들에 대해 함수 호출의 오버헤드를 도입하지 않아요.

예를 들어 단항 마이너스 연산자를 이렇게 오버로드할 수 있어요.

data class Point(val x: Int, val y: Int)

operator fun Point.unaryMinus() = Point(-x, -y)

val point = Point(10, 20)

fun main() {
   println(-point)  // prints "Point(x=-10, y=-20)"
}

증가와 감소

표현식 번역됨
a++ a.inc() + 아래 참고
a-- a.dec() + 아래 참고

inc()dec() 함수는 값을 반환해야 하는데, 그 값이 ++-- 연산이 사용된 변수에 할당돼요. 이 함수들은 incdec가 호출된 객체를 변경하면 안 돼요.

컴파일러는 a++ 같은 후위 형태의 연산자를 해석하기 위해 다음 단계를 거쳐요.

  • a의 타입을 결정한다. 그 타입을 T라고 하자.
  • 타입 T의 리시버에 적용할 수 있는, operator 수정자가 있고 매개변수가 없는 inc() 함수를 찾는다.
  • 함수의 반환 타입이 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)
a..<b a.rangeUntil(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)

대괄호는 적절한 수의 인자를 가진 getset 호출로 번역돼요.

invoke 연산자

표현식 번역됨
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 호출로 번역돼요.

복합 할당 (Augmented assignments)

표현식 번역됨
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

a += b 같은 할당 연산에 대해 컴파일러는 다음 단계를 거쳐요.

  • 오른쪽 열의 함수가 있으면:
    • 대응하는 이항 함수(즉 plusAssign()에 대한 plus())도 있고, a가 변경 가능한 변수이며, plus의 반환 타입이 a의 타입의 하위 타입이라면 오류를 보고한다(모호성).
    • 반환 타입이 Unit인지 확인하고, 그렇지 않으면 오류를 보고한다.
    • a.plusAssign(b)에 대한 코드를 생성한다.
  • 그렇지 않으면 a = a + b에 대한 코드를 생성하려 한다(여기에는 a + b의 타입이 a의 하위 타입이어야 한다는 타입 검사가 포함됨).

할당은 Kotlin에서 표현식이 아니에요.

동등성과 부등성 연산자

표현식 번역됨
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

이 연산자들은 equals(other: Any?): Boolean 함수로만 동작하는데, 이 함수를 오버라이드해 사용자 정의 동등성 검사 구현을 제공할 수 있어요. 같은 이름의 다른 함수(예: equals(other: Foo))는 무시돼요.

Kotlin은 == 표현식에서 두 피연산자가 null과 직접 비교되지 않고, 그 비교가 두 부동소수점 타입 사이가 아닐 때 .equals()를 호출해요. 그 외에는 null 직접 비교에 ===를 사용하고, non-null 부동소수점 값을 숫자 값으로 비교해요.

===!==(항등 검사)는 오버로드할 수 없으므로, 그들에 대한 관례는 존재하지 않아요.

비교 연산자

표현식 번역됨
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

모든 비교는 compareTo 호출로 번역되는데, compareToInt를 반환해야 해요.

프로퍼티 위임 연산자

provideDelegate, getValue, setValue 연산자 함수는 위임된 프로퍼티에서 설명해요.

이름 있는 함수의 중위 호출

중위 함수 호출을 사용해 사용자 정의 중위 연산을 흉내 낼 수 있어요.

더 알아보기 (Learn more)