Kotlin Scoping Functions apply vs. with, let, also, and run #
apply, with, let, also, run #
Kotlin의 Receiver #
객체 외부의 람다 코드 블록을 마치 해당 객체 내부에서 사용하는 것 처럼 작성할 수 있게 해주는 장치
block : T.() -> R
위 람다 블록은 객체 T를 receiver로 이용하여 객체 R을 반환한다.
receiver
: 객체 Treceiver
를 사용하는 람다 : lambda with receiver
block : (T) -> R
위의 경우 객체 T를 리시버가 아니라 람다 파라미터로 받는다.
범위 지정 함수란? #
- 수신객체
- 수신객체 지정 람다 (lambda with receiver)
- 람다 식 내에서 수신 객체의 멤버에 직접 접근할 수 있게 하는 기능
with #
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
- 수신객체 : receiver T
- 수신 객체 지정 람다 : block
Before
class Person {
var name: String? = null
var age: Int? = null
}
val person: Person = getPerson()
print(person.name)
print(person.age)
After
val person: Person = getPerson()
with(person) {
print(name)
print(age)
}
also #
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
- T 의 확장함수로 수신 객체가 암시적으로 제공
- 수신 객체 지정 람다 : 매개변수 T 로 코드 블록 내에 명시적으로 전달
Before
class Person {
var name: String? = null
var age: Int? = null
}
val person: Person = getPerson()
print(person.name)
print(person.age)
After
val person: Person = getPerson().also {
print(it.name)
print(it.age)
}
with, also, apply, let, run 차이점 #
- 호출시에 수신 객체가 매개 변수로 명시적으로 전달되거나 수신 객체의 확장 함수로 암시적 수신 객체 로 전달
- 수신 객체 지정 람다 에 전달되는 수신 객체가 명시적 매개 변수 로 전달 되거나 수신 객체의 확장 함수로 암시적 수신 객체로 코드 블록 내부로 전달
- 범위 지정 함수의 결과로 수신 객체를 그대로 반환하거나 수신 객체 지정 람다 의 실행 결과를 반환
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 사용 규칙 #
- 수신 객체 람다 내부에서 수신 객체의 함수를 사용하지 않고 수신 객체 자신을 다시 반환 하려는 경우
Before
val clark = Person()
clark.name = "Clark"
clark.age = 18
After
val peter = Person().apply {
// apply 의 블록 에서는 오직 프로퍼티 만 사용합니다!
name = "Peter"
age = 18
}
also 사용 규칙 #
- 수신 객체 람다가 전달된 수신 객체를 전혀 사용 하지 않거나 수신 객체의 속성을 변경하지 않고 사용하는 경우
- 수신 객체를 반환 하므로 블록 함수가 다른 값을 반환 해야하는 경우 사용 불가능
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
Before
class Book(val author: Person) {
init {
requireNotNull(author.age)
print(author.name)
}
}
After
class Book(author: Person) {
val author = author.also {
requireNotNull(it.age)
print(it.name)
}
}
apply 와 also : 리시버와 파라미터의 차이 #
public inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
- also : 객체를 람다 파라미터로 받는다.
- apply : 객체를 리시버로 받는다.
class person {
var name = "kotlin"
private val id = "1541"
}
person.also {
println("my name is ${it.name}")
}
person.apply {
println("my name is $name")
}
- also : it을 사용한다.
- apply : this를 사용한다.
it vs this #
내가 작성하고자 하는 코드의 의미(semantics) 에 따라 also를 쓸지 apply를 쓸지 결정하는 것이다.
- it
- also는 객체를 람다 아규먼트로 받기 때문에 객체에 접근할 때 it(혹은 내가 정의한 다른 이름)을 사용
- 이는 코드가 객체 외부에서 해당 객체에 접근한다는 인상을 강하게 준다.
- 객체를 외부에서 접근하는 느낌을 주기 때문에 해당 객체와 더불어(혹은 이용해서) 어떠한 행위를 수행하고자 할 때 쓰인다.
person.also {
println("my name is ${it.steven}")
}
- this
- apply 는 객체를 람다 리시버로 받기 때문에 객체에 접근할 때 this(혹은 생략)을 사용
- 코드가 해당 객체의 외부가 아니라 객체 내부에 있는듯한 인상을 준다.
- apply코드 블록이 객체 내부에 있는 듯한 느낌을 주기 때문에 주로 객체를 초기화 하는 코드 혹은 객체의 상태를 변경하는 코드에 많이 사용된다.
person.apply {
name = "steven"
age = 21
}
let 사용 규칙 #
- 지정된 값이 null 이 아닌 경우에 코드를 실행해야 하는 경우
- Nullable 객체를 다른 Nullable 객체로 변환하는 경우
- 단일 지역 변수의 범위를 제한 하는 경우
inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
Before
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)
After
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)
}
with 사용 규칙 #
- Non-nullable (Null 이 될수 없는) 수신 객체 이고 결과가 필요하지 않은 경우에만 with 를 사용
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
Before
val person: Person = getPerson()
print(person.name)
print(person.age)
After
val person: Person = getPerson()
with(person) {
print(name)
print(age)
}
run 사용 규칙 #
- 어떤 값을 계산할 필요가 있거나 여러개의 지역 변수의 범위를 제한하려면 run 을 사용
- 매개 변수로 전달된 명시적 수신객체 를 암시적 수신 객체로 변환 할때 run ()을 사용
inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
Before
val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
val inserted: Boolean = personDao.insert(person)
fun printAge(person: Person) = {
print(person.age)
}
After
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)
}
여러 범위 지정 함수 결합 #
하나의 코드 블록 내에서 여러 범위 지정 함수를 중첩하지 않는 것이 좋다. 수신객체 지정 람다 에 수신 객체가 암시적으로 전달되는 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
}