오늘도 더 나은 코드를 작성하였습니까?

연산자 오버로딩과 기타 관례(산술 연산자 오버로딩) 본문

Kotlin in Action/코틀린 답게 사용하기

연산자 오버로딩과 기타 관례(산술 연산자 오버로딩)

hik14 2020. 8. 22. 17:28

코틀린은 어떤 언어 기능이 정해진 사용자 작성 함수와 연결되는 경우가 몇 가지 있다.

코틀린에서는 이런 언어 기능이 어떤 타입(class)과 연관되는 것이 아니라 특정 함수 이름과 연관된다.

 

예) 특정 클래스 안에 plus라는 이름의 메서드를 정의하면 그 클래스의 인스턴스에 대해 + 연산자를 사용할 수 있다.

언어의 기능( + 연산자 사용) 미리 정해진 이름의 함수(plus)를 연결해주는 기법을 코틀린에서는 관례(Convention)이라 한다.

 

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

이런 관례를 채택한 이유는 기존 자바 클래스를 코틀린 언어에 적용하기 위해서다.

 

기존의 자바 클래스가 구현하고 인터페이스는 이미 고정돼 있고 코틀린 쪽에서 자바 클래스가 새로운 인터페이스를 구현하게 할 수는 없다. 

즉 코틀린에서 새로만든 인터페이스를 자바쪽에서 구현된 클래스에 적용하기 힘들기다.

 

하지만 확장 함수를 정의하면서 기존에 클래스에 새로운 메서드를 추가하는 것은 가능하다 따라서 기존 자바 클래스에 대해서 확장 함수를 구현하되 그 이름을 관례에 따라 붙여주면 기존 자바 코드를 바꾸지 않아도 새로운 기능을 쉽게 부여한다. 

 

산술 연산자 오버 로딩.

자바에서는 원시 타입에 대해서만 산술 연산자를 허용하고 추가적으로 String클래스에 대해 +연산자를 사용할 수 있다.

 

이항 산술 연산 오버 로딩

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

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

}

연산자를 오버 로딩하는 메서드는 반드시 operator 키워드를 반드시 붙여야 한다. 이 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 하고 operator 키워드  없이 관례에서 사용하는 함수 이름을 사용하여 오버 로딩한다면 에러가 난다.

 

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

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

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

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

 

함수이름
a * b times
a / b div
a % b   mod, rem(ver 1.1이상)
a + b plus
a - b minus

 직접 구현한 함수를 사용할 때도 연산자 우선순위는 언제나 같다. * / % >>> + -

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

피연산자가 같을 필요는 없다 point * 1.5  위의 함수는 처럼 사용할 수 있다. 

*코틀린은 자동으로 교환 법칙을 지원하지 않는다. 1.5 * point 은 정의되지 않는다.

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

operator fun Double.times(other: Point)
        = Point( (this*other.x).toInt(), (this*other.y).toInt())

반환 타입 역시 꼭 두 피연산자의 타입과 일치할 필요가 없다.

// Int를 받아 String 리턴
operator fun Char.times(count: Int)
    = toString().repeat(count)

fun main() {
    println('a'*10)
}

일반 함수와 동일하게 operator 함수도 오버 로딩이 된다. 동일한 이름에 시그니처를 변경하여 여러 개의 함수를 정의할 수이다.

 

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

 

복합 대입 연산자 오버 로딩

 

연산자 오버 로딩을 하게 되면 일반적으로 += , -=등의 복합 대입 연산자는 자동으로 지원이 된다.

단 += 의 경우가 참조를 변경하게 되면 변수는 var 이여야 한다. 

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

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

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

operator fun Double.times(other: Point)
        = Point( (this*other.x).toInt(), (this*other.y).toInt())

fun main() {
    var p1 = Point(10,20)
    val p2 = Point(20,30)

    p1 += p2

    println(p1)
}

만약 참조를 변경하지 않고 객체의 내부 상태를 변경하기를 원한다면,

반환 타입이 Unit인 plusAssign 함수를 정의하면 코틀린은 += 연산자에 plusAssign를 사용한다.

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


fun main() {
    
    val list = mutableListOf(1,2,3,4,5)
    list += 10

    println(list)
}

 

하지만 plus와 plusAssign을 동시에 정의하고 +=를 사용하면 컴파일러는 에러를 발생한다. 

클래스를 일관성 있게 정의하기 위해서는 plus와 plusAssign을 동시에 정의하지 마라

 

Point처럼 참조를 변경해도 상관없고 객체 내부 데이터를 변경 불가능하다면 plus

빌더처럼 참조를 변경하면 안 되고 객체 내부 데이터를 변경해야 된다면 PlusAssign을 사용한다.

 

 

*코틀린 표준 라이브러리는 컬렉션에 있어 두 가지 접근방법을 제공한다.

 

1. + -  항상 새로운 컬렉션을 반환한다. 

 

2. += -= 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화시킨다.

읽기 전용이라면 += -=는  변경을 적용한 복사본을 제공한다.

fun main() {
    val list = arrayListOf(1,2)
    list += 3  // 기존의 객체의 내부 변경.

    val newList= list+ listOf(4,5) // 새로운 컬렉션의 생성하여 할당. 

    println(list)
    println(newList)
}

단항 연산자 오버 로딩

 

단항 연산자를 오버 로딩하는 방법 역시 이항 연산자 오버 로딩과 다를 건 없다.

미리 정해진 이름을 가지고 operator 키워드를 사용하면 된다.

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

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

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

operator fun Point.unaryMinus()
        = Point(-x, -y)
        
fun main() {
    val p1 = Point(10,20)
    println(-p1)
}
함수 이름
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a--  dec