본문 바로가기
Programming/Kotlin

java에는 없고 kotlin에만 있는 함수 : let, also, apply, run

by Renechoi 2023. 10. 22.

java에는 없고 kotlin에만 있는 함수로 let, also, apply, run 이 있다. 하나씩 살펴보자.

let


fun main() {
    // let 

    val now : LocalDateTime?= null

    // 타입에 대한 제네릭을 받아서 리턴해줌 -> map이랑 비슷 
    /**
     * @kotlin.internal.InlineOnly
     * public inline fun <T, R> T.let(block: (T) -> R): R {
     *     contract {
     *         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
     *     }
     *     return block(this)
     * }
     */
    val kst = now?.let{
        // 엘비스 연산자를 통해 있을 때만 처리하도록 함 
        println(it)
    }


    // 안에서 사용되는 지역 변수를 설정함
    // 타입도 지정이 가능 
    val kst2 = now?.let{localDateTime -> 
        println(localDateTime)
    }


    val kst3 = now?.let{localDateTime:LocalDateTime ->
        println(localDateTime)
    }


    // let의 특징 - > 마지막에 있는 것이 리턴됨 
    // 스코프 안에서 
    // return을 적지 않음 


    val kst4 = now?.let {localDateTime ->
        println("")
        "let scope"
    } ?: LocalDateTime.now()



    // let -> 특정한 객체를 받아서 마지막 라인을 리턴, default는 it으로 쓰지만 변수명을 쓸 수 있음
}


fun logic(userDto: UserDto?): UserResponse? {
    return userDto?.let {
        println(it)
        UserEntity(
            name = it.name
        )
    }?.let { userEntity ->
        println(userEntity)

        UserResponse(
            name = userEntity.name
        )
    }
}

data class UserDto(
    var name:String?=null,
)

data class UserEntity(
    var name:String?=null
)

data class UserResponse(
    var name:String?=null
)


// map 이랑 let 차이 ? 
// map -> collection, let -> 모든 객체 

let 함수란?

let 함수는 Kotlin의 고차 함수 중 하나로, 호출한 객체를 인자로 받아 원하는 작업을 수행하고 마지막 표현식의 결과를 반환한다. 주로 null이 아닐 경우에만 특정 로직을 실행하거나, 람다 표현식 내부에서 사용되는 지역 변수를 명시적으로 지정할 때 사용된다.

기본 사용법

기본적으로 let 함수는 객체가 null이 아닐 때 람다 표현식을 실행한다.

val now: LocalDateTime? = null
val kst = now?.let {
    println(it)
}

지역 변수 설정

let 함수 내부에서 사용하는 변수의 이름을 직접 지정할 수 있다.

val kst2 = now?.let { localDateTime ->
    println(localDateTime)
}

타입 지정

또한 변수의 타입을 명시적으로 지정할 수도 있다.

val kst3 = now?.let { localDateTime: LocalDateTime ->
    println(localDateTime)
}

반환값

let 함수는 마지막 표현식의 결과를 반환한다. 따라서 원하는 값을 반환하려면 마지막 줄에 그 값을 적어주면 된다.

val kst4 = now?.let { localDateTime ->
    println("")
    "let scope"
} ?: LocalDateTime.now()

let과 map의 차이

let은 모든 객체에 사용할 수 있는 반면, map 함수는 주로 컬렉션에서만 사용된다.

fun logic(userDto: UserDto?): UserResponse? {
    return userDto?.let {
        UserEntity(
            name = it.name
        )
    }?.let { userEntity ->
        UserResponse(
            name = userEntity.name
        )
    }
}

also

Kotlin의 also 함수는 let과 유사하게 객체를 인자로 받아 원하는 작업을 수행할 수 있다. 그러나 also의 주된 특징은 작업이 끝나면 그대로 원래의 객체를 반환한다는 것이다. also 함수의 동작 방식과 사용 케이스에 대해 살펴보자.

