Dev

Kotlin 연산자 Overloading

April 1, 2018

author:

Kotlin 연산자 Overloading

Kotlin에서 기본으로 제공하는 산술 연산자 plus, minus 등을 +, -로 접근한다. 이러한 기법을 Kotlin에서는 Convention이라고 한다.

이번 글에서는 이러한 Convention을 확장하여 사용할 수 있는 Kotlin의 기법을 살펴보려고 한다. 대부분 산술 연산자이며, List와 Map에 접근할 때 사용하는 []등에 대해서 살펴본다.

우선 아래의 표를 기준으로 Overloading이 가능한데 산술 연산자와 단항 산술 연산자이다.

Function code
plus a + b
minus a – b
div a / b
rem a % b
times a * b
not !a
unaryPlus +a
unaryMinus -a
inc ++a, a++
dec –a, a–

 

연산자 Overloading

연산자를 Overloading 할 수 있는 방법은 아주 간단하다.

위의 산술 연산자 표 정의 function 중에 operator 키워드 만 추가하면 확장이 가능한데 아래와 같다.

operator fun Int.plus(b: Int) = this + b

단순하게 위와 같이 확장하는 게 가능하지만 아래 이미지와 같이 custom function을 정의하는 경우 오류가 발생한다.

custom-operator-error

또한 plus을 확장하는 과정에서 원 함수에 있던 파라메터를 누락하는 경우에도 오류가 발생한다.

plus-operator-error

operator을 이용해서 아래와 같은 확장이 가능한데 Int와 Any을 받아 String으로 합쳐 리턴하는 것도 가능하다.

@Test
fun test() {
    operator fun Int.plus(b: Any): String {
        return "$this $b"
    }
    println(10 + "ABC")
}
// 10 ABC

또는 return 없이 Unit 형태로 사용하는것도 가능하다.

@Test
fun test() {
    operator fun Int.plus(b: Any) {
        println("$this $b")
    }
    10 + "ABC"
}
// 10 ABC

 

산술 연산자 확장하기

operator을 사용하여 산술 연산자 plus 확장하고, 이를 Convention 정의에 따라 + 접근할 수 있음을 확인하였다.

아래 Position data class에 plus 구현해보았다.

data class Position(val a: Int, val b: Int) {

    operator fun plus(item: Position): Position {
        return Position(a + item.a, b + item.b)
    }
}

class ExampleUnitTest {

    @Test
    fun test() {
        val positionOne = Position(1, 2)
        val positionTwo = Position(3, 4)
        println(positionOne + positionTwo)
    }
}

위와 같은 샘플인데 출력하면 Position(a=4, b=6)의 결과를 확인할 수 있다.

그 외 기본 산술 연산자도 위와 같이 확장이 가능하다.

 

단항 산술 연산자

이번엔 단항(++, --, +, -) 산술 연산자의 확장이 가능한데 아래와 같이 구현할 수 있다.

먼저 -unaryMinus을 아래와 같이 확장할 수 있다.

아래 샘플에서 1, 2를 마이너스로 변경하고, 하나는 -20, -10을 다시 마이너스로 변경하여 +로 변경하는 샘플이다.

class ExampleUnitTest {

    @Test
    fun test() {
        println(-Position(1, 2))
        println(-Position(-20, -10))
    }
}

data class Position(var a: Int, var b: Int) {

    operator fun unaryMinus(): Position {
        return Position(-a, -b)
    }
}

// Position(a=-1, b=-2)
// Position(a=20, b=10)

이번에는 ++(inc)을 이용하여 플러스하는 샘플인데, 이번엔 ++의 위치에 따라 더해지는 시점을 함께 확인할 수 있다.

operator의 코드를 보면 a.inc() 함수로 접근함을 확인할 수 있다.

class ExampleUnitTest {

    @Test
    fun test() {
        var position = Position(1, 2)
        println(position++)
        println(++position)
    }
}

data class Position(var a: Int, var b: Int) {

