Kotlin Scope functions 지난 글에 이어서 이번 글에서는 각각 언제 사용해야 하는지 용도를 알아보겠다.
근데 사실 스코프 함수들은 기본적으로 비슷하게 동작하기 때문에, 프로젝트 혹은 팀마다 사용 사례가 다를 수 있다. 코드 동작만 목적으로 둔다면 대부분의 경우 아무거나 사용해도 상관없다. 그래서 이 글에서 정리한 것도 "이렇게 용도가 딱 정해져 있다!" 라는 느낌이 아니라 "보통 이럴 때 사용한다"는 느낌으로 정리했다. 가독성에 따른 일종의 컨벤션이라고 볼 수 있다.
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
}
apply는 보통 객체를 생성할 때 해당 객체의 프로퍼티에 값을 할당하는 데 사용한다. 위의 예시에서도 age와 city에 값을 할당하였다. apply는 "다음 할당을 객체에 적용한다(apply the following assignments to the object.)" 이렇게 읽혀질 수 있기 때문이다.
fun main() {
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") }
.add("four")
}
also는 보통 객체의 프로퍼티 혹은 함수가 필요할 때가 아닌 객체 그 자체를 참조해야 할 때 사용한다. 위의 예시에서도 println() 문을 보면 ${it.size} 이런 식으로 객체의 프로퍼티를 참조하지 않았고, $it으로 객체 그 자체만 참조하였다. 실제 사용될 때는 보통 로깅 작업 같은 작은 추가적인 작업을 할 때 사용한다.
코드를 읽을 때, "그리고 또한 이 객체에 다음 작업을 수행한다(and also do the following with the object.)"로 읽을 수 있다.
fun main() {
val list = listOf<String?>("Hello", null, "Hello", null, "Hello")
val lengthList = mutableListOf<Int?>()
list.forEach { element ->
val length = element?.let {
println("element is not null : $it")
it.length
}
lengthList.add(length)
}
println(lengthList)
}
let은 보통 null체크를 하여 non-null 객체에 대한 작업을 진행하거나, 객체를 다른 형태로 변환(맵핑)할 때 사용한다. 위의 예시에서 두 가지 사례를 모두 볼 수 있다. element?.let을 통하여 null이 아닌 경우에만 작업을 진행하도록 하였고, it.length를 반환하여 String을 Int형태로 변환하였다.
class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}
fun main() {
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
println(result)
println(letResult)
}
사실상 컨텍스트 객체를 this로 참조한다는 점 빼고는 let과 똑같다. 람다 인자 it을 사용할건지 말건지에 따라서 run을 사용할지 let을 사용할지를 결정하면 된다. 언제 it을 사용해야 할지 사용 사례는 이전 글의 this 대신 it을 사용해야 할 때 부분을 참고하자.
확장 함수가 아닌 run은 표현식(expression: 적어도 하나의 값을 리턴하는 코드)이 필요한 곳에서 여러 명령문(statement: 무언가를 수행하지만 값을 리턴하지 않는 코드)을 실행할 수 있게 해준다. 확장 함수가 아닌 run은 "이 코드 블록을 실행(run)하고 결과를 계산한다(run the code block and compute the result.)"라는 의미로 이해할 수 있다.
val hexNumberRegex = run {
val digits = "0-9"
val hexDigits = "A-Fa-f"
val sign = "+-"
Regex("[$sign]?[$digits$hexDigits]+")
}
방금 설명은 공식문서에 있는 내용을 번역한 것이라서 무슨 말인지 잘 감이 안 올 수도 있다. 위의 예시를 보면서 이해해보자. Regex 객체를 반환하기 위한 전체 블록은 바로 표현식(expression)이다. 하지만 그 표현식을 작성하기 위해서 작성한 val digits = "0-9", val hexDigits = "A-Fa-f", val sign = "+-" 이것들은 명령문(statement)이다.
쉽게 말해서, 어떤 값을 리턴하는 코드를 작성해야 하는데 코드를 여러줄 필요할 때 사용한다고 보면 된다. Top-level 초기화가 필요할 때도 유용하게 사용된다.
object.with() 이렇게 작성하지 않고 with(object) 이렇게 작성한다.fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
}
with 함수는 객체에 대한 함수 호출을 그룹핑할 때 사용한다. "이 객체와 함께, 다음 작업을 수행한다(with this object, do the following.)" 이렇게 읽을 수 있다.
이렇게 쭉 정리를 해보았는데 글의 초반에도 말했듯이, 스코프 함수는 기본적으로 비슷하게 동작하기 때문에 용도에 따른 엄격한 규칙은 없다. 하지만 가독성과 상황에 따른 컨벤션이 존재하니, 이에 맞춰서 잘 사용하도록 하자. 공식문서를 참고하는 것도 좋지만, 바쁠 때는 Cheat Sheet를 참고하는 것도 좋을 것 같다.