KotlinInAction/연산자 오버로딩과 기타 관례

산술 연산자 오버로딩

webmaster 2022. 8. 8. 01:57
728x90

어떤 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 코틀린에서는 관례라 부르고, 코틀린은 이 관례에 의존적이다.

이항 산술 연산 오버 로딩

data class Point(val x: Int, val y: Int){
    operator fun plus(other: Point) : Point{ //plus라는 연산자 함수를 정의한다
        return Point(x + other.x, y + other.y) //좌표를 성분별로 더한 새로운 점을 반환한다.
    }
}
fun main(args: Array<String>) {
    val p1 = Point( 10, 20)
    val p2 = Point( 30, 40)
    println(p1 + p2) //+로 계산하면 plus 함수가 호출된다
}
  • plus 함수 앞에 operator 키워드를 붙여야 한다.
    • 연산자를 오버 로딩하는 함수 앞에는 꼭 operator가 있어야 한다.
    • operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 할 수 있다
  • operator가 없는데 실수로 관례에서 사용하는 함수 이름을 쓰고 그 이름에 해당하는 기능을 사용하면 "operator modifier is required... "  오류가 발생한다
  • operator로 plus를 오버 로딩하면 내부적으로 p1 + p2 => p1.plus(p2)가 호출된다
operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}
  • 외부 함수의 클래스에 대한 연산자를 정의할 때는 관례를 따르는 이름의 확장 함수로 구현하는 게 일반적인 패턴이다.
  • 프로젝트 안에서 직접 작성한 클래스에 대해 관례를 따르는 확장 함수를 만들어도 잘 작동한다.

코틀린에서는 프로그래머가 직접 연산자를 만들어 사용할 수는 없지만, 언어에서 미리 정해둔 연산자만 오버 로딩할 수 있으며, 관례에 따르기 위해 클래스에서 정의해야 하는 이름이 연산자 별로 정해져 있다.

함수 이름
a * b times
a / b div
a % b mod(1.1 부터 rem)
a + b plus
a - b minus
  • 직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 언제나 표준 숫자 타입에 대한 연산자 우선순위와 같다
operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}
fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p * 1.5)
}
  • 연산자를 정의할 때 두 피연산자가 같은 타입일 필요는 없다
  • 코틀린 연산자가 자동으로 교환 법칙을 지원하지는 않는다는 사실에 유의하자
    • p * 1.5 외 1.5 * p라고도 쓸 수 있어야 한다면 operator fun Double.time(p: Point): Point와 같이 대응되는 함수를 따로 작성해야 한다
fun main(args: Array<String>) {
    println('a' * 3)
}

operator fun Char.times(count: Int):String{
    return toString().repeat(count)
}
  • 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야 하는 것도 아니다
  • 이 연산자는 char를 좌항으로, int를 우항으로 받아서 String을 돌려준다.
  • 일반 함수와 마찬가지로 operator 함수로 오버 로딩 할 수 있다
    • 이름은 같지만 파라미터 타입이 서로 다른 연산자 함수를 여럿 만들 수 있다.

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

코틀린은 표준 숫자 타입에 대해 비트 연산자를 정의하지 않는다. 따라서 커스텀 타입에서 비트 연산자를 정의할 수도 없다. 대신에 중위 연산자 표기법을 지원하는 일반 함수를 사용해 비트 연산을 수행한다.

복합 대입 연산자 오버로딩

plus와 같은 연산자를 오버 로딩하면 코틀린은 +연산자뿐 아니라 그와 관련된 연산자인 +=도 자동으로 함께 지원한다.

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

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}
fun main(args: Array<String>) {
    var point = Point(1, 2)
    point += Point(3, 4)
    println(point)
}
  • point += Point(3, 4)는 point = point + Point(3, 4)와 같고, 물론 변수가 변경 가능한 경우에만 복합 대입 연산자를 사용할 수 있다
fun main(args: Array<String>) {
    val numbers = ArrayList<Int>()
    numbers += 42
    println(numbers)
}
  • 경우에 따라 복합 대입 연산이 객체에 대한 참조를 다른 참조로 변경하는 것이 아닌 원래 객체의 내부 상태를 변경하고 싶은 때가 있는데, 대표적으로 컬렉션에 원소를 추가하는 연산이다
operator fun <T> MutableCollection<T>.plusAssign(element: T){
    this.add(element)
}
  • 반환 타입이 Unit인 plusAssign 함수를 정의하면 코틀린은 += 연산자에 그 함수를 사용한다.
  • minusAssign, timesAssign과 같이 다른 복합 대입 연산자도 비슷하다
  • 이론적으로는 +=를 plus와 plusAssign 양쪽으로 컴파일할 수 있지만, 실제로 어떤 클래스가 이 두 함수를 모두 정의하고 둘 다 +=에 사용 가능한 경우 컴파일러는 오류를 보고한다.
    • 일반 연산자를 사용해서 해결하거나, var를 val로 변경해 plusAssign을 적용이 불가능하게 해서 해결할 수 있다.
    • 하지만 일반적으로 새로운 클래스를 일관성 있게 설계하는 것이 좋으므로 plus와 plusAssign 연산을 동시에 정의하지 말자
    • 클래스가 변경이 불가능하다면 새로운 값만 반환하는 연산만을, 빌더와 같이 변경 가능한 클래스를 설계한다면 복합 대입 연산자를 작성하는 것을 고려하자
fun main(args: Array<String>) {
    val list = arrayListOf(1, 2)
    list += 3 //+=는 list 를 변경한다
    val newList = list + listOf(4, 5) //+는 두 리스트의 모든 원소를 포함하는 새로운 리스트를 반환한다
    println(list)
    println(newList)
}
  • 코틀린 표준 라이브러리는 컬렉션에 대해  +,- 에는 항상 새로운 컬렉션을 반환하며, +=,-= 과 같은 연산자에는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화시킨다.
  • 또한 읽기 전용 컬렉션에서 +=, -= 는 변경을 적용한 복사본을 반환한다.
  • 이런 연산자의 피연산자로는 개별 원소를 사용하거나 원소 타입이 일치하는 다른 컬렉션을 사용할 수 있다

단항 연산자 오버 로딩

이항 연산자를 오버 로딩하는 것과 마찬가지로 단항 연산자의 오버 로딩도 미리 정해진 이름의 함수를 선언하면서 operator로 표시하면 된다

data class Point(val x: Int, val y: Int) {
}
operator fun Point.unaryMinus():Point{ //단항 minus함수는 파라미터가 없다
    return Point(-x, -y) //좌표에서 각 성분의 음수를 취한 새점을 반환
}
fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(-p)
}
  • 단항 연산자를 오버 로딩하기 위해 사용하는 함수는 인자를 취하지 않는다
함수 이름
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec
fun main(args: Array<String>) {
    var bd = BigDecimal.ZERO
    println(bd++) //후위 증가 연산자는 println이 실행된 후 증가
    println(++bd) //전위 증간 연산자는 println이 실행되기 전에 값을 증가
}
operator fun BigDecimal.inc() = this + BigDecimal.ONE
  • inc/dec 함수를 정의해 증가/감소 연산자를 오버 로딩하는 경우 컴파일러는 일반적인 값에 대한 전위와 후위 증가/감소 연산자와 같은 의미를 제공한다.
  • 후위 ++ 연산은 먼저 현재의 값을 반환한 후 값을 증가시키고 전위 ++ 연산은 그 반대 순서로 작동한다.

 

728x90