Kotlin

[Kotlin] 산술 연산자 오버로딩

sh1mj1 2024. 1. 23. 17:48

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

코틀린에서는 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 convention(관례)라고 한다.

언어 기능을 타입에 의존하는 자바와 달리 코틀린은 함수 이름을 통한 convention 에 의존한다.

 

자바에서는 원시 타입, String 에 대해서만 산술 연산자를 사용할 수 있다.

코틀린에서는 다른 클래스에서도 산술 연산자를 사용할 수 있다.

이항 산술 연산 오버로딩

plus 연산자 구현하기

kotlin
닫기
data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point { // plus 라는 이름의 연산자 함수를 정의
        return Point(x + other.x, y + other.y) // 좌표를 성분 별로 더한 새로운 점을 리턴
    }
}

val p1 = Point(10, 20)
val p2 = Point(30, 40)
assert(p1 + p2 == Point(40, 60))

 plus 함수 앞에 operator 키워드를 붙여야 한다.

operator 가 없는데 실수로 convention 에서 사용하는 함수의 이름을 쓰고 우연히 그 이름에 해당하는 기능을 연산자로 사용한다면, 'operator' modifier is required on 'plus' in .....' 와 같은 컴파일 에러가 표시된다.

 

연산자를 확장 함수로 정의

kotlin
닫기
operator fun Point.plus(other: Point): Point = Point(x + other.x, y + other.y)

 이 구현도 이전 구현과 같다.

외부 라이브러리의 클래스에 대한 연산자를 정의할 때는 convention을 따르는 이름의 확장함수로 구현하는 게 일반적인 패턴이다.

  • 코틀린에서는 오버로딩한 연산자를 정의, 사용하기 쉽다.
  • 직접 연산자를 만들어 사용하지는 못한다. (예를 들어 ¡™£ 라는 이름의 연산자를 만들지는 못함.)
    • 미리 정해둔 연산자만 오버로딩 가능하다.
    • 언어에서 미리 정의해야 하는 이름이 연산자별로 정해져 있음.
함수 이름
a * b times
a / b div
a % b mod(1.1 부터는 rem)
a + b plus
a - b minus

직접 정의한 함수를 통해 구현하더라도 연산자 우선 순위는 언제나 표준 숫자 타입에 대한 연산자 우선순위와 같다.

*,/,% 가 모두 우선순위가 같고 그 다음 우선순위가 +,- 이다.

연산자를 정의할 때 두 피연산자가 같은 타입이 아니어도 된다.

두 피연산자의 타입이 다른 연산자 정의하기

kotlin
닫기
operator fun Point.times(scale: Double): Point = Point((x * scale).toInt(), (y * scale).toInt())

val p = Point(10, 20)
assert(p * 1.5 == Point(15, 30))

 코틀린 연산자가 자동으로 교환 법칙을 지원하지 않는다.

p * 1.5 가 아닌, 1.5 * p 로 쓰고 싶다면, Double 이 수신 객체인 연산자 함수를 더 정의해야 한다.

연산자 함수의 리턴 타입이 꼭 두 피연산자 중 하나와 일치하지 않아도 된다.

결과 타입이 피연산자 타입과 다른 연산자 정의

kotlin
닫기
operator fun Char.times(count: Int): String = toString().repeat(count)

assert('a' * 3 == "aaa")

 Char 을 좌항, Int 를 우항으로 받아서 String 을 리턴한다.

operator 함수도 오버로딩할 수 있다.

즉, 이름은 같지만 파라미터 타입이 서로 다른 연산자 함수도 여럿 만들 수 있다.

비트 연산자에 대해 특별한 연산자 함수를 사용하지 않는다.

코틀린에서는 표준 숫자 타입에 대해 비트 연산자를 정의하지 않는다.

물론, 커스텀 타입에도 비트 연산자를 정의할 수도 없다.

대신에 중위 연산자 표기법을 지원하는 일반 함수를 통해 비트 연산을 수행한다.

 

코틀린에서 비트 연산을 수행하는 함수 목록

  • shl - 왼쪽 시프트 (자바 <<) 0x1 shl 416 ( 이진수 10000 됨.)
  • shr - 오른쪽 시프트 (자바 >>)
  • ushr - 오른쪽 시프트. 0으로 사인(부호) 비트 설정. 자바 >>>
  • and - 비트 곱 (자바 &)
  • or - 비트 합(자바 |)
  • xor - 비트 배타 합(자바 ^)
  • inv - 비트 반전(자바 ~)

복합 대입 연산자 오버로딩

