자바에는 표준 라이브러리와 밀접하게 연관된 언어 기능이 몇 가지 있습니다. 예를 들어 for ... in 루프에 java.lang.Iterable을 구현한 객체를 사용할 수 있고, 자원을 사용하는 try문(try-with-resource statement)에 java.lang.AutoCloseable을 구현한 객체를 사용할 수 있습니다.
이와 비슷하게 코틀린에서도 어떤 언어 기능이 정해진 사용자 작성 함수와 연결되는 경우가 몇 가지 있는데, 코틀린에서는 이런 언어 기능이 어떤 타입(클래스)과 연관되기보다는 특정 함수 이름과 연관됩니다. 예를 들어 어떤 클래스안에 plus라는 이름의 메소드를 정의하면 그 클래스 인스턴스에 대해 + 연산자를 사용할 수 있습니다. 이런 식으로 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 코틀린에서 관례(covention)이라 부릅니다.
언어 기능을 타입에 의존하는 자바와 달리 코틀린은 (함수 이름을 통한
) 관례에 의존합니다. 이런 관례를 채택한 이유는 기존 자바 클래스를 코틀린 언어에 적용하기 위함입니다. 기존 자바 클래스가 구현하는 인터페이스는 이미 고정되있어서 코틀린 쪽에서 자바 클래스가 새로운 인터페이스를 구현하게 만들 수 없습니다. 반면 확장 함수를 사용하면 기존 클래스에 새로운 메소드를 추가할 수 있어서, 기존 자바 클래스에 대해 확장 함수를 구현하면서 관례에 따라 이름을 붙이면 기존 자바 코드를 바꾸지 않아도 새로운 기능을 쉽게 부여할 수 있습니다.
코틀린에서 관례를 사용하는 가장 단순한 예는 산술 연산자입니다. 자바에서는 원시 타입에만 산술 연산자를 사용할 수 있고, 추가로 String에 대해 + 연산자를 사용할 수 있습니다. 그러나 다른 클래스에서도 산술 연산자가 유용할 수 있습니다. 예를 들어 BigInteger 클래스에 add 메소드를 명시적으로 호출하기 보다 + 연산자를 사용하는 편이 더 낫고, 컬렉션에 원소를 추가하더라도 += 연산자를 사용하면 편리합니다. 코틀린에서는 이러한 일이 가능합니다.
아래의 예제는 Point라는 클래스를 정의하고 두 점을 더하는 연산을 추가하는 코드입니다. 이 연산은 두 점의 X좌표와 Y 좌표를 각각 더하는데, + 연산자를 구현하여 만듭니다.
data class Point(val x: Int, val y: Int) {
// 연산 오버로딩
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
// 연산자를 확장 함수로 정의
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
fun main() {
val p1 = Point(10, 20)
val p2 = Point(30, 40)
// +로 계산하면 plus 함수가 호출
println(p1 + p2)
}
plus 함수 앞에 operator 키워드가 있는데, 연산자를 오버로딩 하는 함수 앞에는 꼭 operator가 있어야 합니다. operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 할 수 있습니다. operator가 없는데 실수로 관례에서 사용하는 함수 이름을 쓰고 우연히 그 이름에 해당하는 기능을 사용하면 오류가 발생해 이름이 겹쳤다는 사실을 파악할 수 있습니다.
그리고 연산자를 멤버 함수로 만드는 대신 확장 함수로 정의할 수도 있습니다. 외부 함수의 클래스에 대한 연산자를 정의할 때는 관례를 따르는 이름의 확장 함수로 구현하는게 일반적인 패턴입니다.
다른 언어와 비교해 코틀린에서 오버로딩한 연산자를 정의하고 사용하기 더 쉬운데, 코틀린에서는 프로그래머가 직접 연산자를 만들어 사용할 수 없고 언어에서 미리 정해둔 연산자만 오버로딩할 수 있으며, 관례에 따르기 위해 클래스에서 정의해야 하는 이름이 연산자별로 정해져 있습니다. 아래는 이항 연산자와 그에 상응하는 연산자 함수 이름을 보여줍니다.
식 | 함수 이름 |
---|---|
a * b | times |
a / b | div |
a % b | mod(1.1부터 rem) |
a + b | plus |
a - b | minus |
직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 언제나 표준 숫자 타입에 대한 연산자 우선순위와 같습니다.
코틀린 연산자를 자바에서 호출하기 쉽습니다. 모든 오버로딩한 연산자는 함수로 정의되며, 긴 이름을 사용하면 일반 함수로 호출할 수 있습니다. 만약 자바 클래스에 원하는 연산자 기능을 제공하는 메소드가 이미 있지만, 이름이 다르다면 관례에 맞는 이름을 가진 확장 함수를 작성하고 연산을 기존 자바 메소드에 위임하면 됩니다.
연산자를 정의할 때 두 피연산자(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없습니다. 아래의 코드는 점을 비율에 따라 확대/축소하는 연산인데 파라미터의 타입이 다릅니다.
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
fun main() {
val p1 = Point(10, 20)
println(p1 * 1.5)
}
다만 여기서 코틀린 연산자가 자동으로 교환 법칙(a op b == b op a인 성질)을 지원하지는 않음에 유의해야합니다. 사용자가 p x 1.5 외에 1.5 x p라고도 사용해야 한다면 p x 1.5와 같은 식에 대응하는 연산자 함수인 operator fun Double.times(p: Point): Point를 더 정의해야 합니다.
연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아닙니다. 예를 들어 어던 문자를 여러 번 반복해서 문자열을 만들어내는 연산자를 다음과 같이 정의할 수 있습니다.
// 두 피연산자는 Char와 Int타입
// 반환 타입은 String 타입
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
일반 함수와 마찬가지로 operator 함수도 오버로딩할 수 있습니다. 따라서 이름은 같지만 파라미터 타입이 서로 다른 연산자 함수를 여럿 만들 수 있습니다.
비트 연산자에 대해 특별한 연산자 함수를 사용하지 않습니다.
코틀린은 표준 숫자 타입에 대해 비트 연산자를 정의하지 않습니다. 따라서 커스텀 타입에서 비트 연산자를 정의할 수도 없습니다. 대신 중위 연산자 표기법을 지원하는 일반 함수를 사용해 비트 연산을 수행합니다. 커스텀 타입에서도 그와 비슷한 함수를 정의해 사용할 수 있습니다.
- shl - 왼쪽 시프트(자바 <<)
- shr - 오른쪽 시프트(부호 비트 유지, 자바 >>)
- ushr - 오른쪽 시프트(0으로 부호 비트 설정, 자바 >>>)
- and - 비트 곱(자바 &)
- or - 비트 합(자바 |)
- xor - 비트 배타 합(자바 ^)
- inv - 비트 반전(자바 ~)
plus와 같은 연산자를 오버로딩하면 코틀린은 + 연산뿐 아니라 그와 관련 있는 연산자인 +=도 자동으로 함께 지원하는데, 이런 연산자를 복합 대입(compound assignment) 연산자라 부릅니다.
var point = Point(1, 2)
// 복합 대입 연산자
point += Point(3, 4)
위 코드에서 point += Point(3,4)는 point = point + Point(3,4)와 같습니다. 물론 변수가 변경 가능한 경우(var)에만 복합 대입 연산자를 사용할 수 있습니다. 이 경우에는 위에서 정의한 Plus 연산자가 새로운 Point 객체를 반환하기에 객체에 대한 참조를 변경하게 됩니다.
경우에 따라 += 연산이 객체에 대한 참조를 다른 참조로 바꾸기 보다 원래 객체의 내부 상태를 변경하게 만들고 싶을 때가 있습니다. 변경 가능한 컬렉션에 원소를 추가하는 경우가 대표적입니다.
val numbers = ArrayList<Int>()
numbers += 42
println(numbers[0])
반환 타입이 Unit인 plusAssign 함수를 정의하면 코틀린은 += 연산자에 그 함수를 사용합니다. 다른 복합 연산자 함수도 비슷하게 minusAssign, timesAssign 등의 이름을 사용합니다. 코틀린 표준 라이브러리는 변경 가능한 컬렉션에 대해 plusAssign을 정의하며 앞의 컬렉션에 원소를 추가하는 경우가 그렇습니다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
이론적으로는 코드에 있는 +=를 plus와 plusAssign 양쪽으로 컴파일할 수 있습니다. 다만 책에서는 plus와 plusAssign 연산을 동시에 정의하지 말라고 나와있습니다. 클래스가 앞에서 본 Point처럼 변경이 불가능하다면 plus와 같이 새로운 값을 반환하는 연산만을 추가해야 합니다. 빌더와 같이 변경 가능한 클래스를 설계한다면 plusAssign이나 그와 비슷한 연산만을 제공해야 합니다.
코틀린 표준 라이브러리는 컬렉션에 대해 두 가지 접근 방법을 제공합니다. +와 - 는 항상 새로운 컬렉션을 반환하며, +=와 -= 연산자는 항상 변경 가능한 컬렉션에 적용해 메모리에 있는 객체 상태를 변화시킵니다. 또한 읽기 전용 컬렉션에 +=와 -=는 변경을 적용한 복사본을 반환합니다(var로 선언한 변수가 가리키는 읽기 전용 컬렉션에만 가능). 이런 연산자의 피연산자로는 개별 원소를 사용하거나 원소 타입이 일치하는 다른 컬렉션을 사용할 수 있습니다.
val list = arrayListOf(1, 2)
list += 3 // += 는 list를 변경
val newList = list + listOf(4, 5) // +는 두 리스트의 모든 원소를 포함하는 새로운 리스트를 반환
단항 연산자를 오버로딩하는 절차도 이항 연산자와 마찬가지입니다. 미리 정해진 이름의 함수를 선언하면서 operator로 표시하면 됩니다. 단, 단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 취하지 않습니다.
// 단항 minus(음수) 함수는 파라미터가 없습니다.
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
fun main() {
val p = Point(10, 20)
println(-p)
}
식 | 함수 이름 |
---|---|
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dec |
inc나 dec 함수를 정의해 증가/감소 연산자를 오버로딩하는 경우 컴파일러는 일반적인 값에 대한 전위와 후위 증가/감소 연산자와 같은 의미를 제공합니다. 아래의 예제는 BigDecimal 클래스에서 ++를 오버로딩하는 모습을 보여줍니다.
operator fun BigDecimal.inc() = this + BigDecimal.ONE
fun main() {
var bd = BigDecimal.ZERO
// 후위 증가 연산은 println이 실행된 다음 값을 증가
println(bd++)
// 전위 증가 연산은 println이 실행되기 전 값을 증가
println(++bd)
}
코틀린에서는 산술 연산자와 마찬가지로 원시 타입 값뿐 아니라 모든 객체에 대해 비교 연산을 수행할 수 있습니다. equals나 compareTo를 호출해야 하는 자바와 달리 코틀린에서는 == 비교 연산자를 직접 사용할 수 있어 코드가 간결하고 이해하기 쉽습니다.
코틀린은 == 연산자 호출을 equals 메소드 호출로 컴파일합니다. != 연산자를 사용하는 식도 equals 호출로 컴파일됩니다. 비교 결과를 뒤집은 값을 결과 값으로 사용하는 것입니다. == 와 !=는 내부에서 인자가 널인지 검사하므로 다른 연산과 달리 널이 될 수 있는 값에도 적용할 수 있습니다. 즉, a==b라는 비교를 처리할 때 코틀린은 a가 널인지 판단해서 널이 아닌 경우에만 a.equals(b)를 호출합니다.
위에서 사용한 Point 클래스는 data 클래스이므로 컴파일러가 자동으로 equals를 생성해주는데, 만약 직접 equals를 구현한다면 아래와 비슷합니다.
data 클래스는 자동으로 Any클래스에 정의되어있는 equals, toString, hasoCode 메소드를 정의해줍니다. data 클래스에 대해 자세한 내용은 데이터 클래스에 나와있습니다.
class Point(val x: Int, val y: Int) {
// Any에 정의된 메소드 오버라이딩
override fun equals(other: Any?): Boolean {
// 최적화 - 파라미터가 this와 같은 객체인지
if(other === this) return true
// 파라미터 타입 검사
if(other !is Point) return false
// Point로 스마트 캐스트해서 x와 y 프로퍼티에 접근
return other.x == x && other.y == y
}
}
fun main() {
// == 연산자
println(Point(10, 20) == Point(10, 20))
// != 연산자
println(Point(10, 20) != Point(5, 5))
}
식별자 비교(identity equals) 연산자(===)를 사용해 equals 파라미터가 수신 객체와 같은지 살펴봅니다. 여기서 식별자 비교 연산자는 자바 == 연산자와 같습니다. 따라서 ===는 자신의 두 피연산자가 서로 같은 객체를 가리키는지(원시타입인 경우 두 값이 같은지
) 비교합니다. equals를 구현할 때는 ===를 사용해 자기 자신과의 비교를 최적화하는 경우가 많고, ===를 오버로딩할 수 없습니다.
equals 함수에는 override가 붙어있습니다. 다른 연산자 오버로딩 관례와 달리 equals는 Any에 정의된 메소드이므로 override가 필요합니다. Any의 equals에는 operator가 붙어있지만 그 메소드를 오버라이드하는 메소드 앞에는 operator 변경자를 붙이지 않아도 자동으로 상위 클래스의 operator지정이 적용됩니다. 또한 Any에서 상속받은 equals가 확장 함수보다 우선순위가 높기 때문에 equals를 확장 함수로 정의할 수 없다는 사실에 유의해야 합니다.
또한 예제에서 != 호출이 equals 메소드 호출로 바뀐다는 사실을 보여줍니다. 컴파일러는 equals의 반환 값을 반전시켜 돌려줍니다.
자바에서 정렬이나 최댓값, 최솟값 등 값을 비교해야 하는 알고리즘에 사용할 클래스는 Comparable 인터페이스를 구현해야 합니다. Comparable에 들어있는 compareTo 메소드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내줍니다. 하지만 자바에는 이 메소드를 짧게 호출할 수 있는 방법이 없고, < 나 >
등의 연산자로는 원시 타입의 값만 비교할 수 있습니다.
코틀린도 똑같은 Comparable 인터페이스를 지원하는데, 게다가 안에 있는 compareTo 메소드를 호출하는 관례를 제공합니다. 따라서 아래와 같이 비교 연산자 (<, >, <=, >=)는 compareTo 호출로 컴파일 되고, 반환하는 값은 Int 입니다.
class Person(
val firstName: String, val lastName: String
): Comparable<Person> {
override fun compareTo(other: Person): Int {
// 인자로 받은 함수를 차례로 호출하면서 값을 비교
return compareValuesBy(this, other, Person::lastName, Person::firstName)
}
}
fun main() {
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
// 비교 연산자 사용
println(p1 < p2)
}
여기서 정의한 Person 객체의 Comparable 인터페이스를 코틀린뿐 아니라 자바 쪽의 컬렉션 정렬 메소드 등에도 사용할 수 있습니다. 또한 equals와 마찬가지로 operator를 붙이지 않았습니다. 여기서는 compareValuesBy 메소드를 사용하였지만 필드를 직접 비교하면 코드는 조금 복잡해지지만 비교 속도는 훨씬 더 빨리집니다.
Comparable 인터페이스를 구현하는 모든 자바 클래스를 코틀린에서는 간결한 연산자 구문으로 비교할 수 있습니다.
// 자바의 String 클래스는 Comparable 인터페이스를 구현
println("abc" < "bac")
컬렉션을 다룰 때 가장 많이 쓰이는 연산은 인덱스를 사용해 원소를 읽거나 쓰는 연산과 어떤 값이 컬렉션에 속해있는지 검사하는 연산입니다. 이 연산들을 연산자 구문으로 사용할 수 있습니다. 인덱스를 사용해 원소를 설정하거나 가져오고 싶을 때는 a[b]라는 식을 사용합니다. in 연산자는 원소가 컬렉션이나 범위에 속하는지 검사하거나 컬렉션에 있는 원소를 이터레이션할 때 사용합니다. 사용자 지정 클래스에 이런 연산을 추가할 수 있습니다.
코틀린에서 맵의 원소에 접근할 때나 자바에서 배열 원소에 접근할 때 모두 각괄호([ ])를 사용한다는 사실을 알고 있습니다. 같은 연산자를 사용해 변경 가능 맵에 키/값 쌍을 넣거나 이미 맵에 들어있는 키/값 연관 관계를 변경할 수 있습니다.
val value = map[key]
mutablaMap[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")
}
}
fun main() {
val p = Point(10, 20)
println(p[1])
}
get이라는 메소드를 만들고 operator 변경자를 붙이기만 하면 됩니다. 그 후 p[1]이라는 식은 p가 Point 타입인 경우 방금 정의한 get 메소드로 변환됩니다.
get 메소드의 파라미터로 Int가 아닌 다른 타입도 사용할 수 있습니다. 예를 들어 맵의 경우 get의 파라미터 타입은 맵의 키 타입과 같은 임의의 타입이 될 수 있습니다.
또한 여러 파라미터를 사용하는 get을 정의할 수도 있습니다. 예를 들어 2차원 행열이나 배열을 표현하는 클래스에 operator fun get(rowIndex: Int, colIndex: Int)를 정의하면 matrix[row, col]로 호출할 수 있습니다.
컬렉션 클래스가 다양한 키 타입을 지원해야 한다면 다양한 파라미터 타입에 대해 오버로딩한 get 메소드를 여럿 정의할 수도 있습니다.
인덱스에 해당하는 컬렉션 원소를 쓰고 싶을 때는 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")
}
}
fun main() {
val p = MutablePoint(10, 20)
// set 관례 사용
p[1] = 42
}
대입에 인덱스 연산자를 사용하려면 set이라는 이름의 함수를 정의해야 합니다. set이 받는 마지막 파라미터 값은 대입문의 우항에 들어가고, 나머지 파라미터 값은 인덱스 연산자([ ])에 들어갑니다.
in은 객체가 컬렉션에 들어있는지 검사(멤버십 검사
)합니다. 그런 경우 in 연산자와 대응하는 함수는 contains입니다.
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
// contains 함수를 사용해 in 연산자 만들기
operator fun Rectangle.contains(p: Point): Boolean {
// 범위를 만들고 x좌표, y좌표가 그 범위안에 있는지 검사
return p.x in upperLeft.x until lowerRight.x &&
// until 함수를 사용해 열린 범위를 만듭니다.
// 끝 값을 포함하지 않는 범위
// ..을 사용하면 끝 값을 포함하는 범위
p.y in upperLeft.y until lowerRight.y
}
fun main() {
var rect = Rectangle(Point(10, 20), Point(50, 50))
// in 연산자 사용
println(Point(20, 30) in rect)
}
in의 우항에 있는 객체는 contains 메소드의 수신 객체(예제에서는 Rectangle
)가 되고, in의 좌항에 있는 객체는 contains 메소드에 인자(예제에서는 Point
)로 전달됩니다.
범위를 만들려면 ..구문을 사용합니다. 예를 들어 1..10은 1부터 10까지 모든 수가 들어있는 범위를 가리킵니다. .. 연산자는 rangeTo 함수를 간략하게 표현하는 방법입니다.
rangeTo 함수는 범위를 반환하고 이 연산자를 아무 클래스에나 정의할 수 있습니다. 하지만 어떤 클래스가 Comparable 인터페이스를 구현하면 rangeTo를 정의할 필요가 없습니다. 왜냐하면 코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있기 때문입니다.
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
이 함수는 범위를 반환하며, 어떤 원소가 그 범위 안에 들어있는지 in을 통해 검사할 수 있습니다.
예를 들어 LocalData 클래스를 사용해 날짜의 범위를 만들어보겠습니다.
val now = LocalDate.now()
// now(오늘)부터 시작해 10일짜리 범위를 만듭니다.
val vacation = now..now.plusDays(10)
// 특정 날짜가 날짜 범위 안에 들어가는지 검사합니다.
println(now.plusWeeks(1) in vacation)
now..now.plusDays(10)이라는 식은 컴파일러에 의해 now.rangeTo(now.plusDays(10))으로 변환됩니다. rangeTo 함수는 LocalDate의 멤버는 아니며, 앞에서 설명한대로 Comparable의 확장 함수입니다.
rangeTo 연산자는 다른 산술 연산자보다 우선순위가 낮아 혼동을 피하기 위해서 괄호로 인자를 감싸주는 것이 좋습니다.
val n = 9
println(0..(n + 1))
코틀린의 for 루프는 범위 검사와 똑같이 in 연산자를 사용합니다. 하지만 이 경우의 in은 iterator()를 호출해서 이터레이터를 얻은 다음, 자바와 마찬가지로 그 이터레이터에 대해 hasNext와 next 호출을 반복하는 식으로 변환됩니다.
하지만 코틀린에서는 이 또한 관례이므로 iterator 메소드를 확장 함수로 정의할 수 있습니다. 이런 성질로 인해 일반 자바 문자열에 대한 for 루프가 가능합니다. 왜냐하면 코틀린 표준 라이브러리는 String의 상위 클래스인 CharSequence에 대한 iterator 확장 함수를 제공하기 때문입니다.
// 이 라이브러리 함수는 문자열을 이터레이션할 수 있게 해줍니다.
operator fun CharSequence.iterator(): CharIterator
for (c in "abc") {
}
iterator를 직접 구현할 수도 있는데 아래의 코드는 날짜에 대해 이터레이션 하는 코드입니다.
// ClosedRange : 값의 범위를 표현하는 인터페이스
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
// 이 객체는 LocalDate 원소에 대한 Iterator를 구현
object : Iterator<LocalDate> {
var current = start
override fun hasNext(): Boolean =
// compareTo 관례를 사용해 날짜를 비교
current <= endInclusive
// 현재 날짜를 저장한 다음에 날짜를 변경
// 그 후 저장해둔 날짜를 반환
override fun next() = current.apply {
// 현재 날짜를 1일 뒤로 변경
current = plusDays(1)
}
}
fun main() {
val newYear = LocalDate.ofYearDay(2021, 1)
// 범위 연산자 사용
val daysOff = newYear.minusDays(1)..newYear
// daysOff에 대응하는 iterator 함수가 있으면 daysOff에 대해 이터레이션
for(daOff in daysOff) { println(daysOff) }
}
위에서 살펴본 rangeTo 라이브러리 함수는 ClosedRange의 인스턴스를 반환합니다. 코드에서 ClosedRange<LocalDate>
에 대한 확장 함수 iterator를 정의했기 때문에 LocalDate의 범위 객체를 for 루프에 사용할 수 있습니다.
구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있습니다. 구조 분해 선언은 일반 변수 선언과 비슷하지만 좌변에 여러 변수를 괄호로 묶었다는 점이 다릅니다. 내부에서 구조 분해 선언은 관례를 사용하며, 각 변수를 초기화하기 위해 ComponentN이라는 함수를 호출합니다.
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
// 값을 저장하기 위한 데이터 클래스를 선언
data class NameComponents(val name: String, val extension: String)
fun splitFileName(fullName: String): NameComponents {
// 배열이나 컬렉션에도 componentN 함수가 존재
val (name, ext) = fullName.split('.', limit = 2)
// 함수에서 데이터 클래스의 인스턴스를 반환
return NameComponents(name, ext)
}
fun main() {
// 구조 분해 선언 구문을 사용해 데이터 클래스를 푼다
val (name, ext) = splitFileName("example.kt")
println("name : $name, ext : $ext")
// 결과 - name : example, ext : kt
}
무한히 componentN을 선언할 수는 없고 코틀린 표준 라이브러리에서는 맨 앞의 다섯 요소에 대한 componentN을 제공합니다.
함수 본문 내의 선언문뿐 아니라 변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해를 선언할 수 있습니다. 예를 들면 루프 안에서도 구조 분해 선언을 사용할 수 있는데, 특히 맵의 원소에 대해 이터레이션할 때 구조 분해 선언이 유용합니다.
fun printEntries(map: Map<String, String>) {
// 루프 변수에 구조 분해 선언을 사용
for((key, value) in map) {
println("$key -> $value")
}
}
fun main() {
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
// 결과
// Oracle -> Java
// JetBrains -> Kotlin
// Map.Entry에 대한 확장 함수로 component1과 component2를 제공
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
}
}
참조
Kotlin in action
틀린 부분을 댓글로 남겨주시면 수정하겠습니다..!!