[Kotlin] Scope 함수 (let, run, with, apply, also)
Scope 함수
코틀린 표준 라이브러리는 객체의 컨텍스트 내에서 코드 블럭(익명 함수)을 실행하기 위한 목적을 가진 여러가지 함수를 제공합니다.
이런 함수들은 람다식으로 호출 할 수 있고, 이는 임시로 범위(scope)를 형성합니다.
해당 범위 내에서는 객체의 이름이 없어도 객체에 접근할 수 있습니다.
이러한 함수를 Scope funtions라고 합니다. 스코프 함수의 종류는 아래와 같습니다.
- let
- run
- with
- apply
- also
기본적으로, 이 함수들은 동일한 역할을 수행합니다.
다른점이 있다면, scope 내에서 객체를 어떤 방식으로 호출하는지, 리턴 값을 어떻게 처리하는지입니다.
아래는 Person class가 있을 때 scope function의 사용법입니다.
Person.kt
data class Person(
var name: String,
var age: Int = 0,
var address: String = ""
) {
fun moveTo(address: String) {
this.address = address
}
fun incrementAge() {
this.age += 1
}
}
Person("Charming", 20, "Seoul").let {
println(it) // Person(name=Charming, age=20, address=Seoul)
it.moveTo("Pangyo")
it.incrementAge()
println(it) // Person(name=Charming, age=21, address=Pangyo)
}
만약 let 없이 동일한 내용의 코드를 작성한다면, 아래의 코드처럼 작성해야할 것입니다.
val person = Person("Charming", 20, "Seoul")
println(person) // Person(name=Charming, age=20, address=Seoul)
person.moveTo("Pangyo")
person.incrementAge()
println(person) // Person(name=Charming, age=21, address=Pangyo)
Person 객체를 사용할때마다 변수의 이름을 반복해서 작성하게 됩니다.
scope 함수는 코틀린만의 새로운 기술이 적용된 것이 아닌,
코드를 좀 더 간결하고 읽기 쉽게 만들어주는 것입니다.
scope 함수들의 비슷한 점들로 인해,
특정 상황에 딱 맞아 떨어지는 함수를 사용하는 것이 모호할 수 있습니다.
이 때 작성하고자 하는 코드의 의도와 프로젝트의 컨벤션으로 사용할 함수를 쉽게 결정할 수 있습니다.
아래에서 scope 함수와 각각의 차이점에 대해 상세히 살펴보겠습니다.
Scope 함수 간 차이점
scope 함수는 본질이 비슷하기 때문에, 각각의 차이점을 잘 아는 것이 중요합니다.
각 scope 함수에는 두 가지 큰 차이점이 있습니다.
- 객체의 컨텍스트를 참조하는 방식
- 리턴 값
컨텍스트를 참조하는 방식 : this 혹은 it
scope 함수의 익명 함수(람다식) 내에서 객체는 실제 이름 대신 짧은 참조 이름으로 사용할 수 있습니다.
각각의 scope 함수에서 객체에 접근하기 위해 다음 두 가지 방법 중 하나를 사용합니다.
- 람다 수신자 (this)
- 람다 인자 (it)
둘 다 동일한 역할을 하는데,
상황 별로 각각의 장점과 단점에 대해 알아보고 권장하는 방식을 알아보겠습니다.
// this
"Hello".run {
println("The receiver string length: $length")
println("The receiver string length: ${this.length}")
// 위 두 줄의 코드는 동일한 역할을 합니다.
}
// it
"Hello".let { println("The receiver string's length is ${it.length}") }
this
run, with, apply 는 this 키워드로 람다 수신자로서 컨텍스트 객체를 참조합니다.
그러므로, 이 람다에서 객체는 일반 객체 함수로도 사용이 가능합니다.
대부분의 경우, 수신 객체의 멤버에 접근할 때 this 를 생략함으로써 코드를 간략하게 작성할 수 있습니다.
반면, this 가 생략되면, 수신 객체 멤버와 외부 변수를 구분하기 어려울 수 있습니다.
따라서 수신자(this)로 컨텍스트 객체를 받는 scope 함수는 주로 객체의 함수를 호출하거나,
프로퍼티를 할당하는 것과 같이 객체 멤버를 처리할 때 사용하는 것을 권장합니다.
val charming =
Person("Charming").apply {
age = 20 // this.age = 20 과 charming.age = 20 전부 동일한 역할을 합니다.
address = "Seoul"
}
it
let, also 는 객체를익명 함수 매개변수로 가집니다.
만약 인자의 이름을 정하지 않았다면, 묵시적으로 기본 이름인 it 으로 접근할 수 있습니다.
it 은 this 보다 짧으며, 주로 it 을 사용한 표현식은 읽기 쉽습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also {
println("getRandomInt() generated value $it")
}
}
val randomInt = getRandomInt()
또한, 익명 함수의 매개변수를 지정함으로써
컨텍스트 객체의 커스텀한 이름을 설정할 수 있습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
println("getRandomInt() generated value $value")
}
}
val random = getRandomInt()
리턴 값
scope 함수들은 리턴 값이 다릅니다.
- apply 와 also 는 컨텍스트 객체(this)를 리턴합니다.
- let, run, with 는 익명 함수(람다식)의 결과를 리턴합니다.
이 옵션들은 다음에 어떤 코드를 작성할 것인지에 따라 적절한 함수를 선택할 수 있습니다.
컨텍스트 객체(this)를 리턴하는 함수
apply 와 also 의 리턴 값은 컨텍스트 객체 그 자신(this)입니다.
그러므로 apply 와 also 는 체이닝 함수로도 활용할 수 있습니다.
val numberList =
mutableListOf(6, 5, 4, 3)
.also { println("push the list") }
.apply {
add(7)
add(8)
add(9)
}
.also { println("sort the list") }
.sort()
.also { println(this) }
익명 함수(람다식)의 결과를 리턴하는 함수
let, run, with 는 익명 함수의 결과를 반환합니다.
따라서 이 scope 함수들은 변수에 연산 결과를 할당할 때나,
연산 결과에 연산을 연결할 때 등의 상황에서 사용할 수 있습니다.
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.
또한, 변수를 위한 임시 scope를 만들기 위해 리턴 값을 무시하고 scope 함수를 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
// First item: one, last item: three
}
적절한 scope 함수
상황에 맞는 scope 함수를 선택할 수 있도록 권장하는 방식의 사용 예시와 함께 설명드리겠습니다.
사실 많은 상황에서 scope 함수는 교체될 수 있습니다.
따라서 아래의 예시에서는 일반적인 사용 방식을 선정하였습니다.
let
컨텍스트 객체는 인자(it)으로 사용 가능합니다.
이 컨텍스트 객체의 리턴 값은 람다의 결과입니다.
let 은 호출 체인의 결과로 하나 혹은 그 이상의 함수를 호출할 때 사용됩니다.
예를 들어 다음의 코드는 컬렉션의 두 연산 결과를 출력합니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList) // [5, 4, 4]
같은 기능을 let 으로 아래와 같이 작성할 수 있습니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it) // [5, 4, 4]
// 필요하다면 더 많은 함수를 호출할 수 있습니다.
}
만약 scope 함수 블록에 it 을 인자로 가지는 단일 함수가 있다면
람다 대신 메소드참조(::)를 사용할 수 있습니다.
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println) // [5, 4, 4]
let 은 종종 null이 아닌 상태에서만 코드 블럭을 실행시키는 데에 사용됩니다.
null이 아닌 객체에 동작을 수행하기 위해서는, 해당 객체에 safe call(?.)을 사용하고 let을 호출하면 됩니다.
fun main(str: String?) {
val length =
str?.let {
println("let() called on $it") // str이 존재할 때만 호출 됨
println(it.isNullOrBlank()) // false '?.let { }' 안에서는 'it'이 null이 아님
it.length
}
println(length) // length의 타입은 Int?
}
let 을 사용하는 또 다른 방법은 코드 가독성을 높이기 위해 제한된 scope 내에서 지역 변수를 사용하는 것입니다.
컨텍스트 객체를 이용해 새로운 변수를 선언할 때 주로 사용됩니다.
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem =
numbers
.first()
.let { firstItem ->
println(firstItem) // one
if (firstItem.length >= 5) firstItem else "!$firstItem!"
}
.uppercase(Locale.getDefault())
println(modifiedFirstItem) // !ONE!
with
with 는 비 확장함수이며, 컨텍스트 객체는 익명 함수의 매개변수로 전달되지만,
함수 안에서는 수신자(this)로 사용 가능합니다.
리턴 값은 람다 결과 입니다.
다른 함수와의 차이점은확장 함수 호출 형식이 아닌 일반 함수 호출 형식으로 쓰인다는 점입니다.
with 는 람다 결과 없이 컨텍스트 객체의 호출 함수에서 사용하는 것을 권장합니다.
아래 코드에서 with 는 '이 객체로 다음을 수행하라 (with this object, do the following)' 뜻으로 해석할 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println(this) // [one, two, three]
println(size) // 3
}
with 의 또 다른 사용법은 값을 계산할 때 사용되는
헬퍼 객체 (helper object)의 프로퍼티나 함수를 선언하는데 사용하는 것입니다.
val numbers = mutableListOf("oen", "two", "three")
val firstAndLast = with(numbers) { "${first()} ${last()}" }
println(firstAndLast) // oen three
run
컨텍스트 객체는 수신자(this)로 사용가능합니다.
리턴 값은 람다 결과입니다.
run 은 with 와 같은 역할을 하지만, let 처럼 컨텍스트 객체의 확장 함수처럼 호출합니다.
람다가 객체 초기화와 리턴 값 연산을 모두 포함하고 있을 때 유용합니다.
val service = WebClientService("https://example.com", 80)
val result =
service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
// let() 함수로 쓰인 위와 동일한 코드:
val letResult =
service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
run 은 수신 객체에서 호출하는 것 이외에, 비확장함수로 사용할 수 있습니다.
비확장(non-extention) 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)
// +1234
// -FFFF
// -a
// be
}
apply
컨텍스트 객체는 수신자(this)로 사용할 수 있습니다.
리턴 값은 객체 자기 자신(this)입니다.
apply 는 값을 반환하지 않고, 주로 수신 객체의 멤버를 연산하는 곳에 사용합니다.
일반적인 경우 객체 설정(configuration)입니다.
'다음의 지시를 객체에 적용하라 (apply the following assignments to the object)' 뜻으로 해석할 수 있습니다.
val adam =
Person("Charming").apply {
age = 32
address = "Pangyo"
}
자기 자신(this)를 리턴 값으로 가짐으로써, apply 를 더 복잡한 프로세스를 위한 호출 체인에 쉽게 포함할 수 있습니다.
also
컨텍스트 객체는 인자(it)로 사용 가능합니다.
리턴 값은 객체 자기 자신입니다.
also 는 컨텍스트 객체를 인자로 가지는 것과 같은 작업을 수행하는데 좋습니다.
로깅이나 디버그 정보를 출력하는 것과 같이 객체를 변화시키지 않는 부가적인 작업에 적합합니다.
대부분의 프로그램의 로직을 파괴하지 않고도 호출 체인에서 also 의 호출을 제거하는 것이 가능합니다.
'그리고 또한 다음을 수행하라 (and also do the following)' 뜻으로 해석할 수 있습니다.
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println(it) } // [one, two, three]
.add("four")
함수 선택 (Function Selection)
아래의 표는 목적에 맞게 scope 함수를 선택할 수 있는 차이점을 정리한 표입니다.
Functions | Object Reference |
Return Value |
확장 함수로 호출 |
let | it | 함수 리턴 값 | O |
run | this | 함수 리턴 값 | O |
run | - | 함수 리턴 값 | X (컨텍스트 객체 없이도 호출 가능) |
with | this | 함수 리턴 값 | X (매개변수로 컨텍스트 객체를 사용) |
apply | this | 자기 자신 (this) | Y |
also | it | 자기 자신 (this) | Y |
아래는 의도한 목적에 따라 scope 함수를 선택할 수 있는 간략한 가이드입니다.
널이 아닌 객체에 람다를 실행할 때: let
지역(local) scope에서 표현식(expression)을 변수로 선언할 때: let
객체 설정(configuration): apply
객체설정(configuration)과 결과를 계산할 때: run
표현식이 필요한 곳에 연산을 실행할 때: 비 확장(non-extension) run
부가적인 실행: also
객체에 대한 그룹 함수 호출: with
제일 중요한 점은 scope 함수의 사용 용도가 중복되기때문에,
프로젝트나 팀에서의 구체적인 컨벤션에 따라 어떤 함수를 사용할지 정하시면 좋습니다.
scope 함수는 코드를 더 간결하게 만드는 방법이지만, 남용하는 것은 피해야합니다.
코드 가독성을 떨어트리고 에러를 발생시킬 수 있기 때문입니다.
scope 함수를 중첩하여 사용하는 것은 피해야합니다.
그리고 this 혹은 it 으로 현재의 컨텍스트 객체 값을 혼동하기 쉽기 때문에
연속(chaining)하여 사용할 때는 주의해야합니다.
출처
Scope functions | Kotlin
kotlinlang.org
몇가지 예제 코드는 해당 설명의 특성을 잘 살리지 못한 것 같아서 예제 코드를 수정하였습니다.
해석하면서 개인적으로 느낀점은 코틀린은 정말 함수형 프로그래밍을 위한 언어라는 느낌이 들었습니다.
그렇다고 완전 FP에 치우친 것이 아닌, (고급 언어 + OOP + FP) 세가지의 조화로운 조합을 경험할 수 있었습니다.
[OOP vs FP] 무엇이 더 좋은지 판단하는 두가지의 선택지에서
OOP, FP의 장점만 쏙 빼서 코틀린이라는 언어가 생긴 느낌이라.
저에게는 총 세가지의 선택지가 되었습니다.
'Kotlin' 카테고리의 다른 글
Javascript Promise로 배우는 Kotlin Coroutine (2) | 2022.10.09 |
---|---|
Kotlin 리플렉션(Reflection) (2) | 2022.09.06 |
[Kotlin] companion object, Java static 차이점 (0) | 2022.05.15 |
[Kotlin] coroutine, suspend 함수 정리 및 예제 (0) | 2022.04.19 |
[Kotlin] 확장 함수와 프로퍼티 (0) | 2022.03.20 |
댓글
이 글 공유하기
다른 글
-
Kotlin 리플렉션(Reflection)
Kotlin 리플렉션(Reflection)
2022.09.06 -
[Kotlin] companion object, Java static 차이점
[Kotlin] companion object, Java static 차이점
2022.05.15이 글은 코틀린의 companion object와 Java의 static 키워드의 차이점에 대해 학습한 것을 정리하였습니다. object class 코틀린은 자바에 없는 독특한 싱글톤(Singleton) 선언 방법이 있습니다. 아래처럼 class 키워드 대신 object 키워드를 사용하면 됩니다. object Singleton { val name = "singleton" fun print() = println("hello") } fun main() { println(Singleton.name) // "singleton" Singleton.print() // "hello" val singleton = Singleton() // 에러! } object 키워드로 선언한 속성과 메소드는 static 키워드로 선… -
[Kotlin] coroutine, suspend 함수 정리 및 예제
[Kotlin] coroutine, suspend 함수 정리 및 예제
2022.04.19Coroutine 코루틴이란. 실행의 지연과 재개를 허용함으로서, 비선점 멀티테스킹을 위한 서브루틴을 일반화한 구성요소입니다. 자세히 이해하기 위해 서브루틴과 비선점 멀티테스킹 두가지 개념을 알아야 합니다. 루틴과 서브루틴 루틴은 컴퓨터 프로그램에서 하나의 정리된 일(job)입니다. 프로그램은 보통 크고 작은 여러가지 루틴을 조합시킴으로써 만들어집니다. 루틴은 다시 메인루틴과 서브루틴으로 나뉩니다. 메인루틴은 프로그램 전체의 개괄적인 동작 절차를 표시하도록 만들어 집니다. 서브루틴은 반복되는 특정 기능을 모아 별도로 묶어 이름을 붙여 놓은 것입니다. 그리고 별도의 메모리에 해당 기능을 모아놓고 서브루틴이 호출될 때마다 저장된 메모리로 이동했다가 return 을 통해 원래 호출자의 위치로 돌아오게 됩니다…. -
[Kotlin] 확장 함수와 프로퍼티
[Kotlin] 확장 함수와 프로퍼티
2022.03.20코틀린 확장 함수 코틀린은 기존 클래스에 메소드를 추가할 수 있습니다. 이를 확장 함수라고 합니다. 하지만 기존 클래스에 대해 메소드를 확장적으로 선언을 할 수 있을 뿐, 해당 클래스의 구현부를 바꿀 수는 없습니다. 기존 클래스는 그대로 두고 클래스 주변에 새로운 함수를 추가하여 클래스의 크기를 확장한다. 라고 생각하면 좋을 것 같습니다. String 클래스의 메소드인것 처럼 사용할 수 있는 lastChar 메소드를 확장해보겠습니다. 확장 함수의 선언 방법은 아래와 같습니다. fun String.lastChar():Char = this.get(this.length - 1) // this 생략 가능 fun String.lastChar():Char = get(length - 1) // 사용시 println(…
댓글을 사용할 수 없습니다.