본문 바로가기

Dev Book/Kotlin IN ACTION

CH7(7.1). 산술 연산자 오버로딩

어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 코틀린에서는 관례라고 부른다. 언어 기능을 타입에 의존하는 자바와 달리 코틀린은 관례에 의존한다.

1. 이항 산술 연산 오버로딩

data class Point(val x:Int, val y:Int){
    operator fun plus(other:Point):Point{
        return Point(x+other.x, y+other.y)
    }
}

>>>val p1 = Point(10, 20)
>>>val p2 = Point(30, 40)
>>>println(p1 + p2)
Point(x=40, y=60)

 

연산자를 오버로딩하는 함수 앞에는 꼭 operator가 있어야 한다. operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 할 수 있다. operator 변경자를 추가해 plus 함수를 선언하고 나면 + 기호로 두 Point 객체를 더할 수 있다.

 

operator fun Point.minus(other:Point):Point{
    return Point(x-other.x, y-other.y)
}

 

연산자를 멤버 함수 대신 확장 함수로 정의할 수도 있다.

 

- 코틀린에서는 프로그래머가 직접 연산자를 만들어 사용할 수 없고 언어에서 미리 정해둔 연산자만 오버로딩할 수 있으며 관례에 따르기 위해 클래스에서 정의해야 하는 이름이 연산자별로 정해져 있다. (연산자 우선순위는항상 표준 숫자 타입에 대한 연산자 우선순위와 같다.)

 

함수 이름
a * b times
a / b div
a % b mod
a + b plus
a - b minus

 

operator fun Point.times(scale:Double):Point{
    return Point((x*scale).toInt(), (y*scale).toInt())
}

>>>val p = Point(10, 20)
>>>println(p*1.5) //p.times(1.5)로 컴파일
Point(x=15, y=30)

 

연산자를 정의할 때 두 피연산자가 같은 타입일 필요는 없다. 다만, 코틀린 연산자가 자동으로 교환 법칙을 지원하지는 않기 때문에 p * 1.5가 아닌 1.5 * p를 쓰고 싶다면 operator fun Double.times(p:Point):Point를 따로 정의해야 한다.

 

operator fun Char.times(count:Int):String{
    return toString().repeat(count)
}

>>>println('a'*3)
aaa

 

연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다. 이 연산자는 Char을 좌항, Int를 우항으로 받아서 String을 반환한다.

2. 복합 대입 연산자 오버로딩

- plus와 같은 연산자를 오버로딩하면 코틀린은 + 연산자뿐 아니라 그와 관련 있는 연산자인 +=도 자동으로 함께 지원한다. +=, -= 등의 연산자는 복합 대입 연산자라 불린다.

 

>>>var point = Point(1, 2)
>>>point += Point(3,4)
>>>println(point)
Point(x=4, y=6)

 

operator fun <T> MutableCollection<T>.plusAssign(element:T){
    this.add(element)
}

 

반환 타입이 Unit인 plusAssign 함수를 정의하면 코틀린은 += 연산자에 그 함수를 사용한다. 다른 복합 대입 연산자 함수도 비슷하게 minusAssign, timesAssign 등의 이름을 사용한다. 코틀린 표준 라이브러리는 변경 가능한 컬렉션에 대해 위와 같이 plusAssign을 정의한다.

 

>>>val numbers = ArrayList<Int>()
>>>numbers += 42
>>>println(numbers[0])
42

 

다음의 예제에 +=는 위의 정의된 plusAssign을 사용한다.

 

- 어떤 클래스가 plus와 plusAssign을 모두 정의하고 둘 다 +=에 사용 가능한 경우 컴파일러는 오류를 보고한다. 일반 연산자를 사용하거나, var을 val로 바꿔서 plusAssign 적용을 불가능하게 해 해결할 수 있다. 하지만 두 연산을 동시에 정의하지 않는 것이 가장 좋다. 클래스가 변경 불가능하다면 plus와 같이 새로운 값을 반환하는 연산만을 추가하고, 빌더와 같이 변경 가능한 클래스라면 plusAssign과 같은 연산만을 제공하여 일관성 있게 설계하도록 한다.

 

- 코틀린 표준 라이브러리는 컬렉션에 대해 두가지 접근 방법을 함께 제공한다. +와 -는 항상 새로운 컬렉션을 반환하고, +=, -= 연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화시킨다. -=, +=가 읽기 전용 컬렉션에 적용될 경우 변경을 적용한 복사본을 반환한다.

 

val list = arrayListOf(1,2)
list += 3 //+=는 list를 변경한다.
val newList = list + listOf(4,5) //+는 두 리스트의 모든 원소를 포함하는 새로운 리스트를 반환한다.
>>>println(list)
[1,2,3]
>>>println(newList)
[1,2,3,4,5]

3. 단항 연산자 오버로딩

- 단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 취하지 않는다. 

 

operator fun Point.unaryMinus():Point{
    return Point(-x, -y)
}

 

 

함수 이름
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec

 

operator fun BigDecimal.inc() = this + BigDecimal.ONE

>>>var bd = BigDecimal.ZERO
>>>println(bd++) //println이 실행된 다음 값을 증가시킴
>>>println(++bd) //값을 증가시키고 println 실행