[Kotlin] 컬렉션과 범위에 대해 쓸 수 있는 convention(관례)
Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.
컬렉션에는 인덱스 연산자(`a[b]`) 를 사용항 인덱스로 원소를 설정하거나 가져올 수 있다.
`in` 연산자는 원소가 컬렉션이나 범위에 속하는지 검사하거나 원소를 iteration 할 때 사용한다.
커스텀 클래스에서 이러한 연산들을 추가할 수 있다.
인덱스로 원소에 접근: get & set
코틀린이나 자바에서 맵이나 배열 원소에 접근할 때 모두 `[]` 을 사용한다.
코틀린에서는 인덱스 연산자도 convention 을 따른다.
- 인덱스 연산자를 사용해 원소를 읽는 연산 -> `get` 연산자 메서드로 변환
- 인덱스 연산자를 사용해 원소를 쓰는 연산 -> `set` 연산자 메서드로 변환
`Point` 클래스에 점의 좌표를 읽을 때 인덱스 연산을 사용할 수 있다.
`Point` 클래스 `get` convention 구현하기
data class Point(var x: Int, var y: Int)
// get 연산자 함수를 정의
operator fun Point.get(index: Int): Int = when (index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
val p = Point(10, 20)
assert(p[1] == 20)
`get` 메서드를 만들고 `operator` 변경자를 붙이기만 하면 된다.
- `get` 메서드의 파라미터로 `Int` 가 아닌 타입도 사용할 수 있다. (맵 인덱스 연산에서 키 타입을 인덱스로 사용)
- 여러 파라미터를 사용하는 `get` 을 정의할 수도 있다.
- 예: 2차원 행렬이나 배열을 표현하는 클래스에 `operator fun get(rowIndex: Int, colIncex: Int)` 를 정의하면 `matrix[row, col]` 로 그 메서드를 호출할 수 있다.
인덱스에 해당하는 컬렉션 원소를 쓰고(write) 싶을 때는 `set` 이라는 이름의 함수를 정의하면 된다.
위에서 사용한 `Point` 는 immutable 클래스이므로 새 클래스를 정의해서 `set` 을 구현해보자.
`MutablePoint` 에 convention 을 따르는 `set` 구현
data class MutablePoint(var x: Int, var y: Int)
// set 연산자 함수 정의
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
assert(p == MutablePoint(10, 42))
이 예제도 간단하다.
in 관례
`in` 은 객체가 컬렉션에 들어있는지 검사(멤버십 검사: membership test)한다.
`in` 연산자와 대응하는 함수는 `contains` 이다.
`in` convention 구현하기: 어떤 점이 사각형 안에 들어가는지 판단
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean =
p.x in upperLeft.x until lowerRight.x && p.x in upperLeft.y until lowerRight.y
val rect = Rectangle(Point(10, 20), Point(50, 50))
assertTrue { Point(20, 30) in rect }
assertFalse { Point(5, 5) in rect }
`in` 의 우항에 있는 객체는 `contains` 메서드의 수신 객체(receiver)가 되고, `in` 의 좌항에 있는 객체는 `contains` 메서드에 인자로 전달된다.
`10..20` 이라는 식을 사용하면 닫힌 범위가 만들어져서 10 이상 20 이하인 범위가 생긴다
`10 until 20` 식을 사용하면 열린 범위가 만들어져서 10 이상 19 이하인 범위가 생긴다.
코틀린 1.9 부터는 `raungeUnitl` 이라는 operator 함수를 사용하는 것이 권고된다.
그래서 `until` 대신 `..<` 을 사용할 수 있다./** * Creates a range from this value up to but excluding the specified [other] value. * * If the [other] value is less than or equal to `this` value, then the returned range is empty. */ @SinceKotlin("1.9") @WasExperimental(ExperimentalStdlibApi::class) public operator fun rangeUntil(other: Int): IntRange
p.x in upperLeft.x..<lowerRight.x && p.x in upperLeft.y..<lowerRight.y
rangeTo 관례
범위를 만들 때 `..` 구문을 사용한다. (`1 .. 10` 은 1 부터 10까지 모든 수의 범위)
`..` 연산자는 `rangeTo` 함수를 간략하게 표현하는 방법이다.
- `rangeTo` 함수는 범위를 리턴한다.
- 아무 클래스에나 정의할 수 있다.
- `Comparable` 인터페이스를 구현한 클래스는 `rangeTo` 를 정의할 필요가 없다.
/**
* Creates a range from this [Comparable] value to the specified [that] value.
*
* This value needs to be smaller than or equal to [that] value, otherwise the returned range will be empty.
* @sample samples.ranges.Ranges.rangeFromComparable
*/
public operator fun <T : Comparable<T>> T.rangeTo(that: T): ClosedRange<T> = ComparableRange(this, that)
이 함수는 범위를 리턴하며 어떤 원소가 범위 안에 들어있는지 `in` 을 통해 검사할 수 있다.
날짜의 범위 다루기: `LocalDate` 클래스 사용
val now = LocalDate.now()
val vacation = now..now.plusDays(10)
assertTrue { now.plusWeeks(1) in vacation }
`now..nowPlusDays(10)` 이라는 식은 컴파일러에 의해 `now.rangeTo(now.plusDays(10))` 으로 변환된다.
- `rangeTo` 함수는 `LocalDate` 의 멤버가 아닌 `Comparable` 에 대한 확장 함수이다.
- `rangeTo` 함수는 다른 산술 연산자보다 우선순위가 낮다.
혼동을 피하기 위해 괄호로 인자를 감싸주자.- `0..n.forEach { }` 식은 컴파일되지 않는다.
`(0..n).forEach { }` 처럼 범위를 괄호로 둘러싸자.
- `0..n.forEach { }` 식은 컴파일되지 않는다.
for 루프를 위한 iterator 관례
`for (x in list) { ... }` 처럼 코틀린의 for 루프는 `in` 연산자를 사용한다.
하지만 `in` 의 의미는 범위 검사와 다르다.
위와 같은 문장은 `list.iterator()` 를 호출해서 `iterator` 를 얻은 다음, 자바와 마찬가지로 그 `iterator` 에 대해 `hasNext` 와 `next` 호출을 반복하는 식으로 변환된다.
`Iterator` 인터페이스
/**
* An iterator over a collection or another entity that can be represented as a sequence of elements.
* Allows to sequentially access the elements.
*/
public interface Iterator<out T> {
/**
* Returns the next element in the iteration.
*/
public operator fun next(): T
/**
* Returns `true` if the iteration has more elements.
*/
public operator fun hasNext(): Boolean
}
코틀린에서는 이 또한 관례이므로 iterator 메서드를 확장 함수로 정의할 수 있다.
그래서 일반 자바 문자열에 대한 for 루프가 가능하다.
코틀린 stlib 의 `CharSequence` 에 대한 `iterator` 확장함수
public operator fun CharSequence.iterator(): CharIterator = object : CharIterator() {
private var index = 0
public override fun nextChar(): Char = get(index++)
public override fun hasNext(): Boolean = index < length
}
클래스 안에 직접 iterator 메서드를 구현할 수도 있다.
날짜 범위에 대한 `iterator` 구현하기
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> { // 이 객체는 LocalDate 원소에 대한 iterator 를 구현
var current = start // start 는 ClosedRange 의 프로퍼티임
override fun hasNext(): Boolean = current <= endInclusive // compareTo 관례를 사용하여 날짜를 비교
override fun next(): LocalDate = current.apply { // 현재 날짜를 저장한 후 날짜를 변경. 그 후 저장해둔 날짜를 리턴
current = plusDays(1) // 현재 날짜를 1일 뒤로 변경
}
}
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) { println(dayOff) }
// print
/*
2016-12-31
2017-01-01
*/
hasNext 와 next 호출의 반복으로 이루어진다.
`hasNext` 와 `next` 호출 과정
처음 `current` 는 `2016-12-31`
`hasNext` 호출 -> `2016-12-31 <= 2017-01-01` 이므로 `true`
`next` 호출 -> `2016-12-31` 리턴. `current` 가 `2017-01-01` 이 됨.
`hasNext` 호출 -> `2017-01-01 <= 2017-01-01` 이므로 `true`
`next` 호출 -> `2017-01-01` 리턴. `current` 가 `2017-01-02` 이 됨.
`hasNext` 호출 -> `2017-01-02 > 2017-01-01` 이므로 `false`