목차
Kotlin 연산자와 조건식
Kotlin의 연산자는 단순한 기호처럼 보이지만 일부는 해당 타입의 함수 호출로 해석된다. 예를 들어 a + b는 타입에 따라 a.plus(b)에 대응하고, item in collection은 collection.contains(item)에 대응한다. 따라서 사용자 정의 타입도 적절한 operator 함수를 제공하면 같은 문법을 쓸 수 있다. 이 글의 예제는 각 코드 블록을 독립적으로 실행한다고 가정한다.
산술과 대입
| 식 | 관련 함수 | 확인할 점 |
|---|---|---|
a + b, a - b | plus, minus | 입력 타입과 결과 타입 |
a * b, a / b | times, div | 정수 나눗셈의 소수 부분 손실 |
a % b | rem | 0으로 나눌 때의 오류 |
a += b | plusAssign 또는 plus 뒤 재대입 | 타입과 연산자 정의에 따라 해석 |
위 표의 a += b를 언제나 a.plusAssign(b)로만 설명하면 부정확하다. 타입이 plusAssign을 제공하면 이를 사용할 수 있고, 그렇지 않으면 a = a + b 형태가 사용될 수 있다. val에 대해 새 값을 대입하는 경로는 허용되지 않는다. 타입의 연산자 구현을 확인해야 한다.
val whole = 7 / 2
val fraction = 7.0 / 2
val remainder = 7 % 2
println("$whole / $fraction / $remainder") // 3 / 3.5 / 1
var total = 10
total += 5
println(total) // 15
정수끼리 나누면 정수 결과다. 소수 결과가 필요하면 피연산자 하나를 Double로 만든다. 분모가 0일 수 있는 외부 입력이라면 계산 전에 검증해야 한다. Int 계산은 큰 값에서 오버플로할 수 있으므로 단위와 최대 범위를 정한다.
단항·비트 연산자
var count = 3
println(-count) // -3: unaryMinus
println(count.inc()) // 4: 새 값을 계산하지만 count는 그대로
println(count) // 3
count++
println(count) // 4
val flags = 0b1010
println(flags shl 1) // 20
println(flags and 0b0010) // 2
println(flags.inv()) // 모든 비트를 반전한 음수
inc()를 호출하는 것과 count++는 다르다. 전자는 새 값을 돌려주기만 하고, 후자는 그 결과를 변수에 다시 저장한다. 비트 연산은 shl, shr, ushr, and, or, xor, inv 같은 함수 형태로 쓴다. inv()는 정해진 정수 비트 폭 전체를 뒤집으므로 단순히 “10의 0과 1만 반전”이라고 읽으면 안 된다. 원문 예제의 varbit1 = 10은 변수 선언이 아니었고, mod와 rem을 같은 현재 권장 표현으로 나란히 둔 표도 혼동을 줬다.
포함과 컬렉션 조건
val height = 46
println(height in 1..53) // true: 양끝 포함
println(height !in 1..53) // false
val readings = listOf(1, 2, null)
println(readings.any { it == null }) // true
println(readings.all { it != null }) // false
println(readings.none { it == null }) // false
in은 오른쪽 범위나 컬렉션의 contains로 판단한다. !in은 그 결과를 뒤집는다. any는 조건에 맞는 항목이 하나라도, all은 모두, none은 아무것도 없을 때 참이다. 빈 목록에서 any는 거짓, all과 none은 참이므로 입력이 비어 있는 경우를 허용할지 별도로 확인한다.
값 동등성과 참조 동등성
data class Point(val x: Int, val y: Int)
val first = Point(1, 2)
val sameValues = Point(1, 2)
val sameReference = first
println(first == sameValues) // true
println(first === sameValues) // false
println(first === sameReference) // true
==는 구조적 동등성으로, 값이 null일 수 있는 경우도 안전하게 처리한다. ===는 같은 객체 참조인지 비교한다. 데이터 클래스는 값 비교용 equals를 제공하므로 두 Point의 좌표가 같으면 ==가 참이다. 일반 클래스가 equals를 재정의하지 않았다면 ==도 기본적으로 참조 기준이 될 수 있다. 숫자 기본 타입의 박싱이나 플랫폼 차이까지 고려해야 하므로 ===로 숫자 값을 비교하는 것은 피한다.
when과 try도 값을 만든다
fun coreLabel(cores: Int): String = when (cores) {
1 -> "1 Core"
in 2..16 -> "$cores Cores"
else -> "범위를 벗어난 코어 수"
}
println(coreLabel(4)) // 4 Cores
fun parsePort(raw: String): Int? = try {
raw.toInt().takeIf { it in 1..65535 }
} catch (e: NumberFormatException) {
null
}
println(parsePort("8080")) // 8080
println(parsePort("abc")) // null
when을 식으로 사용하면 모든 가능한 경우에서 값을 만들어야 하므로 위처럼 else를 둔다. try도 식이라 성공 값 또는 catch의 값을 반환한다. 포트 예제에서 문자 파싱 실패는 null, 범위 밖 숫자도 null로 처리했다. 호출자가 두 실패 원인을 구분해야 한다면 Result나 구체적인 오류 타입을 선택한다. 예외를 모든 경우에 무조건 삼키는 것보다 무엇이 정상적인 잘못된 입력인지 정하는 것이 중요하다.