본 내용은 Scope function
에 대해 정확하게 공부하기 위해 kotlin 공식문서의 Scope function
의 내용을 번역한 글로 Scope function(1)에서는 각 함수별로 어떤 차이가 있는지에 대한 내용을 알아봤다.
이 글에서는 각 함수가 어떤 용도로 사용되는게 적절한지 알아본다.
[TIL] Scope Function (1) - 보러가기
함수별로 자세한 설명을 통해 어떤 경우에 해당 함수를 사용하는게 적절한지 알아본다.
Scope function
은 서로 대체해서 사용하는게 가능하기 때문에 예제를 통해 어떤경우에 해당 함수를 사용하는게 옳은지 확인해보자.
- 객체를 참조할때 인자로 참조한다. (
it
으로 참조)- 람다식의 결과값을 반환한다.
let
은 함수를 연속적으로 호출한뒤 해당 결과 값을 대상으로 하나 혹은 이상의 함수를 호출해야 하는 경우 사용할 수 있다.
예를들어 다음과 같은 코드는 MutableList
에 대해 2가지 작업을 수행한다.
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)
// println말고도 다른 함수를 더 넣을수도 있다.
}
만약 let
안에 사용된 코드가 함수 단 한개일때, 그리고 인자로 해당 객체만을 받는 경우에는 람다 인자로 사용하지 않고 참조함수를 사용하는것도 가능하다. (::
)
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
또한 let
은 non-null
값으로 실행되야 하는 함수를 사용하기 위해 쓰기도 한다. non-null
값으로 작업을 수행하기 위해서 safe call
연산자인 ?.
를 사용해 let
을 호출하고 람다식을 작성한다.
val str: String? = "Hello"
//processNonNullString(str) // 만약 그냥 실행하면 컴파일 에러가 난다.
val length = str?.let { // error: str can be null
println("let() called on $it")
processNonNullString(it) // ?.let을 통해 널체크가 되었기 때문에 에러없이 실행된다.
it.length
}
가독성을 위해 참조한 객체의 이름을 바꿔서 let
의 Scope
내부에서만 사용되는 새로운 이름의 지역변수처럼 사용할수도 있다. 이 경우 람자인자로 전달받은 객체에 이름을 명시해 기본값인 it
대신 사용할 수 있도록 해야한다.
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem -> // Scope 내부에서 numbers.first()를
println("The first item of the list is '$firstItem'") // firstItem으로 사용
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")
- 객체를 참조할때 수신자로 참조한다. (
this
으로 참조)- 람다식의 결과값을 반환한다.
with
는 확장함수가 아니기 때문에 객체가 인자로 전달된다. 하지만 람다 내부에서는 수신자로 참조되어 this
로 참조한다.
with
의 경우 람다의 반환값을 사용할 필요가 없는 경우 사용하는 것이 권장된다. 코드내에 with
가 사용된경우 "이 객체를 사용해 다음 코드들을 실행해라."
라고 해석할 수 있다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
또한 결과값을 변수에 대입할때 해당 결과값의 계산에 객체의 프로퍼티나 함수를 사용해야하는경우 with
를 helper object
로 사용해 가독성을 높일 수 있다.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," + // 결과값 계산에 number에 대한 함수를 사용
" the last element is ${last()}"
}
println(firstAndLast)
- 객체를 참조할때 수신자로 참조한다. (
this
으로 참조)- 람다식의 결과값을 반환한다.
run
은 기본적으로 with
와 기능이 비슷하다. 하지만 확장함수이기 때문에 let
처럼 .
사용해 객체에서 직접 호출할 수 있다.
run
은 람다내부에서 객체를 초기화 한 뒤 결과값을 계산할 경우 사용한다.
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080 // service의 port프로퍼티를 초기화
query(prepareRequest() + " to port $port") // 초기화 된 port를 사용해 결과값을 result에 대입
}
// 똑같은 코드를 let으로 사용한 경우
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
또한 run
은 비확장 함수로도 호출이 가능하다. 비확장 함수로 run
을 호출할 경우 run
은 참조할 객체는 없지만, 여전히 람다결과값을 반환한다.
따라서 표현식을 사용할때 여러줄의 코드를 작성하고 싶은 경우 run
을 비확장 함수로 사용하면 된다.
비확장함수로 사용되는 run
은 "람다 내부의 코드들을 실행하고 결과를 계산해라."
라고 해석이 가능하다.
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+") // Regex에 위의 변수들을 사용해 결과를 계산
}
for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
println(match.value)
}
- 객체를 참조할때 수신자로 참조한다. (
this
으로 참조)- 참조한 객체 자체를 반환한다.
apply
는 참조한 객체 자체를 반환하기 때문에 주로 참조한 객체의 멤버에 접근하는 코드를 사용하는게 좋다.
(결과값을 반환하지 않는 코드)
보통 apply
는 객체를 구성(초기화)할 때 사용한다. 이렇게 사용할경우 "객체 멤버들에 다음과 같은 내용들을 할당한다."
라고 해석하는게 가능하다.
val adam = Person("Adam").apply { // 결과값을 계산하지 않고 Person객체 내부의 프로퍼티를 할당
age = 32
city = "London"
}
println(adam)
또한 연속적으로 함수가 호출될 경우 여러 작업을 수행하고 결과적으로 수정된 객체를 반환하게 해 복잡한 작업을 보다 보기 쉽게 만들 수 있다.
data class Car(var make: String, var model: String, var year: Int)
val car = Car("Toyota", "Camry", 2022).apply {
year = 2023 // apply로 year를 초기화
}.run {
make = make.toUpperCase() // run으로 make를 대문자로 변환
this // 수정된 객체를 this로 반환
}.apply {
model = "$model Sport" // apply로 model을 변경한 뒤 객체를 반환
}
println("Final: $car") // Final: Car(make=TOYOTA, model=Camry Sport, year=2023)
- 객체를 참조할때 인자로 참조한다. (
it
으로 참조)- 참조한 객체 자체를 반환한다.
also
는 참조 객체를 매개변수로 전달받아야하는 코드를 사용할 경우 유용하다.
참조한 객체의 프로퍼티나 메서드에 접근할 필요없이 객체 자체에 대한 참조만 필요할 경우나,
this
를 사용할 경우 Scope
외부에 있는 참조와 겹칠때 이를 방지하기위해 사용할 수 있다.
코드에 also
가 사용됐을경우 "이 객체를 사용해 다음의 코드도 실행한다"
라고 해석할 수 있다.
추가적으로 takeIf
와 takeUnless
에 대한 내용이 나오지만 심화적인 부분이라고 판단되어 따로 해석하진 않았다.
(나중에 이 함수들의 필요성이 느껴질 때 번역해야겠다.)