fun main() {
    // also -> 또한


    // 넘어온 객체를 그대로 리턴 -> map이랑 동일한 부분은 키워드를 it으로 받되 변경 가능 
    // 타입도 지정 가능 
    // let이랑 차이 ? -? let은 마지막 라인을 리턴 
    // also는 받은 파라미터를 그대로 리턴한다. 

    /**
     * @kotlin.internal.InlineOnly
     * @SinceKotlin("1.1")
     * public inline fun <T> T.also(block: (T) -> Unit): T {
     *     contract {
     *         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
     *     }
     *     block(this)
     *     return this
     * }
     */


    UserDto(
        name = "홍길동"
    ).also {
        println(it)

        // let 이랑 차이는 받은 것을 그대로 리턴하므로 아래 코드를 리턴하지 않음 
        UserDto(name = "유관순")
    }

    // 그대로 넘기므로 로깅 등에 사용 

}

also 함수의 정의와 특징

also 함수는 람다 표현식을 인자로 받아 이 블록 안에서 원하는 로직을 수행한다. 그 후, 받은 객체를 그대로 반환한다. 이를 통해 메서드 체이닝(method chaining)이나 로깅 같은 작업이 용이해진다.

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

기본 사용법

also 함수를 이용하여 객체를 생성과 동시에 그 객체에 대한 추가 작업을 할 수 있다.

UserDto(
    name = "홍길동"
).also {
    println(it)
}

반환값

also 함수는 작업이 끝나면 원래의 객체를 반환한다. 이 특징은 let과는 다르다. let은 마지막 표현식의 결과를 반환하기 때문에, 두 함수의 반환값에 차이가 생긴다.

// also는 원래 객체를 그대로 반환
val user = UserDto(name = "홍길동").also {
    UserDto(name = "유관순")
}
println(user.name)  // 출력: "홍길동"

로깅 및 디버깅

also 함수는 로깅 또는 디버깅에 자주 사용된다. 이는 작업 중간에 상태를 확인하거나 로그를 출력하고 싶을 때 유용하다.

val list = mutableListOf(1, 2, 3).also {
    println("리스트 초기 상태: $it")
}.apply {
    add(4)
}.also {
    println("리스트 최종 상태: $it")
}

apply

Kotlin에서는 객체의 초기화와 동시에 다양한 설정을 쉽게 할 수 있도록 apply 함수를 제공한다.


fun main() {

    // apply, 생성자 패턴 


    // t 라는 제네릭에 함수가 있음 -> t 제네릭 확장 함수 
    // block이라는 람다식이 넘어가게 되어 있음 
    // 반환형이 없는 Unit이 매개변수로 넘어감 

    // also 랑 비교 ? -> also의 경우 타입을 넣어줌 -> it.name으로 접근이 가능 
    // apply는 특정 클래스에 function을 추가 -> 확장 함수 람다 
    // 람다식이 넘어가는데 그것이 확장함수 

    // apply는 초기화를 시킬 때 많이 사용하는데 키워드가 this로 넘어옴 -> 수신 받은 객체 본인 

    /**
     * @kotlin.internal.InlineOnly
     * public inline fun <T> T.apply(block: T.() -> Unit): T {
     *     contract {
     *         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
     *     }
     *     block()
     *     return this
     * }
     */

    var userDto = UserDto().apply {
        this.name="홍길동"
        name = "홍길동"
        ""
    }

    // 수신 받은 객체를 리턴하는데, this를 변경할 수는 없음 

    // 초기화 블록 
    // 마지막 라인을 리턴하는 게 아니고 수신 받은 객체를 그대로 리턴하므로 변경 x 


    println(userDto)


    // also 와 유사 -> 수신 받은 객체를 그대로 전달 
    // 생성자때 많이 사용한다. 
}



fun String.myStr(){
    // 본인에게 접근할 때 this로 접근 

}


fun <T> T.myStr():Unit{
    // 즉, T에다가 본인 자신 접근 -> this. 
    // 모든 제네릭에다가 apply 적용 모든 제네릭 클래스들이 가지고 있는 기본 메서드 -> 넘겨주는 확장 함수는 이 안에서만 쓸 수 있다. 
    // 본인을 던져주기 때문에
    // 즉 apply에 넘어가는 것은 확장 함수가 넘어가면 됨 
}


fun UserDto.myUserDto(){
    println(this.name)
}

apply 함수의 정의와 특징

apply 함수는 수신 객체(this)의 람다에서 호출되는 확장 함수이다. 이 함수는 초기화 작업이나 값을 변경할 때 주로 사용된다.

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

기본 사용법

