Study(종료)/Kotlin 22.09.13 ~ 12.18

11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)

Ski_ 2022. 11. 24. 22:45

11장

11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)

11-1-2.  도메인 특화 언어 : 연산자 오버로딩(6~8)

11-2.  도메인 특화 언어 : 위임 프로퍼티

11-3. 도메인 특화 언어 : 고차 함수와 DSL


 

11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)

 

연산자 오버로딩은 + - * / 등 코틀린 내장 연산자에 대해

새로운 의미를 부여할 수 있게 해주는 언어 기능이다.

당장 떠오르는건 같은 + 연산자를 사용하더라도,

Number 타입의 경우 덧셈 연산을 하지만 문자 타입의 경우 문자를 합치는 연산을 한다.

이게 가능한 이유가 +가 오버로딩돼 있어 다양한 구현을 제공하기 때문이라고 한다.

이러한 기능들을 사용하기 위한 연산자 오버로딩에 대해 좀 더 자세히 알아보자.

 

1) 단항 연산

오버로딩이 가능한 단항 연산자로는 전위 + - ! 연산자가 있다.

컴파일러는 이러한 연산자를 함수 호출로 표현하는 방법을 제공한다.

의미
+e e.unaryPlus()
-e e.unaryMinus()
!e e.not()

이런 함수를 인자 식의 타입에 대해 멤버 함수나 확장 함수로 정의할 수 있다.

 

이를 이용해 not() 컨벤션을  보색 관계를 ! 연산자를 표현할 수 있다.

enum class Color{
    Black, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, WHITE;

    operator fun not() = when (this){
        Black -> WHITE
        RED -> CYAN
        GREEN -> MAGENTA
        BLUE -> YELLOW
        YELLOW -> BLUE
        CYAN -> RED
        MAGENTA -> GREEN
        WHITE -> Black
    }
}
fun main() {
    val black = Color.Black
    println(!black) // WHITE
    val red = Color.RED
    println(!red) // CYAN
}

연산자 함수를 확장 함수로 정의하면 임의의 타입에 대한 연산자를 오버로딩하는것도 가능하다

operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = {!this(it)}
fun isShort(s: String) = s.length <= 4
fun String.isUpperCase() = all { it.isUpperCase() }
fun main() {
    val data = listOf("abc", "ABCDE", "ababc", "BdAc", "xyz")
    println(data.count(::isShort)) // 3
    // abc, ababc, xyz
    println(data.count(!::isShort)) // 2
    // ABCDE, BdAc
    println(data.count(String:: isUpperCase)) // 1
    // ABCDE
    println(data.count(!String:: isUpperCase)) //4
    // abc, ababc, BdAc, xyz
}

2) 증가와 감소

증가(++)와 감소(--)연산자도 피연산자 타입에 대한 파라미터가 없는 inc(), dec() 함수로 오버로딩할 수 있다.

이 함수의 반환 타입은 증가 아후 값과 증가 이전 값이 같은 타입이어야 한다.

enum class RainbowColor{
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
    operator fun inc() = values()[(ordinal + 1) % values().size]
    operator fun dec() = values()[(ordinal + values().size - 1) % values().size]
}

fun main() {
    var color = RainbowColor.INDIGO
    println(color++) // INDIGO
    println(color) // VIOLET
}

3) 이항 연산

이상 연산자 역시 오버로딩할 수 있다. 마찬가지로 정해진 이름의 연산자 함수를 정의하면 된다.

의미
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a .. b a.rangeTo(b)
a in b b.contains(a)
a !in b !b.contains(a)
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0

% 연산자의 이름은 mod() 연산자였지만 rem() 연산자로 대체되었고, mod()는 사용 불가능하다.

개인적으로 비교 연산을 할 때 함수가 compareTo 연산 이후 0과 비교하는게 신기했다.

 

이항 연산자중 동등성 관련 연산도 있다. ==나 !=를 사용하면 컴파일러는 equals() 함수를 호출한다.

여기서 equals() 구현이 Any 클래스에 정의된 기반 구현을 상속하므로

명시적인 operator 변경자를 붙이지 않아도 된다.

또한 같은 이유로 equals()를 항상 멤버 함수로 정의해야 한다.

 

코틀린에서 && 나 || 연산자는 오버로딩할 수 없다.

이들은 Boolean값에 대해서만 지원되는 내장 연산이다.

참조 동등성 연산자인 ===나 !==도 마찬가지로 오버로딩할 수 없는 내장 연산이다.


4) 중위 연산

중위 연산(infix function)에 대해서 앞장에서 공부한 적이 있다.

중위 연산은 변수 두 개 사이에 정의된 infix 변경자를 이용한 연산을 하는것으로 기억한다.

val pair1 = 1 to 2 // 중위 호출
val pair2 = 1.to(2) // 일반적인 호출

표준 to 함수 구현은 아래와 같다.

infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

이를 이용해 논리곱, 논리합을 표현하는 중위 연산을 정의할 수 있다.

앞에서 작성했던 함수를 가져와서 비교해보자면,

operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = {!this(it)}
fun isShort(s: String) = s.length <= 4
fun String.isUpperCase() = all { it.isUpperCase() }
fun main() {
    val data = listOf("abc", "ABCDE", "ababc", "BdAc", "xyz")
    println(data.count(::isShort)) // 3
    // abc, ababc, xyz
    println(data.count(!::isShort)) // 2
    // ABCDE, BdAc
    println(data.count(String:: isUpperCase)) // 1
    // ABCDE
    println(data.count(!String:: isUpperCase)) //4
    // abc, ababc, BdAc, xyz
}

위 함수를 중위 연산 정의를 통해 좀 더 간결하게 만들 수 있다.

infix fun <T> ((T) -> Boolean).and(
        other: (T) -> Boolean
): (T) -> Boolean{
    return { this(it) && other(it) }
}

infix fun <T> ((T) -> Boolean).or(
        other: (T) -> Boolean
): (T) -> Boolean{
    return { this(it) || other(it) }
}

operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = {!this(it)}
fun String.isUpperCase() = all { it.isUpperCase() }
fun isShort(s: String) = s.length <= 4
fun main() {
    val data = listOf("abc", "ABCDE", "ababc", "BdAc", "xyz")
    println(data.count(::isShort and String::isUpperCase)) // infix fun .and 사용
    // 0
    println(data.count(::isShort or String::isUpperCase)) // infix fun .or 사용
    // 4 >> abc, ABCDE, BdAc, xyz
    println(data.count(!::isShort and String::isUpperCase)) // infix fun .and 사용
    // 1 >> ABCDE
    println(data.count(!(::isShort and String::isUpperCase))) // infix fun .or 사용
    // 5 >> abc, ABCDE, ababc, BdAc, xyz
}

5) 대입

다른 이항 연산으로 +=와 같은 복합 대입 연산이 있다.

가변 컬렉션과 불변 컬렉션에 따라 복합 대입 연산의 동작이 달라진다.

+=를 불변 컬렉션 타입의 변수에 적용하면 새로운 컬렉션 객체가 생기고

이 객체를 변수에 대입해 변수 값이 바뀐다. 그래서 이 경우 변수가 가변이어야 한다.

var numbers = listOf(1, 2, 3)
numbers += 4
println(numbers) // [1, 2, 3, 4]

말이 어려워서 혼자 이것저것 해봤더니, 불변 컬렉션(immutable collection)의 경우(위 코드에서 list)

+=을 var 타입에 변수에 대입하면, 컬렉션은 불변 컬렉션이므로 

기존 값 + 추가한 값을 가진 새로운 컬렉션을 만든 뒤 numbers에 대입한다.

(즉 numbers가 가리키고 있는 변수의 주소가 바뀐다)

이 때 numbers는 val 타입의 변수인 경우 변경이 불가능하므로, var 타입의 변수여야만 한다.

val numbers1 = listOf(1, 2, 3)
numbers1 += 4 // ERROR : Val cannot be reassigned

반대로 가변 컬렉션의 경우 += 연산을 컬렉션에 적용하면, 컬렉션의 내용은 바뀌지만

객체 자체의 정체성(가리키고 있는 주소)는 바뀌지 않는다.

var numbers1 = listOf(1, 2, 3)
println(numbers1) // ref : @875
numbers1 += 4
println(numbers1) // ref : @890
// numbers1이 가리키는 주소가 바뀌는것을 확인

val numbers2 = mutableListOf(1, 2, 4)
println(numbers2) // ref : @ 893
numbers2 += 5
println(numbers2) // ref : @ 893
// numbers2이 가리키는 주소가 동일한 것을 확인

임의의 타입에 대해 위 두 가지 관습(convention)을 적용할 수 있다.

+= 같은 연산자를 복합 대입 연산자(augmented assignment operator)라고 부르며

정해진 관습에 따라 연산자에 대응하는 함수표를 작성해야 한다.

의미 의미
a += b a = a.plus(b) a.plusAssign(b)
a -= b a = a.minus(b) a.minusAssign(b)
a *= b a = a.times(b) a.timesAssign(b)
a /= b a = a.div(b) a.divAssign(b)
a %= b a = a.rem(b) a.remAssign(b)

복합 대입 연산자를 해석하는 방법은 다음과 같다.

 

  ● 커스텀 복합 대입 함수가 있다면 그 함수를 사용

    ○ 복합 대입 함수의 존재 여부에 따라 복합 대입문을 대응하는 복합 함수로 변환해 컴파일

    ○ (+=의 경우 plusAssign()이 있는지, -=의 경우 minusAssign()이 있는지 등)

 

  ● 복합 대입 연산자 함수가 없는 경우(plusAssign() 등) 복합 대입문을

      이행 연산자 + 대입을 사용한 연산으로 해석

    ○ a + b의 경우 plus()가 있으면 a = a.plus(b)로 해석

 

  ● 복합 대입 연산자의 왼쪽 피연산자가 불변인 경우 변수에 새 값을 대입할 수 없으므로

      a = a.plus(b)와 같이 일반 대입문과 이항 연산을 활용한 방식으로 해석이 불가능


다음글

11-1-2.  도메인 특화 언어 : 연산자 오버로딩(6~8)

반응형