Mobile/Kotlin
[Kotlin]의 apply, with, also, let, run 구분
[Ellie]
2020. 3. 24. 17:40
Kotlin 표준 라이브러리는 다양하고 편리한 기능으로 함수형 프로그래밍을 쉽게 적용할 수 있도록 도와준다.
그 중에서 apply, with, also, let, run가 있는데 이는 서로 비슷하면서 다르기 때문에 적절한 상황에 잘 사용해야 한다. 어떤 상황에서 어느 함수를 사용하면 좋은지 구분하기 위해 정리를 해 보았다.
먼저, 아래와 같이 Kotlin의 함수를 구분하는 유명한 표가 있다.
처음 위의 표를 봤을 때는 이해가 잘 되지 않았지만 아래 5가지 함수의 정의를 보고 의미를 이해할 수 있었다.
// with - 호출 시 수신 객체 T가 parameter로 명시적 전달
// 코드 블럭으로 수신 객체 T가 receiver로 암시적 전달 & 코드 수행 결과 반환
inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
// also - 호출 시 수신 객체 T가 receiver로 암시적 전달
// 코드 블럭으로 수신 객체 T가 parameter로 명시적 전달 & 수신 객체 반환
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
// apply - 호출 시 수신 객체 T가 receiver로 암시적 전달
// 코드 블럭으로 수신 객체 T가 receiver로 암시적 전달 & 수신 객체 반환
inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
// let - 호출 시 수신 객체 T가 receiver로 암시적 전달
// 코드 블럭으로 수신 객체 T가 parameter로 명시적 전달 & 코드 수행 결과 반환
inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
// run - 호출 시 수신 객체 T가 receiver로 암시적 전달
// 코드 블럭으로 수신 객체 T가 receiver로 암시적 전달 & 코드 수행 결과 반환
inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
사용 규칙
그렇다면 이제 어느 상황에 어떤 함수를 사용하는 것이 적합한지 사용 규칙을 알아보자.
[with]
- 객체가 Non-nullable이고 결과가 필요하지 않은 경우
- 같은 객체를 여러 번 호출할 때 호출부가 길어지고 복잡해지는 것을 해결할 수 있다.
ex) 데이터베이스 Insert 시 ContentValues()에 컬럼에 대한 값 세팅
val memoValues = ContentValues()
with(memoValues) {
put(MemoEntry._ID, memo.id)
put(MemoEntry.TITLE_COL, memo.title)
put(MemoEntry.CONTENT_COL, memo.content)
}
[also]
- 객체의 속성을 변경하지 않고 사용하는 경우 (자기 자신 return)
ex) 객체의 side effect 확인, 객체 프로퍼티의 데이터 유효성 검사
class Book(author: Person) {
val author = author.also {
requireNotNull(it.age)
print(it.name)
}
}
[apply]
- 내부 메서드를 사용하지 않고 프로퍼티만을 사용하는 경우 (자기 자신 return)
ex) 인스턴스 초기화
val peter = Person().apply {
name = "Peter"
age = 18
}
[let]
- 지정된 값이 null이 아닌 경우에 코드를 실행
- Nullable 객체를 다른 nullable 객체로 변환
- 단일 지역 변수의 범위 제한
ex)
getNullablePerson()?.let {
// null 이 아닐때만 실행
promote(it)
}
val driversLicence: Licence? = getNullablePerson()?.let {
// nullable personal객체를 nullable driversLicence 객체로 변경
licenceService.getDriversLicence(it)
}
getPersonDao().let {
// 변수 person 의 범위를 이 블록 안으로 제한
val person: Person = getPerson()
it.insert(person)
}
[run]
- 어떤 값을 계산할 필요가 있거나 여러 개의 지역변수의 범위를 제한 (수행 결과 return)
- 매개 변수로 전달된 명시적 수신객체 를 암시적 수신 객체로 변환
ex)
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을 사용하지 말고 명시적인 이름을 사용해 혼동을 줄인다.
-
Chaining이 가능한 경우 코드 가독성도 향상되기 때문에 사용하는 것이 좋다.
ex) chaning 코드
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
}
[참고한 사이트]