apply 함수는 객체 생성과 동시에 해당 객체의 필드를 쉽게 초기화할 수 있다.

val userDto = UserDto().apply {
    name = "홍길동"
}

이 예시에서는 UserDto 객체를 생성하고, apply 함수를 이용해 이름을 초기화한다. 이렇게 하면, 생성자에서 이런 작업을 별도로 해주지 않아도 된다.

apply와 also의 차이

  • apply 함수는 수신 객체를 this로 참조. 따라서, 수신 객체의 멤버에 바로 접근할 수 있다.
  • also 함수는 수신 객체를 it로 참조.
// apply 사용 예
val user1 = UserDto().apply {
    name = "홍길동"
}

// also 사용 예
val user2 = UserDto().also {
    it.name = "유관순"
}

확장 함수의 사용

apply는 수신 객체의 멤버 뿐만 아니라 수신 객체를 이용한 확장 함수도 호출할 수 있다.

fun UserDto.init() {
    name = "초기 이름"
}

val userDto = UserDto().apply {
    init()
}

수신 객체를 this로 참조하여 람다 블록 내에서 자유롭게 멤버나 함수를 호출할 수 있다. 이 함수는 객체 생성과 초기화를 한 번에 수행하므로 코드를 간결하게 만들어 준다.

run

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
 * @author        : Rene
 * @since         : 2023/10/22
 */

fun main(){
    // run , 지역 scope

    /**
     * @kotlin.internal.InlineOnly
     * public inline fun <T, R> T.run(block: T.() -> R): R {
     *     contract {
     *         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
     *     }
     *     return block()
     * }
     */


    // let 함수와 동일하게 마지막에 있는 게 리턴된다. 
    // 확장 함수를 전달 받아 안에 있는 것을 바로 접근 가능하다. 

    val userDto = UserDto("").run {
        name = "홍길동"
        "empty"
    }

    println("result $userDto")

    val x = 10


    // 주로 지역 스코프를 만들 때 많이 사용된다. 
    // 안에 있는 x를 통해서 연산이 됨 
    val sum = run {
        val x = 2
        val y = 3
        x + y
    }

    println(sum)

    val now: LocalDateTime? = null

    val n = now?.let { it }?: LocalDateTime.now()

    val d = now?.let {
        val minusTime = it.minusDays(1)
        minusTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
        // 특정 스코프를 만들어 주어야 할 때 
        // 위에서는 로직이 필요 없음 -> 로직을 만들어야 한다면 스코프를 만들어야 하기 때문에 run을 만들고 사용하고자 하는 로직을 넣는다. 
    }?: run {
        val now = LocalDateTime.now()
        val minusTime = now.minusDays(1)
        minusTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
    }

    println("result $d")
}

run 함수의 정의와 특징

run 함수는 let과 유사하게 마지막 라인을 리턴하는 특징이 있다. 다만, run은 수신 객체를 this로 참조하기 때문에, 람다 블록 내에서 수신 객체의 멤버에 바로 접근할 수 있다.

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

기본 사용법

val userDto = UserDto("").run {
    name = "홍길동"
    "empty"
}

println("result $userDto")  // 출력: "result empty"

지역 스코프의 생성

run 함수는 주로 지역 스코프를 생성할 때 사용된다.

val sum = run {
    val x = 2
    val y = 3
    x + y
}

println(sum)  // 출력: 5

여기에서 run 내부의 xyrun 블록 외부에서 접근할 수 없다. 이렇게 지역 스코프를 생성하여 변수의 범위를 제한할 수 있다.

조건부 로직에 활용

run은 특정 조건에 따라 다른 로직을 실행할 때 유용하게 사용될 수 있다.

val d = now?.let {
    it.minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
} ?: run {
    LocalDateTime.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
}

println("result $d")

이 예시에서는 now 값이 null인지 아닌지에 따라 다른 로직을 실행한다. null이 아니면 let을, null이면 run을 사용하여 로직을 처리한다.

이처럼 Kotlin의 run 함수는 주로 지역 스코프를 만들거나, 조건부로 다른 로직을 실행할 때 유용하게 사용된다. 수신 객체를 this로 참조하여 람다 블록 내에서 자유롭게 멤버나 함수를 호출할 수 있어 매우 유용한 함수이며, let과 함께 많이 사용된다.




reference

반응형