[Kotlin] let, also, run, with, apply.. 뭐가 다른데?

Janzizu·2023년 9월 21일
0

오늘은 제목에서 썼다시피 코틀린언어를 학습하거나 혹은 안드로이드에서 코틀린으로 개발하다보면
흔하게 접할 수 있는 위의 5가지 함수에 대해 알아보려고 한다.

위의 5가지 함수는 작동 방식이나 결과가 매우 비슷하기 때문에 서로를 대체해서 사용할 수도 있다.
(비슷하기 때문에 잘 모르고 쓰는 경우가 많다. ---> 어쨌든 작동은 하니까?😒)

범위함수 (scope function) ?

위의 5가지 함수는 범위함수라고도 불린다.
범위함수는 무엇인가?

주어진 객체에 대한 람다 함수 블록을 실행하고, 이 블록 내에서 객체에 대한 작업을 수행한 후 결과를 반환하거나 또는 다른 값을 반환할 수 있도록 해주는 함수 라고 할 수 있다.

글보다는 코드로 보고 이해하는게 더 좋을 것 같으니, 알아보도록 하겠다.

잠깐! 👀

들어가기 앞서 간단하기 제네릭 타입 매개변수를 먼저 보면
이 5가지 함수에는 각 각 TR 타입 매개변수가 쓰이는데,
T는 타입파라미터로 함수가 호출될 때 실제 객체 타입으로 대체되며
R은 T와 마찬 가지로 타입 파라미터로서, 람다 함수가 실행되고 반환되는 값의 타입을 나타낸다.

또한 앞으로 글에서 수신자 객체T 라는 말이 쓰일건데,
수신 객체란 확장함수나 범위함수 내에서 작업을 수행하는 대상 객체를 나타낸다.


let

let() 함수는 함수를 호출하는 수신자 객체 T를 블록의 인자로 넘기고, 블록의 결과값을 반환하는 함수이다.

함수 시그니처는 아래와 같다.

public inline fun <T,R> T.let(block: (T) -> R): R = block(this)

1. null 가능성이 있는 객체에서 활용

var name: String?
...

name?.let {
	Toast.makeText(....).show()
}

위 코드를 보면 name은 nullable한 변수이다.
이 때 세이프콜(?.)과 let을 함께사용하면 null검사를 대체할 수 있다.
즉, 위 코드는 name이 null이 아닐 때 let블록안의 코드가 수행된다.

2. 메서드 체이닝

var x = 10
var y = 20

x = x.let { it + 2}.let {
	val i = it + b
    i
}

위의 식에서 두 번째 let의 마지막 구문(i)이 반환되어 a에 할당된다.


also

also() 함수는 let()과 마찬가지로 함수를 호출하는 수신자 객체 T를 블록의 인자로 넘기지만
let()과는 다르게 수신자 객체T 자체를 반환한다.
따라서 수신 객체의 속성을 변경하지 않고 사용하는 경우에 쓰면 된다.

함수 시그니처는 아래와 같다.

public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

let() 함수와는 반환하는 값이 다른 걸 알 수 있다.
let() 함수에서는 블록 내에 마지막으로 수행된 코드블록의 결과를 반환하지만,
also() 함수는 코드블록 결과와는 상관없이 수신객체T 를반환한다.

var num = 1
num = num.also {
	it + 3
}

위의 코드에서 num은 also 블록내에서 +3을 해주었지만 코드의 수행결과와는 상관없이 it ,즉 원본 값이었던 1을 반환한다.
응?? 그럼 항상 수신객체 T를 반환한다면 어떨 때 쓰일 수 있는 걸까??? 🙄

1. also() 함수는 주로 로깅 및 디버깅에 쓰인다.

val singer = Singer("IU","930516")
singer.also {
	println("값:${it}")
}

singer 객체 속성값을 로그로 알아보는 디버깅의 경우.

2. 유효성 검사

val isValid = age.also {
	if (it.age < 20) println("minor") else println("adult")
    it.age > 20
}

also() 함수를 사용하여 나이를 검사하고 있고, 조건에 맞는 스트링 값을 출력하며 also()는 수신객체를 반환하기 때문에 마지막 조건문은 생략된다.

isValid 변수 값은 age 객체와 동일한 참조를 가진다.


apply

apply() 함수는 이전 함수들과 마찬가지로 수신객체T를 블록의 인자로 전달하고 객체 자체인 this를 반환한다.
특정 객체를 생성하면서 함께 초기화를 하는 경우에 주로 쓰인다.

