서른세 번째, 연산자 오버로딩입니다.
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 연산자 함수는 위임 프로퍼티에 설명돼 있습니다.
이름있는 함수의 중위 호출
중위 함수 호출을 사용하여 맞춤화된 중위 연산을 흉내낼 수 있습니다.
'Kotlin' 카테고리의 다른 글
공식 문서로 배우는 코틀린 - 35. Using builders with builder type inference (0) | 2024.03.15 |
---|---|
공식 문서로 배우는 코틀린 - 34. Type-safe builders (2) | 2024.03.15 |
공식 문서로 배우는 코틀린 - 32. Inline functions (0) | 2024.03.14 |
공식 문서로 배우는 코틀린 - 31. Higher-order functions and lambdas (0) | 2024.03.13 |
공식 문서로 배우는 코틀린 - 30. Functions (0) | 2024.03.13 |