본문 바로가기

Kotlin

공식 문서로 배우는 코틀린 - 33. Operator overloading

서른세 번째, 연산자 오버로딩입니다.

 

Kotlin은 타입들에 대해 미리 정의된 연산자를 맞춤화(custom)하는 것을 허용합니다. 이러한 연산자는 미리 정의된 기호(예, +, *)와 우선 순위를 가지고 있습니다. 연산자를 구현하기 위해서는 해당 타입에 맞는 특별한 이름을 갖는 멤버 함수나 확장 함수를 만들어야 합니다. 이 타입은 이항 연산의 경우에는 왼쪽 부분의 타입이 되며 단항 연산에서는 인수 타입이 됩니다. 즉, 이항 연산의 오버로딩의 경우에는 앞쪽 타입에 적용할 수 있는 구현이어야 하고, 단항 연산에서는 연산자의 인수(예를 들어, -a의 경우 a가 인수) 타입에 적용할 수 있는 구현이어야 합니다.

 

연산자를 오버로딩하기 위해서는 해당 함수에 operator 수정자를 추가합니다.

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

 

오버로딩한 것을 오버라이딩할 때는 operator 수정자를 생략할 수 있습니다. 

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


단항 연산

단항 접두어 연산자

표현 변환 형태
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

 

이 표는 컴파일러가 예를 들어 +a를 처리할 때, 다음과 같은 단계를 수행한다는 것을 얘기해 줍니다.

  • a의 타입을 결정합니다. 이를 T라고 하겠습니다.
  • 수신자 T를 위한 매개변수가 없고, operator 수정자가 붙은 unaryPlus() 함수를 찾습니다. 즉, 이 조건에 맞는 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() 함수는 반드시 값을 반환해야 하는데, 이 값은 ++나 --가 연산에 사용된 변수에 할당될 값입니다. 이 두 함수는 inc나 dec가 호출된 객체를 변경해서는 안됩니다. 예를 들어, a++로 인해 a.inc()로 호출되는 경우 a를 변경해서는 안 됩니다.

 

컴파일러는 예를 들어 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)
    }
}

fun main() {
    val foo = Counter(1)
    val bar = foo + 10 
    println(bar)
}

/* 결과
Counter(dayIndex=11)
*/

 

in 연산자

표현식  변환 형태
a in b b.contains(a)
a !in b !b.contains(a)

 

in 과 !in에 대한 절차도 똑같습니다. 다만, 인수의 순서가 반대입니다.

 

인덱스 접근 연산자 (indexed access operator)

표현식 변환 형태
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)

 

대괄호는 적절한 수의 인수를 갖는 get과 set 호출로 변환됩니다.

 

호출(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 적절한 수의 인수를 갖는 invoke 호출로 변환됩니다.

 

call vs invoke

함수를 호출한다라고 할 때, 영어로는 call을 쓰기도 하고 invoke를 쓰기도 합니다. 둘은 대부분 구분 없이 혼용해서 사용합니다.
JavaScript 강의 문서 같은 곳에서는 JavaScript에서 함수를 직접 호출할 수 도 있지만, new를 붙여 호출하거나 call()이나 apply()를 통해서 호출할 수도 있기 때문에, call과 invoke를 구분해서 쓰기도 합니다.
Kotlin 공식 문서에서도 두 단어를 혼용해서 쓰고 있습니다. 그리고, 이 호출 연산자 부분에서는 호출 연산자에 대응하는 함수가 invoke()이므로 당연히 invoke라는 단어를 쓰고 있습니다.

 

복합 할당(Augumented 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.plubAssign(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?): Boolen 함수하고 동작하는데, 이 함수는 맞춤화된 동등성 검사 구현으로 오버라이딩 될 수 있습니다. 이름만 같은 어떠한 다른 형태의 함수도 이 연산자와 관련해서는 호출되지 않습니다(예: equals(other: Foo).

 

※ 동일성 검사 연산자인 ===와 !==는 오버라이딩이 불가하여 현재 이를 위한 연산자 오버로딩 방법은 없습니다.

 

null과 관련된 == 연산은 특별합니다. null == null 은 항상 true이며, 널이 아닌 x에 대한 x == null은 x.equals() 호출 없이 항상 false입니다.

 

비교 연산자

표현식 변환 형태
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 호출로 변환됩니다. 이 함수는 Int를 반환해야 합니다.

 

프로퍼티 위임 연산

provideDelegate, getValue, setValue 연산자 함수는 위임 프로퍼티에 설명돼 있습니다.

 

이름있는 함수의 중위 호출

중위 함수 호출을 사용하여 맞춤화된 중위 연산을 흉내낼 수 있습니다.