함수 시그니처는 아래와 같다.

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

얼핏보면 also() 함수와 같아 보이지만 apply()는 람다식이 확장 함수(T.())로 처리된다.
따라서 also() 와는 다르게 수신객체T 타입의 객체 내에서 작업을 수행할 수 있다.
(객체의 프로퍼티에 접근하여 변경할 수 있다.)

1. 객체의 생성과 함께 초기화

val singer = Singer("IU","930516").apply{
	this.name = "태연"
    this.info = "890309"
}
println(singer) // 태연,890309

apply()는 확장함수로서 singer를 this로 받아오는데, 이 객체의 프로퍼티를 변경하면
원본 객체에 또한 변경이되고 이것은 this로 반환된다.
이 때 여기서 this는 생략이 가능하기 때문에 아래와 같이 작성할 수 있다.

val singer = Singer("IU","930516").apply{
	name = "태연"
    info = "890309"
}

🤔 also() 함수도 생략이 가능한가???

아니다! also() 함수와 다르게 apply() 함수가 생략이 가능한 것은 확장함수와 관련이 있다.
우선 also() 함수에서는 수신 객체를 변경하지 않고 그 자체를 반환하기 때문에 this를 사용하여 객체의 상태에 접근할 수 없다!
apply() 함수는 확장 함수로 처리되어 대상 객체에 접근할 수 있기 때문에 this를 생략할 수 있는 것이다.


with

with() 함수는 인자로 받는 객체를 block의 receiver로 전달하며 결괏값을 반환한다.

함수 시그니처는 아래와 같다.

public inline fun <T,R> with(receiver: T, block: T.() -> R):R = receiver.block()

receiver는 수신객체로 사용할 객체이며, block은 receiver객체내에서 수행할 작업을 정의하는 람다함수이다.
with() 함수는 매개변수가 2개 이므로 with() {...} 와 같은 형태로 사용된다.

1. null이 아닌것이 확실하고, 결과가 필요하지 않을 때

val singer = Singer("IU","930516")
val result = with (singer) {
	name = "태연"
    info = "890309"
}

위와같이 기본적으로 Unit이 반환되지만 필요한 경우 마지막 표현식을 반환할 수 있다.`

val singer = Singer("IU","930516")
val result = with (singer) {
	name = "태연"
    info = "890309"
    "Good!!!"
}

위 코드에서 with() 함수블록에서 마지막 코드가 반환되므로 result변수는 string형의 변수가 된다.


run

run() 함수는 인자가 없는 익명 함수처럼 동작하는 형태와, 객체에서 호출하는 형태 2가지로 활용할 수 있다.

함수 시그니처는 아래와 같다.

public inline fun <R> run(block: () -> R): R = return block()
public inline fun <T,R> T.run(block: T.() -> R): R = return block()
var skills = "kotlin"
val a = 10
skils = run {
	val level = "kotlin Lev:" + a
    level
}

block이 독립적으로 사용되어 마지막 블록인 level을 반환한다.

run또한 시그니처 두 번째줄과 같이 람다식이 확장함수로 처리되기 때문에,
객체의 프로퍼티에 this를 생략하여 접근이 가능하다.

val result = person.run {
	country = "korea"
    sex = "women"
    "ok"
}

마지막 블록인 ok를 반환하여 result는 string형 변수가 된다.


이렇게 지금까지 5가지 코틀린의 범위함수에 대해 알아보았다.
다시 한 번 더 함수 시그니처를 정리해보자.

  • let
public inline fun <T,R> T.let(block: (T) -> R): R = block(this)

람다식 접근을 it으로 하며 마지막 블록 결과를 반환

  • also
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

람다식 접근을 it으로 하며 수신객체 T를 반환

  • apply
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

람다식 접근을 this 혹은 생략할 수 있으며 수신객체 T를 반환

  • with
public inline fun <T,R> with(receiver: T, block: T.() -> R):R = receiver.block()

람다식 접근을 this 혹은 생략할 수 있으며 기본적으론 Unit을 반환하지만 마지막 블록도 반환할 수 있음

  • run
public inline fun <R> run(block: () -> R): R = return block()
public inline fun <T,R> T.run(block: T.() -> R): R = return block()

1.수신 객체를 가지지 않고, 익명함수처럼 동작하여 this와 같은 참조는 하지 않음. 마지막 블록 반환
2. 람다식 접근을 this 혹은 생략할 수 있으며 마지막 블록 반환

0개의 댓글