코틀린에는 스코프 함수라는 특이한 기능이 있다.
총 5가지 종류가 있지만 사용법이 비슷해서 헷갈리지만 익혀두면 매우 유용하게 쓰인다.
T
는 리시버, R
는 람다 함수이며 표에는 알아보기 쉽게 하려고 {R}
이라고 썼지만 원래는 괄호 없이 R
만 쓴다.
리시버는 람다 함수에 전달 할 객체를 말하며 람다 함수는 그 객체를 받아 함수 안의 내용을 수행한다.
간단한 예제이다.
fun main() {
val a = "val a"
println("with : " + with("$a?") { "$this!" })
println("run : " + "$a?".run { "$this!" })
println("let : " + "$a?".let { "$it!" })
println("apply : " + "$a?".apply { "$this!" })
println("also : " + "$a?".also { "$it!" })
println("run : " + run { a })
}
//실행 결과
//
//with : val a?!
//run : val a?!
//let : val a?!
//apply : val a?
//also : val a?
//run : val a
여기서 T
는 변수 a
에 ?
를 더한 뒤 R
에 전달해 준다.
R
은 전달 받은 val a?
에 !
를 더한다.
with, run, let
은 반환값이 R
이기 때문에 val a
에 물음표가 붙은 상태로 람다 함수로 전달된 뒤 느낌표가 붙어서 출력됐다.
하지만 apply, also
는 반환값이 T
이기 때문에 람다 함수 안의 코드 결과와 관계 없이 물음표만 붙어서 출력됐다.
let, run은 사용 방법이 거의 비슷하다.
우선 let을 살펴보자.
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
inline, contract, callsInPlace 등등 무슨 말인지 잘 모르는 게 너무 많다.
알아보기 쉽게 최대한 간추려 보자.
T.let(block: (T) -> R): R
이제 좀 볼만 하다.
T객체를 인자로 받아서 T를 R의 파라미터로 넘겨준다. 여기서 람다 함수에 it 대신 다른 이름으로 사용할 수도 있다.
T를 여러번 사용해야 할 때 많이 쓰는 것 같다.
fun exLet() {
val list = listOf(1, 2, 3, 4)
println(list.sum().let { it % 2 + 5 }) //결과 : 5
println(list.sum().let { sum -> sum % 2 + 5 }) //결과 : 5
}
run 또한 비슷하다.
T.run(block: T.() -> R): R
T객체를 인자로 받아 R함수에 확장 함수 형식으로 전달한다. 이 때는 this로만 사용할 수 있고 this.get(0)
처럼 사용하거나 this
를 생략하고 get(0)
처럼 사용 가능하다.
fun exRun() {
val sum = listOf(1, 2, 3, 4).run { first() + last() + size }
println(sum) //결과 : 9
}
리스트의 처음, 마지막 값과 크기를 더한 값을 반환해서 sum에 저장한다. first()
는 List에 있는 함수이며 this.first()
와 결과가 같다.
이 외에 가장 중요한 사용법은 Nullable 객체를 Null safe call을 사용해 T가 null일 때 안전하게 지나갈 수 있다.
fun nullSafe(){
val list = listOf(1,2,null)
list.forEach { i -> i?.let { println(it) } }
}
//결과
//1
//2
with는 run과 거의 같지만 확장 함수가 아니고, Null safe call을 지원하지 않는다.
with(receiver: T, block: T.() -> R): R
T를 파라미터로 전달하는 특징이 있고 R은 확장 함수 형식이다.
fun exWith() {
val list = listOf(1, 2, 3, 4, 5)
println(with(list) { first() + last() }) //결과 : 6
}
파라미터로 T를 전달하고 null을 사용하지 못하는 것 외에 run과 큰 차이점을 모르겠다.
T.apply(block: T.() -> Unit): T
apply는 T를 확장 함수로 R에 전달하고 다시 T를 반환한다. R은 항상 Unit을 반환한다.
fun exApply() {
val list = arrayListOf(1, 2, 3)
println(list.apply { toString() + "123" }) //결과 : [1, 2, 3]
println(list.apply { plus(1) }) //결과 : [1, 2, 3]
println(list.plus(1).apply { toString() }) //결과 : [1, 2, 3, 1]
println(list.apply { add(1) }) //결과 : [1, 2, 3, 1]
}
list.toString()
에 123
을 추가하는 코드이고 안의 결과는 [1, 2, 3]123
일 것이다. 하지만 반환되어 println으로 출력한 값은 원래 list인 [1, 2, 3]
이다.[1, 2, 3]
을 출력했다.[1, 2, 3, 1]
인 List객체므로 출력 결과는 [1, 2, 3, 1]
이다.[1, 2, 3, 1]
이 출력된다.T.also(block: (T) -> Unit): T
also는 apply와 거의 같고 T를 파라미터로 R에 넘겨주며 T를 다시 반환한다.
fun exAlso() {
var count = 0
val list = List(10) { it.also { if (it % 2 == 0) count += 1 } }
println(list) //결과 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
println(count) //결과 : 5
}
리스트의 초기화 람다 함수에 it은 각 원소의 인덱스이다. it이 짝수일 때 count가 1증가하도록 R함수를 정의했고 R과 관계 없이 list는 it이 들어가고, count는 5가 됐다.
이러한 예시처럼 T의 반환값도 필요하지만 T를 활용해 다른 객체를 변화시킬 때 사용한다.
이 외에도 자주 사용하는 방식은 두 객체를 서로 교환하는 것이다.
fun swap() {
var a = 1
var b = 2
a = b.apply { b = a }
println("$a $b")//결과 : 2 1
a = 1
b = 2
a = b.also { b = a }
println("$a $b")//결과 : 2 1
}
python에서 a, b = b, a
처럼 교환은 안 되지만 apply와 also를 사용하면 같은 효과를 볼 수 있다. a = b가 Unit을 반환하는데 run, let, with는 R을 반환하고 a에는 Unit이 들어갈 수 없기 때문에 불가능하다.
자세히 설명하지 않고 넘어갔지만 그냥 모르고 넘어갈 수는 없는 내용이다.
그래서 2가지만 분석해 보려고 한다.
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
inline
: 무의미한 객체를 여러개 생성하는 불필요한 작업을 방지한다.<T, R>
: T, R 타입의 제네릭 함수로 선언T.run
: T클래스의 확장 함수block: T.() -> R
: run 함수의 block 파라미터를 T의 확장 함수로 설정하고, 그 함수의 타입은 파라미터로 받은 람다 함수 R로 설정한다.R
: 반환값은 R의 실행 결과이다.return block()
: block(=R)을 실행한 결과를 반환한다.public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
block: (T) -> Unit
: block은 T를 파라미터로 갖는 함수를 파라미터로 받고 그 함수의 반환 타입은 Unit이다.
: T
: also함수의 반환값의 타입은 T이다.
block(this)
: block 함수에 파라미터 T를 주며 실행시킨다.
return this
: 반환값은 T를 그대로 준다.
contract 부분은 잘 이해가 안 가서 따로 적지 않았다. 컴파일 할 때 도움을 주는 함수인 것 같다.
참고 링크 :
https://kotlinlang.org/docs/scope-functions.html
https://velog.io/@ejjjang0414/코틀린-대표적인-표준함수-let-also-apply-run-with-의-차이
https://codechacha.com/ko/kotlin-standard-library-functions/
https://velog.io/@haero_kim/Kotlin-with-vs-run-명확한-차이점-톺아보기