-
11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 11. 24. 22:45
11장
11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)
11-1-2. 도메인 특화 언어 : 연산자 오버로딩(6~8)
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)와 같이 일반 대입문과 이항 연산을 활용한 방식으로 해석이 불가능
다음글
반응형'Study(종료) > Kotlin 22.09.13 ~ 12.18' 카테고리의 다른 글
11-2. 도메인 특화 언어 : 위임 프로퍼티 (0) 2022.12.01 11-1-2. 도메인 특화 언어 : 연산자 오버로딩(5~8) (0) 2022.11.25 10-2. 어노테이션과 리플렉션 : 리플렉션 (0) 2022.11.19 10-1. 어노테이션과 리플렉션 : 어노테이션 (0) 2022.11.17 9-3. 제네릭스 : 타입 별명(Type Alias) (0) 2022.11.13