+=,-= 등의 연산자는 복합 대입(compound assignment) 연산자라고 부른다.

kotlin
닫기
var point = Point(1, 2)
point += Point(3, 4) // point = point + Point(3, 4) 와 같다
assert(point == Point(4, 6))

 경우에 따라 += 연산이 객체에 대한 참조를 다른 참조로 바꾸기보다, 원래 객체의 내부 상태를 변경하게 만들고 싶을 때가 있다.

mutable 컬렉션에 원소를 추가하는 경우가 대표적인 예이다.

kotlin
닫기
val numbers = ArrayList<Int>()
numbers += 42
assert(numbers[0] == 42)

 

리턴 타입이 UnitplusAssign 함수를 정의하면 코틀린은 += 연산자에 그 함수를 사용한다.

다른 복합 대입 연산자도 비슷하게 minusAssign, timesAssign 의 이름이다.

kotlin stlib 이 정의한 mutable 컬렉션에 대한 plusAssign

kotlin
닫기
@kotlin.internal.InlineOnly
public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
    this.add(element)
}

 

 위에서 사용한 += 도 이를 사용한 것이다.

생각해보면, +=plusplusAssign 양쪽으로 컴파일 할 수 있다.

만약 두 함수의 타입이 같다면, 무엇으로 컴파일할지 알 수 없어 오류가 발생한다.

  • 일반 연산자를 사용하여 이를 해결
kotlin
닫기
data class Point(var x: Int, var y: Int) 

operator fun Point.plus(i: Int): Point = Point(x + i, y + i)

operator fun Point.plusAssign(i: Int) {
    x += i + i
    y += i + i
}

// plus 의 일반 연산자를 사용하여 ambiguity 피하기
var p = Point(10, 20)
p = p.plus(1)
assert(p == Point(11,21))

// plusAssign 의 일반 연산자를 사용하여 ambiguity 피하기
val p = Point(10, 20)
p.plusAssign(1)
assert(p == Point(12,22))
  • varval 로 바꾸어서 plus 적용이 불가능하게 하기
kotlin
닫기
var p = Point(10, 20) // p 가 var 임. 변경 가능이어서 plus 와 plusAssign 둘다 호출 가능해짐.. ambiguity 발생.
p += 1 // [COMPILE ERROR] Assignment operators ambiguity

///////
////////
// p 가 val 임.. 변경 불가. 그래서 오직 plusAssign 만 호출 가능해짐.
val p = Point(10, 20)
p += 1
assert(p == Point(12, 22))

 하지만 일반적으로 새로운 클래스를 일관성 있게 설계하는 게 가장 좋다.

plusplusAssign 연산을 동시에 정의하지 말자.

 

kotlin stlib 는 컬렉션에 대해 두 접근 방법을 제공한다.

  • +-  는 항상 새로운 컬렉션을 리턴
  • +=-=
    • mutable 컬렉션에 작용해 메모리에 있는 객체 상태를 변화시킴.
    • read-only 컬렉션에서는 변경을 적용한 복사본을 리턴한다
kotlin
닫기
val list = arrayListOf(1, 2)
list += 3 // 리스트를 변경
assert(list == arrayListOf(1, 2, 3))

val newList = list + listOf(4, 5) // 두 리스트의 모든 원소를 포함하는 새 리스트 리턴
assert(newList == listOf(1, 2, 3, 4, 5))

단항 연산자 오버로딩

이전과 똑같이 미리 정해진 이름의 함수를 선언하면서 operator 로 표시하면된다.

단항 연산자 정의하기

kotlin
닫기
operator fun Point.unaryMinus(): Point = Point(-x, -y)

val p = Point(10, 20)
assert(-p == Point(-10, -20))

 

오버로딩할 수 있는 단항 산술 연산자

  • +a             unaryPlus
  • -a :                unaryMinus
  • !a.:                 not
  • ++a, a++inc  
  • --a, a--  dec
    • incdec 함수를 정의하여 증가/감소 연산자를 오버로딩하는 경우, 컴파일러는 일반적인 값에 대한 전위와 후위 증가/감소 연산자와 같은 의미를 제공한다.

증가 연산자 정의하기

kotlin
닫기
operator fun BigDecimal.inc() = this + BigDecimal.ONE

var bd = BigDecimal.ZERO
assert(bd++ == BigDecimal.ZERO) // 후위 증가 연산자는 assert 가 실행된 다음에 값을 증가
assert(++bd == BigDecimal.TWO) // 전위 증가 연산자는  assert 가 실행되기 전에 값을 증가