구조 분해 선언(Destructuring declarations)
구조 분해 선언(Destructuring declarations)
객체를 여러 변수로 한 번에 분해하면 편리할 때가 있어요. 예를 들어 이런 코드죠:
val (name, age) = person
이런 문법을 구조 분해 선언(destructuring declaration) 이라고 불러요. 구조 분해 선언은 여러 변수를 한 번에 만들어냅니다. 위에서 name과 age라는 새 변수 두 개를 선언했고, 그 둘을 각각 독립적으로 사용할 수 있어요:
println(name)
println(age)
구조 분해 선언은 컴파일되면 다음과 같은 코드로 바뀝니다:
val name = person.component1()
val age = person.component2()
component1()과 component2() 함수는 Kotlin에서 널리 쓰이는 관례(convention) 원칙의 또 다른 예시예요(연산자 +나 *, for 루프도 그 예시죠). 구조 분해 선언의 오른쪽에는 필요한 개수만큼의 component 함수를 호출할 수 있으면 무엇이든 올 수 있어요. 물론 component3(), component4()처럼 더 이어질 수도 있고요.
구조 분해 선언은 for 루프에서도 동작합니다:
for ((a, b) in collection) { ... }
변수 a와 b는 컬렉션의 각 요소에 대해 호출된 component1()과 component2()가 반환하는 값을 받아요.
출처: Kotlin 공식 문서
본문
예시: 함수에서 두 값 반환하기
함수에서 두 가지를 반환해야 한다고 가정해 볼게요 — 예를 들어 결과 객체와 어떤 상태 같은 것 말이죠. Kotlin에서 이걸 간결하게 처리하는 방법은 data class를 선언하고 그 인스턴스를 반환하는 거예요:
data class Result(val result: Int, val status: Status)
fun function(...): Result {
// computations
return Result(result, status)
}
// Now, to use this function:
val (result, status) = function(...)
data class는 componentN() 함수를 자동으로 선언해 주기 때문에, 구조 분해 선언이 여기서 잘 동작합니다.
예시: 구조 분해 선언과 맵
맵을 순회하는 가장 멋진 방법은 아마 이거일 거예요:
for ((key, value) in map) {
// do something with the key and the value
}
이게 동작하게 하려면,
iterator()함수를 제공해서 맵을 값의 시퀀스로 나타내야 해요.component1()과component2()함수를 제공해서 각 요소를 쌍(Pair)으로 나타내야 하고요.
실제로 표준 라이브러리가 그런 확장을 제공합니다:
operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()
그래서 맵(또는 data class 인스턴스 컬렉션 같은 것들)과 함께 for 루프에서 구조 분해 선언을 자유롭게 쓸 수 있어요.
사용하지 않는 변수는 밑줄로
구조 분해 선언에서 변수가 필요 없다면 이름 대신 밑줄을 넣을 수 있어요:
val (_, status) = getResult()
이렇게 건너뛰는 component들에 대해서는 componentN() 연산자 함수가 호출되지 않습니다.
람다에서의 구조 분해
람다 파라미터에도 구조 분해 선언 문법을 쓸 수 있어요. 람다에 Pair 타입(또는 Map.Entry, 적절한 componentN 함수를 가진 다른 타입)의 파라미터가 있다면, 그 하나 대신 괄호에 여러 새 파라미터를 넣을 수 있습니다:
map.mapValues { entry -> "${entry.value}!" }
map.mapValues { (key, value) -> "$value!" }
파라미터 두 개를 선언하는 것과, 파라미터 대신 구조 분해 쌍을 선언하는 것의 차이를 잘 보세요:
{ a -> ... } // one parameter
{ a, b -> ... } // two parameters
{ (a, b) -> ... } // a destructured pair
{ (a, b), c -> ... } // a destructured pair and another parameter
구조 분해된 파라미터의 component가 사용되지 않는다면 밑줄로 바꿔서 이름을 짓지 않아도 돼요:
map.mapValues { (_, value) -> "$value!" }
구조 분해된 파라미터 전체의 타입이나, 특정 component 하나의 타입을 지정할 수도 있습니다:
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" }
map.mapValues { (_, value: String) -> "$value!" }
이름 기반 구조 분해(Name-based destructuring)
Kotlin은 이름 기반 구조 분해 선언을 지원해요. 이 경우 변수가 위치 기반 구조 분해의 componentN() 함수로 정해지는 위치가 아니라 속성 이름으로 값에 매칭되죠.
위치 기반 구조 분해에서 변수는 componentN() 함수의 순서에 대응돼요. 예를 들어 볼게요:
data class User(val username: String, val email: String)
fun main() {
val user = User("alice", "[email protected]")
val (email, username) = user
println(email)
// alice
println(username)
// [email protected]
}
이 예시에서는 구조 분해가 componentN() 함수의 순서에 의존하기 때문에, email은 username의 값을 받고 username은 email의 값을 받아요.
이름 기반 구조 분해에서는 어떤 값을 추출할지가 componentN() 함수의 위치가 아니라 속성 이름으로 정해집니다:
fun main() {
val user = User("alice", "[email protected]")
// Uses name-based destructuring with explicit form
(val mail = email, val name = username) = user
println(name)
// alice
println(mail)
// [email protected]
}
이름 기반 구조 분해는 Experimental(실험적) 기능이에요. 이 기능을 켜면 대괄호를 쓰는 위치 기반 구조 분해 문법도 새로 도입됩니다. 이 문법은 순서가 중요한 타입 — 리스트 같은 순서 있는 컬렉션이나 Pair, Triple 같은 이름 없는 튜플 — 에 사용해요:
val point = Pair(10, 20)
// Uses position-based destructuring
val [x, y] = point
구조 분해 선언을 컴파일러가 어떻게 해석할지는 -Xname-based-destructuring 컴파일러 옵션으로 제어할 수 있어요. 다음 모드들이 있습니다:
only-syntax— 기존 구조 분해 선언의 동작을 바꾸지 않으면서 이름 기반 구조 분해의 명시적 형태만 활성화해요.name-mismatch— data class의 위치 기반 구조 분해에서 변수 이름이 속성 이름과 맞지 않으면 경고를 보고해요.complete— 괄호를 쓰는 축약형 이름 기반 구조 분해를 활성화하고, 대괄호 문법의 위치 기반 구조 분해는 계속 지원해요.
complete 모드를 쓰면 괄호를 쓰는 축약형 구조 분해 문법이 위치가 아니라 속성 이름에 변수를 매칭시켜요:
val (email, username) = user
프로젝트에서 이름 기반 구조 분해를 켜려면 빌드 설정 파일에 컴파일러 옵션을 추가하세요:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xname-based-destructuring=only-syntax")
}
}
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xname-based-destructuring=only-syntax</arg>
</args>
</configuration>
</plugin>
</plugins>
</build>