[Kotlin] Scope Functions : let, run, with, apply, also
범위 함수(Scope Functions)
Kotlin의 범위 함수는 객체를 일시적으로 특정 범위(scope) 안에서 사용하도록 도와주는 함수들이다. 이 범위 안에서 객체를 이름 없이 접근할 수 있다.
범위 함수는 새로운 기술적 기능을 도입하지는 않지만 코드를 더 간결하고 가독성 있게 만들 수 있다. 범위 함수의 선택은 주로 프로젝트의 의도와 사용의 일관성에 따라 달라진다.
범위 함수의 선택
let
: 널이 아닌 객체에서 람다 실행 (null이 아닌 경우에만 실행하고 싶을 때)let
: 로컬 범위에서 표현식을 변수로 도입 (블록 내에서 임시 변수를 만들고 싶을 때)apply
: 객체 구성 (객체를 초기화하거나 구성할 때)run
: 객체 구성 및 결과 계산 (객체를 초기화하면서 결과 값을 계산할 때)non-extension
run
: 표현식이 필요한 경우 실행문 (범위를 형성하지 않고 코드 블록을 실행해야 할 때)also
: 추가 효과 (주로 객체를 반환하면서 로그를 남기거나 디버깅할 때)with
: 객체에 대한 함수 호출 그룹화 (같은 객체에 대해 여러 함수를 호출해야 할 때)
범위 함수는 코드를 더 간결하게 만들 수 있지만, 남용하지 않도록 주의해야 한다. 남용하면 코드가 읽기 어려워지고 오류가 발생할 수 있다. 또한, 범위 함수를 중첩해서 사용하지 않도록 하고, 체이닝할 때도 주의해야 한다. 현재 컨텍스트 객체와 this
나 it
의 값이 무엇인지 혼동하기 쉽기 때문이다.
범위 함수의 2가지 주요 차이점
1. 컨텍스트 객체를 참조하는 방식 (this
or it
)
2. 반환 값
범위 함수에 전달된 람다 내에서 컨텍스트 객체는 실제 이름 대신 짧은 참조로 사용할 수 있다. 각 범위 함수는 람다 수신자(this) 또는 람다 인수(it)로 컨텍스트 객체를 참조하는 두 가지 방법 중 하나를 사용한다. 둘 다 동일한 기능을 제공한다.
Context object: this or it
run
, with
, apply
는 컨텍스트 객체를 람다 수신자(receiver)로 참조하며, 키워드 this
를 사용한다. 이 함수들의 람다 블록 내에서는 객체가 일반 클래스 함수처럼 사용 할 수 있다.
대부분의 경우, 수신 객체의 멤버에 접근할 때 this
를 생략할 수 있어서 코드가 더 짧아진다. 그러나 this
를 생략하면 수신 객체의 멤버와 외부 객체 또는 함수 간의 구분이 어려울 수 있다.
따라서 객체의 함수 호출이나 속성에 값을 할당하는 등 주로 객체의 멤버를 조작하는 람다에서는 this
를 수신 객체로 사용하는 것이 권장됩니다.
val adam = Person("Adam").apply {
age = 20 // same as this.age = 20
city = "London"
}
println(adam)
// Person(name=Adam, age=20, city=London)
두 가지 범위 함수(let
, also
)는 람다 블록 안에서 컨텍스트 객체를 it
이라는 이름으로 참조합니다. it
은 기본적으로 제공되는 이름으로, 이를 통해 객체에 접근할 수 있습니다.
val name = "Kotlin"
name.let {
println(it) // it은 "Kotlin"을 가리킴
it.length // it의 길이 반환
}
위 코드에서 it
은 name
객체를 가리킨다. it
을 사용하면 객체의 속성과 메서드에 접근할 수 있다. it
은 this
보다 짧아서 코드가 더 간결하고 표현이 간단해져서 읽기 쉽다.
주의할 점은 암시적으로 접근할 수 없다는 점이다. 객체의 메서드나 속성을 호출할 때 this
처럼 암시적으로 접근할 수 없어서, 항상 it
을 사용해야 한다.
그렇다면 언제 it을 사용하는 것이 적합할까?
함수 인자로 주로 사용될 때: 객체가 함수의 인자로 주로 사용될 때
it
이 더 적합하다.여러 변수를 사용할 때: 코드 블록에서 여러 변수를 사용할 때
it
이 더 명확하다.
val age = 25
person.let {
println(it.name) // it은 person을 가리킴
println(age) // 외부의 age를 가리킴
}
이렇게 하면 it과 외부 변수를 명확하게 구분할 수 있다.
반환 값 (Return value)
범위 함수들은 반환 값에 따라 다르다.
apply
와also
는 컨텍스트 객체를 반환한다.let
,run
,with
는 람다의 결과를 반환한다.
let
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)
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
}
list의 연산 결과를 변수에 재할당하지 않을 수 있다.
let
에 전달된 코드 블록이 it
을 인자로 받는 단일 함수인 경우, 람다 대신 메서드 참조(::)를 사용할 수 있다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
let
은 널이 아닌 값이 포함된 코드 블록을 실행하는 데 자주 사용된다. 널이 아닌 객체에 대해 작업을 수행하려면 해당 객체에 안전 호출 연산자 ?.
를 사용하고 해당 람다에 있는 작업으로 let
을 호출한다.
val str: String? = "Hello"
// processNonNullString(str)
// compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it)
// OK: 'it' is not ull inside '?.let { }'
it.length
}
// let() called on Hello