코틀린 의 apply, with, let, also, run 은 언제 사용하는가?
원문 : “Kotlin Scoping Functions apply vs. with, let, also, and run”
medium.com
원문 : “Kotlin Scoping Functions apply vs. with, let, also, and run”
https://medium.com/@fatihcoskun/kotlin-scoping-functions-apply-vs-with-let-also-run-816e4efb75f5
Kotlin Scoping Functions apply vs. with, let, also, and run
Functional-style programming is highly advocated and supported by Kotlin’s syntax as well as a range of functions in Kotlin’s standard…
medium.com
코틀린의 표준 라이브러리는 다양한 기능 과 편리한 기능으로 함수형 프로그래밍을 쉽게 적용할 수 있도록 도와줍니다.
그중에서도 apply, with, let, also, run 함수들을 얼마나 잘 사용하고 계신가요?
이 5개의 함수는 전달받는 인자와 작동 방식, 결과가 매우 비슷하기 때문에 많은 경우에 서로를 대체 해서 사용할수도 있습니다.
이런 특성 때문에 상황에 따라 어떤 함수를 사용하는것이 적절한 사용법인지 고민하게 됩니다.
이 글에서는 우선 이 5개의 범위 지정 함수의 공통점과 차이점을 알아보겠습니다. 그리고 알아본 특징을 바탕으로 각 함수들을 언제 사용할지 에 대한 규칙을 정해서 학습해 봅시다.
범위 지정 함수는 무엇을 하는가?
이 5가지 함수는 기본적으로 매우 비슷한 기능을 합니다.
이 함수들은 두가지 구성 요소를 가집니다.
- 수신 객체
- 수신 객체 지정 람다 (lambda with receiver)
먼저 with 가 어떻게 동작 하는지 살펴 보겠습니다. with 는 다음과 같이 정의됩니다.
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
정의에서 receiver 가 수신 객체 , block 이 수신 객체 지정 람다 입니다.
이를 사용하여 코드를보다 간결하게 만들 수 있습니다. 먼저 범위 지정 함수를 사용하지 않는 일반 코드를 살펴 보겠습니다.
class Person {
var name: String? = null
var age: Int? = null
}val person: Person = getPerson()
print(person.name)
print(person.age)
다음 코드는 person 의 중복 사용을 제거하기 위해 범위 지정 함수 with 를 사용한다는 점을 제외하고는 위와 동일합니다.
val person: Person = getPerson()
with(person) {
print(name)
print(age)
}
with 는 많은 케이스에서 매우 유용합니다. 그런데 왜 우리는 비슷한 역할을 하는 다섯 가지의 함수가 필요 할까요?
apply, with, let, also, run 의 차이점
이러한 함수는 매우 유사한 기능을 수행하지만 함수의 정의 와 구현에 중요한 차이가 있습니다.
이러한 차이점이 각각의 함수가 어떻게 사용 되어야 하는지를 결정합니다.
with 와 also 가 어떻게 다른지 비교해 보도록 하겠습니다. also 는 다음과 같이 정의됩니다.
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
with 와 also 는 다음의 차이점을 갖습니다.
- 범위 지정 함수 호출 시에 수신 객체가 어떻게 전달 되는가?
- with 에서는 수신 객체가 매개 변수 T 로 제공됩니다. 이를 명시적으로 제공된 수신 객체 라고 합니다.
- also 에서는 T 의 확장함수로 수신 객체가 암시적으로 제공됩니다.
2. 범위 지정 함수 에 전달된 수신객체가 다시 수신 객체 람다에 어떠한 형식으로 전달할것인가?
- with 는 수신 객체 지정 람다 가 T 의 확장함수 형태로 코드 블록 내에 수신 객체가 암시적으로 전달 됩니다.
- also 는 수신 객체 지정 람다에 매개변수 T 로 코드 블록 내에 명시적으로 전달 됩니다.
3. 범위 지정 함수 의 최종적인 반환 값이 무엇인가?
- with 는 람다를 실행한 결과를 반환 합니다.
- also 는 코드 블록 내에 전달된 수신객체를 그대로 다시 반환 합니다.
이러한 3 가지 차이 때문에 also 는 with 와는 다른 방식으로 사용해야 합니다.
val person: Person = getPerson().also {
print(it.name)
print(it.age)
}
이 코드는 getPerson() 함수를 사용하여 사람을 검색하고 person 변수에 할당합니다. person 변수에 할당하기 전에 also 는 검색된 사람의 이름과 나이를 출력 합니다.
즉, with, also, apply, let, run 은 아래의 3 가지 차이점 중 1 가지가 서로 다릅니다.
- 범위 지정 함수 의 호출시에 수신 객체가 매개 변수로 명시적으로 전달되거나 수신 객체의 확장 함수로 암시적 수신 객체 로 전달된다.
- 범위 지정 함수 의 수신 객체 지정 람다 에 전달되는 수신 객체가 명시적 매개 변수 로 전달 되거나 수신 객체의 확장 함수로 암시적 수신 객체로 코드 블록 내부로 전달 된다.
- 범위 지정 함수의 결과로 수신 객체를 그대로 반환하거나 수신 객체 지정 람다 의 실행 결과를 반환한다.
다음은 5 가지 함수의 정의 입니다.
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
이러한 차이점을 기억하기 어려울 수 있습니다.
다음의 표는 각 함수의 차이점을 보여줍니다. 이 표 를 인쇄해서 참고 하는것도 방법입니다.

