본문 바로가기
Kotlin

[Kotlin in Action] 7.3 컬렉션과 범위에 대해 쓸 수 있는 관례

by Nhahan 2025. 3. 27.

7.3.1 인덱스로 원소에 접근: get과 set

mutableMap[key] = newValue

코틀린에서는 인덱스 연산자도 관례를 따른다. 인덱스 연산자를 사용해 원소를 읽는 연산은 get 연산자 메소드로 변환되고, 원소를 쓰는 연산은 set 연산자 메소드로 변환된다. Map과 MutableMap 인터페이스는 그 두 메소드가 이미 들어있다.

 

// get 관례 구현하기
operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}
>>> val p = Point(10, 20)
>>> println(p[1])
20

get이라는 메소드를 만들고 operator 변경자를 붙이면, 점의 좌표를 읽을 때 인덱스 연산을 사용할 수 있다. p[0]은 X 좌표를 의미하고, p[1]은 Y 좌표를 의미하게 된다.

 

인덱스에 해당하는 컬렉션 원소를 쓰고 싶을 때는 set이라는 이름의 함수를 정의하면 된다. Point 클래스는 불변 클래스이므로 set이 의미가 없다. 대신 변경 가능한 점을 표현하는 다른 클래스를 만들어서 예제로 사용하자.

data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else ->
         throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}
>>> val p = MutablePoint(10, 20)
>>> p[1] = 42
>>> println(p)
MutablePoint(x=10, y=42)

 

7.3.2 in 관례

컬렉션이 지원하는 다른 연산자로는 in이 있다. in은 객체가 컬렉션에 들어있는지 검사한다. 그런 경우 in 연산자와 대응하는 함수는 contains다.

어떤 점이 사각형 영역에 들어가는지 판단할 때 in 연산자를 사용하게 구현해보자.

// in 관례 구현하기
data class Rectable(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Pont): Boolean {
    return p.x in upperLeft.x until lowerRight.x && // 범위를 만들고 "x" 좌표가 그 범위 안에 있는지 검사
        p.y in upperLeft.y until lowerRight.y // "until" 함수를 사용해 열린 범위를 만듦 (열린 범위란 끝 값을 포함하지 않는 범위)
}
>>> val rect = Rectangle(Point(10, 20), Point(50, 50))
>>> println(Point(20, 30) in rect)
true
>>> println(Point(5, 5) in rect)
false

in의 우항에 있는 객체는 contains 메소드의 수신 객체가 되고, in의 좌항에 있는 객체는 contains 메소드에 인자로 전달된다.

 

7.3.3 rangeTo 관례

범위를 만드려면 .. 구문을 사용해야 한다. 예를 들어, 1..10은 1부터 10까지 모든 수가 들어있는 범위를 가리킨다. .. 연산자는 rangeTo 함수를 간략하게 표현하는 방법이다.

rangeTo 함수는 범위를 반환한다. 이 연산자를 아무 클래스에나 정의할 수 있다. 하지만 어떤 클래스가 Comparable 인터페이스를 구현하면 rangeTo를 정의할 필요가 없다. 코틀린 표준 라이브러리를 통해 비교 가능한 원소로 이뤄진 범위를 쉽게 만들 수 있다. 코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

이 함수는 범위를 반환하며, 어떤 원소가 그 범위 안에 들어있는지 in을 통해 검사할 수 있다.

 

예를 들어, LocalDate 클래스를 사용해 날짜의 범위를 만들어보자.

// 날짜의 범위 다루기
>>> val now = LocalDate.now()
>>> val vacation = now..now.plusDays(10)
>>> println(now.plusWeeks(1) in vacation)
true

now..now.plusDays(1)이라는 식은 컴파일러에 의해 now.rangeTo(now.plusDays(10))으로 변환된다. rangeTo 함수는 LocalDate의 멤버는 아니며, 앞에서 본대로 Comparable에 대한 확장 함수다.

rangeTo 연산자는 다른 산술 연산자보다 우선순위가 낮다. 하지만 혼동을 피하기 위해 괄호로 인자를 감싸주면 더 좋다.

>>> val n = 9
>>> println(0..(n + 1))
0..10

또한 0..n.forEach {}와 같은 식은 컴파일할 수 없음에 유의하자. 범위 연산자는 우선순위가 낮아서 범위의 메소드를 호출하려면 범위를 괄호로 둘러싸야 한다.

>>> (0..n).forEach { print(it) }
123456789

 

7.3.4 for 루프를 위한 iterator 관례

코틀린의 for 루프는 범위 검사와 똑같이 in 연산자를 사용한다. 하지만 이 경우 in의 의미는 다르다. for (x in list) { .. }와 같은 문장은 list.iterator()를 호출해서 이터레이터를 얻은 다음, 자바와 마찬가지로 그 이터레이터에 대해 hasNext와 next 호출을 반복하는 식으로 변환된다.

하지만 코틀린에서는 이 또한 관례이므로 iterator 메소드를 확장 함수로 정의할 수 있다. 이런 성질로 인해 일반 자바 문자열에 대한 for 루프가 가능하다. 코틀린 표준 라이브러리는 String의 상위 클래스인 CharSequence에 대한 iterator 확장 함수를 제공한다.

operatror fun CharSequence.iterator(): CharIterator // 이 라이브러리 함수는 문자열을 이터레이션할 수 있게 해준다.
>>> for (c in "abc") {}

클래스 안에서 직접 iterator 메소드를 구현할 수도 있다. 예를 들어 날짜에 대해 이터레이션하는 다음 코드를 살펴보자.

// 날짜 범위에 대한 이터레이터 구현하기
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
    object : Iterator<LocalDate> { // 이 객체는 LocalDate 원소에 대한 Iterator를 구현한다.
        var current = start
        override fun hasNext() = current <= endInclusive // compareTo 관례를 사용해 날짜를 비교한다.
        override fun next() = current.apply { // 현재 날짜를 저장한 다음에 날짜를 변경한다. 그 후 저장해둔 날짜를 반환한다(apply).
            current = plustDays(1) // 현재 날짜를 1일 뒤로 변경한다.
        }
    }
}
>>> val newYear = LocalDate.ofYearDay(2017, 1)
>>> val dayOff = newYear.minusDays(1)..newYear
>>> for (dayOff in daysOff) { println(dayOff } // daysOff에 대응하는 iterator 함수가 있으면 daysOff에 대해 이터레이션한다.
2016-12-31
2017-01-01

여기서 범위 타입에 대한 iterator 메소드를 어떻게 정의하는지 살펴보라. 앞 절에서 살펴본 rangeTo 라이브러리 함수는 ClosedRnage의 인스턴스를 반환한다. 코드에서 ClosedRange<LocalDate>에 대한 확장 함수 iterator를 정의했기 때문에 LocalDate의 범위 객체를 for 루프에 사용할 수 있다.

 

댓글