자바에는 없지만 코틀린에서 제공하는 기본 라이브러리 함수들이 있습니다. 그 중에 Scope functions는 let, run, with, apply, also를 말합니다.
Scope functions는 객체에 접근하는 방법을 쉽게 해 줍니다. 이런 함수들을 이용하면 코드가 간결해지고, 가독성을 높여줄 수 있습니다.
간단히 예를들면, 이런 코드가 있습니다.
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
let
을 사용하면 아래처럼 간결하게 쓸 수 있습니다.
alice
로 접근해야 했던 것을 let
의 도움으로 it
를 사용하여 접근하였습니다.
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
위의 5개 함수들은 매우 비슷합니다. 공통점은 객체에 간단한 코드로 접근할 수 있고 어떤 값을 리턴한다는 점입니다. 그리고 약간의 차이점이 있습니다. 이런 차이점을 알고 용도에 맞게 사용하는 것이 중요합니다. 각각 언제 사용하고, 차이점이 무엇인지에 알아보겠습니다.
리시버와 람다함수
Scope functions를 설명하면서 리시버와 람다함수란 용어가 나옵니다. Scope functions를 사용할 때 두개의 객체를 넘겨줍니다. 하나는 리시버, 하나는 람다함수입니다.
아래 코드에서 Person
객체를 리시버라고 하고 let 다음의 { ... }
를 람다함수라고 합니다.
람다함수에서는 Person객체에 접근할 수 있습니다.
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
차이점
차이점은 크게 두가지 입니다.
- 객체를 접근하는 방법: this or it
- 리턴 값
객체에 접근하는 방법은 this와 it(람다 인자)가 있습니다.
run, with, apply는 this
를 사용하여 객체에 접근하고 let과 also는 it
를 사용합니다.
리턴 값으로, apply, also는 리시버(context) 객체를 마지막에 리턴해줍니다. 반면에 let, run, with는 람다 함수의 마지막 결과를 리턴합니다.
아래 코드는 run을 사용한 예로, 리턴 값은 count { it.endsWith("e") }
의 결과가 됩니다.
따라서 countEndsWithE은 3으로 설정됩니다.
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")
// 실행 결과
There are 3 elements that end with e.
let
let
은 it로 리시버에 접근하고, 람다함수의 마지막 결과를 리턴합니다.
let
은 리시버의 여러 함수들을 호출할 때 사용할 수 있습니다.
예를들어 이렇게 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}
또, 아래 코드처럼 사용하면 리시버가 null이 아닐 때만 동작하도록 할 때 사용할 수 있습니다.
null이 아닐 때 람다함수의 코드가 동작하고 마지막 결과가 리턴됩니다.
여기서는 it.length
가 리턴되어 length로 설정됩니다.
val str: String? = "Hello"
val length = str?.let {
println("let() called on $it")
it.length
}
// 실행 결과
5
with
with
은 this로 리시버에 접근하고, 람다함수의 마지막 결과를 리턴합니다.
let
은 리시버 객체의 확장함수(extension function)로 쓰이지만, with
는 그렇지 않습니다.
리시버 객체는 with(리시버 객체) { 람다함수 }
로 전달합니다.
with
는 리턴값을 사용하지 않는 경우에 쓸 것을 권장하고 있습니다.
아래 코드에서 with
의 람다함수는 this로 numbers에 접근하고 있습니다.
그리고 리턴값은 쓰지 않고 있습니다. 이렇게 사용하면 코드가 간결해지고 가독성이 높아질 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
이런식으로도 사용될 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)
run
run
은 this로 리시버에 접근하고, 람다함수의 마지막 결과를 리턴합니다.
run
은 with
동일합니다. this로 리시버에 접근하고, 람다함수의 마지막 값을 리턴합니다.
하지만 run은 리시버의 확장 함수(extension function)로 쓸 수 있습니다.
with는 with(리시버) { 람다함수 }
처럼 인자로만 전달하지만 run은 리시버.run { 람다함수 }
처럼 리시버 객체의 확장함수로 쓸 수 있습니다.
run
은 람다함수에서 여러 값을 초기화하고 리턴 값을 어떤 객체의 초기값으로 사용할 때 쓰면 좋습니다.
아래 코드처럼 사용할 수 있습니다.
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
위의 코드는 let
을 사용하여 아래처럼 쓸 수 있습니다. 차이점은 it로 리시버에 접근한다는 점입니다.
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
run
의 특징으로, 확장함수로 사용하지 않아도 됩니다. run { }
처럼 사용할 수도 있습니다.
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
println(match.value)
}
apply
apply
는 this로 리시버에 접근하고, 리시버 객체를 리턴합니다.
아래 코드에서 리시버는 Person
객체이고, 리턴 값으로 리시버 객체인 Person
이 리턴됩니다.
따라서, 객체 adam
은 age=32
, city=Lo리ndonadam
가 설정된 Person객체가 됩니다.
val adam = Person("Adam").apply {
age = 32
city = "London"
}
also
also
은 it로 리시버에 접근하고, 리시버 객체를 리턴합니다.
아래 코드처럼 사용할 수 있습니다. also
가 리시버 스스로를 리턴하기 때문에 빌더패턴처럼 연속적으로 numbers의 함수를 호출할 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
정리
Scope functions에 대해서 알아보았습니다. 5개 모두 비슷한 성질이 있고 약간의 차이점만 있습니다. 그리고 매우 유사한 것들은 서로 대체해서 사용할 수도 있습니다. 가장 좋은 것은 프로젝트의 코드가 간결해지고 가독성이 좋아지는 방향으로 쓰이는 것입니다. 여기서 정리한 5개 함수들의 특징에 대해서 간략히 알아두면 도움이 될 것 같습니다.
참고
Related Posts
- Kotlin - 배열에서 최소 값, 최대 값 찾기
- Kotlin - 2차원 배열 선언, 초기화 방법
- Kotlin - 배열 선언, 초기화 방법
- Kotlin - 리스트, 배열 길이 가져오기
- Kotlin - 리스트에서 최대, 최소 값 찾기
- Kotlin - for 반복문, 배열/리스트 순회
- Kotlin - Timer, 주기적으로 함수 실행
- Kotlin - sleep, 쓰레드 몇 초 지연
- Kotlin - Thread 생성 및 실행
- Kotlin에서 정규표현식 사용하기
- Kotlin - 문자열 길이 계산
- Kotlin - 문자열 비교 방법(equals, ==, compareTo)
- Kotlin - 2개의 배열 하나로 합치기
- Kotlin - 2개의 List 하나로 합치기
- Kotlin - 디렉토리의 모든 파일 리스트 출력
- Kotlin - 리스트 정렬 방법 (sort, sortBy, sortWith)
- Kotlin - 문자열 뒤집기 (Reverse String)
- Kotlin - 랜덤 숫자 생성 (Random, SecureRandom)
- Kotlin - Range, 숫자 범위 표현
- Kotlin - 음수를 양수로 변환, math.abs()
- Kotlin - List를 Set로 변환
- Kotlin - Set를 List로 변환
- Kotlin - 문자열에서 숫자(int)만 추출하는 방법
- Kotlin - Map을 List로 변환하는 방법
- Kotlin - File, Directory가 존재하는지 확인
- Kotlin - List를 Map으로 변환
- Kotlin - List의 중복 요소 제거
- Kotlin - List를 Array로 변환
- Kotlin - 엘비스 연산자 (Elvis Operation)
- Kotlin - Array를 List로 변환
- Kotlin - String을 Float으로 변환
- Kotlin - String을 Double으로 변환
- Kotlin - String을 Int로 변환
- Kotlin - String을 Long으로 변환
- Kotlin - String Null 또는 Empty 체크