apply, with, let, also, run 은 언제 사용해야 하는가?
이제 이 다섯 가지 함수가 서로 무엇이 다른지 알게되었습니다.
그러나 여전히 이 함수들은 비슷해 보이고, 실제로도 서로 많은 케이스에서 교환하여 사용이 가능하므로 어느 함수를 어디에 사용해야 하는지 판단하기는 어렵습니다.
코틀린 공식 문서에는 이 다섯 가지 함수에 대한 몇 가지 모범 사례와 규칙이 있습니다.
이러한 규칙을 학습하면 더 많은 관용구 코드를 작성할 수 있으며 다른 개발자 코드의 의도를 더 빨리 이해하는 데 도움이 됩니다.
apply 사용 규칙
수신 객체 람다 내부에서 수신 객체의 함수를 사용하지 않고 수신 객체 자신을 다시 반환 하려는 경우에 apply 를 사용합니다.
수신 객체 의 프로퍼티 만을 사용하는 대표적인 경우가 객체의 초기화 이며, 이곳에 apply 를 사용합니다.
val peter = Person().apply {
// apply 의 블록 에서는 오직 프로퍼티 만 사용합니다!
name = "Peter"
age = 18
}
apply 를 사용하지 않는 동일한 코드는 다음과 같습니다.
val clark = Person()
clark.name = "Clark"
clark.age = 18
also 사용 규칙
수신 객체 람다가 전달된 수신 객체를 전혀 사용 하지 않거나 수신 객체의 속성을 변경하지 않고 사용하는 경우 also 를 사용합니다.
also 는 apply 와 마찬가지로 수신 객체를 반환 하므로 블록 함수가 다른 값을 반환 해야하는 경우에는 also 를 사용할수 없습니다.
예를 들자면, 객체의 사이드 이팩트를 확인하거나 수신 객체의 프로퍼티에 데이터를 할당하기 전에 해당 데이터의 유효성을 검사 할 때 매우 유용합니다.
class Book(author: Person) {
val author = author.also {
requireNotNull(it.age)
print(it.name)
}
}
also 를 사용하지 않는 동일한 코드는 다음과 같습니다.
class Book(val author: Person) {
init {
requireNotNull(author.age)
print(author.name)
}
}
let 사용 규칙
다음과 같은 경우에 let 을 사용합니다.
- 지정된 값이 null 이 아닌 경우에 코드를 실행해야 하는 경우.
- Nullable 객체를 다른 Nullable 객체로 변환하는 경우.
- 단일 지역 변수의 범위를 제한 하는 경우.
getNullablePerson()?.let {
// null 이 아닐때만 실행됩니다.
promote(it)
}val driversLicence: Licence? = getNullablePerson()?.let {
// nullable personal객체를 nullable driversLicence 객체로 변경합니다.
licenceService.getDriversLicence(it)
}val person: Person = getPerson()
getPersonDao().let { dao ->
// 변수 dao 의 범위는 이 블록 안 으로 제한 됩니다.
dao.insert(person)
}
let 을 사용하지 않는 동일한 코드는 다음과 같습니다.
val person: Person? = getPromotablePerson()
if (person != null) {
promote(person)
}val driver: Person? = getDriver()
val driversLicence: Licence? = if (driver == null) null else
licenceService.getDriversLicence(it)val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
personDao.insert(person)
with 사용 규칙
Non-nullable (Null 이 될수 없는) 수신 객체 이고 결과가 필요하지 않은 경우에만 with 를 사용합니다.
val person: Person = getPerson()
with(person) {
print(name)
print(age)
}
with 를 사용하지 않는 동일한 코드는 다음과 같습니다.
val person: Person = getPerson()
print(person.name)
print(person.age)
run 사용 규칙
어떤 값을 계산할 필요가 있거나 여러개의 지역 변수의 범위를 제한하려면 run 을 사용합니다.
매개 변수로 전달된 명시적 수신객체 를 암시적 수신 객체로 변환 할때 run ()을 사용할수 있습니다.
val inserted: Boolean = run {
// person 과 personDao 의 범위를 제한 합니다.
val person: Person = getPerson()
val personDao: PersonDao = getPersonDao() // 수행 결과를 반환 합니다.
personDao.insert(person)
}fun printAge(person: Person) = person.run {
// person 을 수신객체로 변환하여 age 값을 사용합니다.
print(age)
}
run 을 사용하지 않는 동일한 코드는 다음과 같습니다.
val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
val inserted: Boolean = personDao.insert(person)fun printAge(person: Person) = {
print(person.age)
}
여러 범위 지정 함수 결합
코드 가독성을 향상시키기 위해 범위 지정 기능을 분리하여 어떻게 사용할 수 있는지 보여주었습니다.
하나의 코드 블록 내에서 여러 범위 지정 함수를 중첩 하려는 경우가 종종 있습니다. 그러나 범위 지정 함수가 중첩되면 코드의 가독성이 떨어지고 파악하기 어려워 집니다.
원칙적으로 중첩은 하지 않는 것이 좋습니다.
수신객체 지정 람다 에 수신 객체가 암시적으로 전달되는 apply, run, with 는 중첩하지 마십시오.
이 함수들은 수신 객체를 this 또는 생략하여 사용하며, 수신객체의 이름을 다르게 지정할수 없기 때문에 중첩될 경우 혼동 하기 쉬워집니다.
also 와 let 을 중첩 해야만 할때는 암시적 수신 객체 를 가르키는 매개 변수 인 it 을 사용하지 마십시오. 대신 명시적인 이름을 제공하여 코드 상의 이름이 혼동되지 않도록 해야 합니다.
범위 지정 함수를 호출 체인에 결합 할 수 있습니다.
중첩 과는 달리 범위 지정 기능을 호출 체인에 결합하면 코드의 가독성이 향상됩니다.
호출 체인에서 범위 지정 함수를 결합하는 예 를 살펴 보겠습니다.
private fun insert(user: User) = SqlBuilder().apply {
append("INSERT INTO user (email, name, age) VALUES ")
append("(?", user.email)
append(",?", user.name)
append(",?)", user.age)
}.also {
print("Executing SQL update: $it.")
}.run {
jdbc.update(this) > 0
}
위 예시는 사용자를 데이터베이스에 삽입하기위한 DAO 기능을 보여줍니다.
SQL 준비, SQL 로그 출력 및 SQL 실행과 같은 구현을 범위 지정 함수로 분리합니다. 마지막으로 이 함수는 삽입 성공을 나타내는 boolean 값을 반환합니다.
15. run/let, apply/also 함수
▷ scope 함수를 사용하면 class의 instance 속성이나 함수를 좀더 깔끔하게 불러 쓸 수 있음
· scope function에는 apply, run, with, also, let 의 5가지가 있음
· run/let : 최종 결과값을 반환, let은 alias 가능
· apply/also : 중간 값을 반환, also는 alias 가능, with은 …
· apply : instance를 생성한 후 변수에 담기 전에 '초기화 과정'을 수행할때 많이 쓰임
▷ 위 함수는 class를 정의하고 초기화, 변수 재사용, 함수 재사용 등을 통해 코드의 가독성을 높이는데 유용하게 사용됨
-------------------------------------------------
fun main () {
var a = Book("kotlin자습서", 5000)
a.printBook()
a.discount()
// 아래와 같이 하면 위 보다 코드가 깔끔 해짐
var b = Book("kotlin", 5000).apply {
name = name +"__study"
printBook()
discount()
}
// run/let은 람다함수 처럼 마지막 구문의 결과 값을 반환
// 이미 instance가 만들어진 후에 instance의 함수나 속성을 scope 내에서 사용해야 할때
b.run {
println("책이름 : ${name}, 가격(할인) : ${price}")
}
// with은 run과 동일하나 instance를 참조연산자 대신 파라미터로 받음
with(b){
println("책이름 : ${name}, 할인가격: ${price}")
}
// apply/also, let은 run, with과 같은 기능, main 외부의 class에서 갖는 값을 우선해서 사용
// 만약 main 내에서 price값을 지정하면 run, with는 main 내에서 지정한 price를 갖다 쓰나
// also, let을 it.price로 하면 class에서 지정한 값을 사용해서 출력함
var price = 10000
var name = "also Test"
a.run {
println("책이름 : ${name}, 가격(할인) : ${price}")
}
a.also {
println("책이름 : ${it.name}, 가격(할인) : ${it.price}")
}
}
class Book (var name: String, var price: Int) {
fun printBook() {
println("이책의 이름은 ${name}이고, 가격은 ${price} 입니다.")
}
fun discount (){
price -= 2000
println("할인된 가격은 ${price} 입니다.") } }
======================================= 다른 곳에서 정의 한 것 =============
좀 햇갈림.... 추가 정리 필요
https://kotlinlang.org/docs/scope-functions.html#function-selection
Scope functions | Kotlin
kotlinlang.org
scope functions : run, let, apply, also and with
run(this) / let(it) => 자기 자신을 반환(적용한 내용을 반환 => 변수에 담을 수 있음)
apply(this), also(it) => 객체를 반환(print)
* 일반적으로 this는 안써도 됨
Basically, these functions do the same: execute a block of code on an object. What's different is how this object becomes available inside the block and what is the result of the whole expression.
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
=================================
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
Function selection
To help you choose the right scope function for your purpose, we provide the table of key differences between them.
let | it | Lambda result | Yes |
run | this | Lambda result | Yes |
run | - | Lambda result | No: called without the context object |
with | this | Lambda result | No: takes the context object as an argument. |
apply | this | Context object | Yes |
also | it | Context object | Yes |
The detailed information about the differences is provided in the dedicated sections below.
Here is a short guide for choosing scope functions depending on the intended purpose:
- Executing a lambda on non-null objects: let
- Introducing an expression as a variable in local scope: let
- Object configuration: apply
- Object configuration and computing the result: run
- Running statements where an expression is required: non-extension run
- Additional effects: also
- Grouping function calls on an object: with
The use cases of different functions overlap, so that you can choose the functions based on the specific conventions used in your project or team.
Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease your code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them: it's easy to get confused about the current context object and the value of this or it.
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()
numberList.forEach{ print("$it ,")}
'kotlin' 카테고리의 다른 글
kotlin extension Function (0) | 2023.03.27 |
---|---|
Kotlin Collection Functions Cheat Sheet (0) | 2023.03.27 |
kotlin - lambda (0) | 2023.02.13 |
kotlin Array (0) | 2023.02.07 |
kotlin Cast - 형변환 (0) | 2023.02.07 |