    operator fun inc(): Position {
        return Position(a.inc(), b.inc())
    }
}

위의 결과는 아래와 같이 확인 가능하다.

Position(a=1, b=2)
Position(a=3, b=4)

 

Collection get/set 확장하기

Collection의 list/map에서는 []을 통해 get/set을 접근할 수 있다.

class ExampleUnitTest {

    @Test
    fun test() {
        val list = mutableListOf("A", "B", "C")
        println(list)
        list[0] = "D"
        println(list)

        val map = mutableMapOf(0 to "A", 1 to "B", 2 to "C")
        println(map)
        map[0] = "D"
        println(map)
    }
}

이러한 []을 확장하여, Collection이 아닌 곳에서도 사용 가능하게 만들 수 있는데 set을 확장하여 아래와 같은 코드를 만들어 볼 수 있다.

그러면 [0]을 통해 값을 변경하고, get 대신 [0]의 값을 가져올 수 있다.

class ExampleUnitTest {

    @Test
    fun test() {
        val position = Position(10, 20)
        println(position)
        position[0] = 30
        println(position)
        println(position[1])
    }
}

data class Position(var a: Int, var b: Int) {

    operator fun set(position: Int, value: Int) {
        when (position) {
            0 -> a = value
            1 -> b = value
            else -> throw IndexOutOfBoundsException("Invalid coordinate $position")
        }
    }

    operator fun get(position: Int): Int = when (position) {
        0 -> a
        1 -> b
        else -> throw IndexOutOfBoundsException("Invalid coordinate $position")
    }
}

 

Destructuring Declarations(분할)

kotlin의 특징 중 하나인 Destructuring Declarations이다.

val position = Position(10, 20)으로 선언하고, println(position.a), println(position.b)로 접근하는 게 아닌 아래와 같은 접근이 가능하다.

val (a, b) = Position(10, 20)
println(a)
println(b)

a, b의 변수에 바로 접근할 수 있도록 만드는 것인데 kotlin 디컴파일을 통해 아래와 같은 코드를 확인해볼 수 있다.

@Test
public final void test() {
   Position var3 = new Position(10, 20);
   int a = var3.component1();
   int b = var3.component2();
   System.out.println(a);
   System.out.println(b);
}

이러한 접근 방법은 최대 componentN까지 제공한다.

map에서는 key/value을 한 번에 아래와 같이 받는 것도 가능하다.

val map = mutableMapOf(0 to "A", 1 to "B", 2 to "C")
for ((key, value) in map) {
    println("$key to $value")
}

forEach에서는?

추가로 forEach 문을 제공하는데, Java 1.8의 기법과 kotlin의 접근 방법 2가지가 있다.

Java 1.8 버전을 사용하는 경우에는 아래와 같이 forEach 문 작성이 가능하다.

val map = mutableMapOf(0 to "A", 1 to "B", 2 to "C")
map.forEach { t, u ->
  // ...
}

그리고 Java 1.7 이하의 경우 Kotlin에서 아래와 같이 접근해야 한다.

val map = mutableMapOf(0 to "A", 1 to "B", 2 to "C")
map.forEach { (key, value) ->
    // ...
}

split에서 사용하기

split에서 아래와 같이 사용하는 게 가능한데, split의 결과를 (fileName, extension)으로 정의하고, 데이터 클래스를 초기화한 리턴을 받을 수 있다.

class ExampleUnitTest {

    @Test
    fun test() {
        println("ABC.md".split())
    }
}

data class DataName(val fileName: String, val extension: String)

fun String.split(): DataName {
    val (fileName, extension) = split(".", limit = 2)
    return DataName(fileName, extension)
}

 

마무리

여기까지 kotlin에서 제공하는 Convention의 확장과 Destructuring Declarations(분할)을 살펴보았다.

Destructuring Declarations(분할)의 경우 이미 익숙하게 사용하고 있었지만, Convention의 확장은 처음 알아보았다.

아무래도 클래스끼리 합치는 부분에서 자주 사용할 수 있을 것 같다.