연산자 오버로드는 의미에 맞게 사용해야 합니다.
코틀린은 객체의 연산자 기능을 함수로 오버로드할 수 있습니다. 이 기능은 아주 강력한 기능이지만 잘못 사용할 경우 가독성 문제뿐만 아니라 큰 위험으로 다가올수도 있습니다.
팩토리얼 함수를 예를 들어 봅시다. 수학에서는 팩토리얼 함수를 A!로 표현합니다. 그렇지만 대부분의 코드에서는 저런 팩토리얼 같은 기능을 연산자로 재공하고 있지는 않습니다.
fun Int.factorial(): Int = (1..this).product()
fun Iterable<Int>.product(): Int =
fold(1) { acc, i -> acc * i }
대부분 이런 식으로 로직을 작성해야 합니다.
print(10 * 6.facorial()) // 10 * (6!) = 7200
// 연산자 오버로딩
opertaor fun Int.not() = factorial()
print(10 * !6)
그런데 한 개발자가! 기호를 사용하면 더욱 가독성 있는 코드를 짤 수 있을 거라 생각하여 위와 같이 코드를 작성하였습니다. 그렇지만! 같은 경우 not이라는 기본 사용처가 있습니다. not이므로 논리연사에 사용해야 하면 위와 같이 팩토리얼 연산에 사용하면 안 됩니다. 이렇게 작성하면 혼란스럽고 오해의 소지가 있습니다.
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a..b | a.rangeTo(b) |
a..<b | a.rangeUntil(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b) |
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
코틀린에서 연산자에 대응되는 함수 이름들입니다. 개발자가 연산자 오버로딩을 사용할 때에는 항상 신중해야 합니다.
분명하지 않은 경우
관례를 충족하는지 어긋나는지 확실하지 않은 경우가 문제입니다. 예를 들어 함수를 곱한다는 것 (* 연산자)의 의미를 두 가지로 해석할 수 있습니다. 새로운 함수(() → Unit)를 만들어 낸다든가 아니면 같은 함수를 n번 호출한다는 뜻으로 말이죠.
opertaor fun Int.times(operation: ()->Unit) : ()->Unit = {
repeat(this) { operation() }
}
val tripledHello = 3 * { print("Hello") }
tripleHello() // 출력 : HelloHelloHello
새로운 함수(() → Unit)를 만들어 낼 때
opertaor fun Int.times(operation: ()->Unit) : Unit {
repeat(this) { operation() }
}
3 * { print("Hello") } // 출력 : HelloHelloHello
같은 함수를 n번 호출할 때
infix fun Int.timesRepeated(operation: ()->Unit) = {
repeat(this) { operation() }
}
val tripledHello = 3 timesRepeated { print("Hello") } // 2항 연산자 처럼 사용
tripleHello() // 출력 : HelloHelloHello
의미가 명확하지 않다면, infix를 활용한 확장 함수를 사용하는 것이 좋습니다.
repeat(3) { print("Hello") } // 출력: HelloHelloHello
또는 톱레벨 함수를 사용하는 것도 좋습니다
.
도메인 특화언어를 쓰는 경우 이러한 사항을 무시해도 되겠지만 안드로이드 개발 같은 경우 위와 같은 규칙을 지켜야 합니다. 결과적으로 연산자 오버로딩은 그 이름의 의미에 맞게 사용해야 합니다. 의미가 명확하지 않더라도 연산자 오버로딩을 사용하지 않는 것이 좋습니다. 대신 이름이 있는 일반 함수를 사용하는 것이 좋습니다. 연사자처럼 함수를 사용하고 싶다면 infix함수 또는 톱레벨 함수를 사용하는 것이 좋습니다.
'코틀린 > Effective Kotlin' 카테고리의 다른 글
[Effective Kotlin] 11. 가독성 - 가독성을 목표로 설계하라 (0) | 2023.08.09 |
---|---|
[Effective Kotlin] 안정성파트를 끝내며 (0) | 2023.08.03 |
[Effective Kotlin] 10. 안정성 - 단위 테스트(Unit Test)를 만들어라 (0) | 2023.08.02 |
[Effective Kotlin] 9. 안정성 - use를 사용하여 리소스를 닫아라 (0) | 2023.08.01 |
[Effective Kotlin] 8. 안정성 - 적절하게 null을 처리하라 (0) | 2023.07.28 |