본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로프로그래밍 › Kotlin › 기초

2. 연산자

목차

Kotlin 연산자와 조건식

Kotlin의 연산자는 단순한 기호처럼 보이지만 일부는 해당 타입의 함수 호출로 해석된다. 예를 들어 a + b는 타입에 따라 a.plus(b)에 대응하고, item in collection은 collection.contains(item)에 대응한다. 따라서 사용자 정의 타입도 적절한 operator 함수를 제공하면 같은 문법을 쓸 수 있다. 이 글의 예제는 각 코드 블록을 독립적으로 실행한다고 가정한다.

산술과 대입

식관련 함수확인할 점
a + b, a - bplus, minus입력 타입과 결과 타입
a * b, a / btimes, div정수 나눗셈의 소수 부분 손실
a % brem0으로 나눌 때의 오류
a += bplusAssign 또는 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나 구체적인 오류 타입을 선택한다. 예외를 모든 경우에 무조건 삼키는 것보다 무엇이 정상적인 잘못된 입력인지 정하는 것이 중요하다.

참고: Kotlin Operator Overloading, Equality, Control flow

같은 카테고리의 글