004 Kotlin Scoping Functions

Kotlin Scoping Functions apply vs. with, let, also, and run #

apply, with, let, also, run #

img.png

Kotlin의 Receiver #

객체 외부의 람다 코드 블록을 마치 해당 객체 내부에서 사용하는 것 처럼 작성할 수 있게 해주는 장치

block : T.() -> R

위 람다 블록은 객체 T를 receiver로 이용하여 객체 R을 반환한다.

  1. receiver : 객체 T
  2. receiver를 사용하는 람다 : lambda with receiver
block : (T) -> R

위의 경우 객체 T를 리시버가 아니라 람다 파라미터로 받는다.

범위 지정 함수란? #

  1. 수신객체
  2. 수신객체 지정 람다 (lambda with receiver)
  • 람다 식 내에서 수신 객체의 멤버에 직접 접근할 수 있게 하는 기능

with #

inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}
  1. 수신객체 : receiver T
  2. 수신 객체 지정 람다 : 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
}
  1. T 의 확장함수로 수신 객체가 암시적으로 제공
  2. 수신 객체 지정 람다 : 매개변수 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 차이점 #

  1. 호출시에 수신 객체가 매개 변수로 명시적으로 전달되거나 수신 객체의 확장 함수로 암시적 수신 객체 로 전달
  2. 수신 객체 지정 람다 에 전달되는 수신 객체가 명시적 매개 변수 로 전달 되거나 수신 객체의 확장 함수로 암시적 수신 객체로 코드 블록 내부로 전달
  3. 범위 지정 함수의 결과로 수신 객체를 그대로 반환하거나 수신 객체 지정 람다 의 실행 결과를 반환
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()
}

img.png

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를 쓸지 결정하는 것이다.

  1. it
  • also는 객체를 람다 아규먼트로 받기 때문에 객체에 접근할 때 it(혹은 내가 정의한 다른 이름)을 사용
    • 이는 코드가 객체 외부에서 해당 객체에 접근한다는 인상을 강하게 준다.
  • 객체를 외부에서 접근하는 느낌을 주기 때문에 해당 객체와 더불어(혹은 이용해서) 어떠한 행위를 수행하고자 할 때 쓰인다.
person.also {
    println("my name is ${it.steven}")
}
  1. 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
}