본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin] 클로저와 표준 함수 let, also, apply, run, with

by 주 녕 2021. 7. 8.
반응형

모든 내용은 이지스퍼블리싱의 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

Kotlin의 표준 함수

Kotlin 표준 라이브러리에서 제공하는 함수를 이용하면 코드를 더 단순화하고 가독성을 높일 수 있음

이 표준 함수는 람다식고차 함수를 이용하여 선언되어 있음

 

람다식

    val 변수 이름: 자료형 = { 매개변수[, ...] -> 람다식 본문 }
    
    val sum: (Int, Int) -> Int = { x, y -> x + y }
    val mul: { x: Int, y: Int -> x * y}
    val add: (Int) -> Int = { it + 1 }
    
    val isPositive: (Int) -> Boolean = {
        val isPositive = it > 0
        isPositive  // 반환값
    }
    
    val isPositiveLabel: (Int) -> Boolean = number@ {
        val is Positive = it > 0
        return@number isPositive  // 라벨을 이용해 반환
    }
  • sum은 익명 함수
  • mul은 변수의 자료형 표기가 생략된 형태 : (Int, Int) -> Int 형임을 알 수 있음
  • add는 매개변수가 1개인 경우, 매개변수를 생략하고 it으로 표기할 수 있음
  • 표현식이 여러 줄일 경우 마지막 줄의 표현식이 반환값으로 처리됨
  • return@라벨 형태로 반환할 수도 있음

 

고차함수

함수의 매개변수로 함수를 받거나 함수 자체를 반환할 수 있는 함수

    fun inc(x: Int): Int {
        return x+1
    }

    fun high(name: String, body: (Int)->Int): Int {
        println("name: $name")
        val x = 0
        return body(x)
    }

    // 아래처럼 사용 가능
    val result1 = high("June", { x -> inc(x + 3) })
    val result2 = high("June") { inc(it + 3) }
    val result3 = high("June", ::inc)
    val result4 = high("June") { x -> x + 3 }
    val result5 = high("June") { it + 3 }

 

 


 

클로저(Closure)

람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념

외부 변수를 람다식 안의 외부 변수는 값을 유지하기 위해 람다식이 포획(Capture)한 변수라고 함

 

클로저에서는 포획한 변수는 참조가 유지되어 함수가 종료되어도 사라지지 않고 함수의 변수에 접근하거나 수정할 수 있게 해 줌

⭐ 기본적으로 함수 안에 정의된 변수는 지역 변수로서 스택에 저장되어 있다가 함수가 끝나면 같이 사라짐

 

[ 장점 ]

  • 내부 람다식에서 외부 함수의 변수에 접근하여 처리할 수 있기 때문에 효율이 높음
  • 완전히 다른 함수에서 변수에 접근하는 것을 제한할 수 있음

[ 조건 ]

  • final 변수를 포획한 경우, 변수 값을 람다식과 함께 저장한다
  • final이 아닌 변수를 포획한 경우, 변수를 특정 래퍼(wrapper)로 감싸서 나중에 변경하거나 읽을 수 있게 함. 이때 래퍼에 대한 참조를 람다식과 함께 저장한다

👆 Java에서는 외부 변수를 포획할 때 final만 가능! 

∴ Kotlin에서 final이 아닌 변수를 포획한 경우에는 내부적으로 변환된 자바 코드에서 final로 지정해 사용함

 

fun main() {
    val calc = Calc()
    var result = 0  // 외부 변수
    calc.addNum(2, 3) { x, y -> result = x + y }  // 클로저
    println(result)
}

class Calc {
    fun addNum(a: Int, b: Int, add: (Int, Int) -> Unit) {
        add(a, b)
    }
}

 

Tools > Kotlin > Show Kotlin ByteCode > Decompile 버튼으로 자바 코드 확인

위에 var result로 선언한 변수 앞에 final이 붙은 것을 확인할 수 있음!

...
public final class ClosureTestKt {
   public static final void main() {
      Calc calc = new Calc();
      final IntRef result = new IntRef();
      result.element = 0;
      calc.addNum(2, 3, (Function2)(new Function2() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1, Object var2) {
            this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
            return Unit.INSTANCE;
         }
...

 

 


 

Kotlin의 표준 라이브러리

함수명 람다식의 접근 방법 반환 방법
  T.let   it   block 결과
  T.also   it   T caller (it)
  T.apply   this   T caller (this)
  T.run 또는 run   this   block 결과
  with   this   Unit

 

let() 함수

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

[ 용도 ] 객체의 상태를 변경할 수 있음

  • 제네릭의 확장 함수 형태 → 어디든 사용 가능
    • 매개변수로 람다식 형태의 block이 있고, T를 매개변수로 받아 R을 반환함
    • let() 함수 또한 R을 반환함 ( = 람다식 결과 부분을 그대로 반환)
    • 이 함수를 호출한 객체를 인자로 받는 형태 → 다른 메서드를 실행하거나 연산을 수행해야 하는 경우 사용
fun main() {
    val score: Int? = 20

    fun checkScore() {
        if (score != null) {
            println("Score: $score")
        }
    }

    fun checkScoreLet() {
        score?.let { println("Score: $it")}
        val str = score.let { it.toString() }
        println(str)
    }

    checkScore()
    checkScoreLet()
}
  • score는 nullable한 변수 이기 때문에 checkScore() 함수에서는 null 체크를 하고 있음
  • checkScoreLet() 함수에서는 세이프 콜(?.)을 사용하여 null인 경우 람다식이 실행되지 않도록 함
    • 첫번째 let : T->R: R 이므로 자기 자신의 값 it으로 score를 받아서 처리함(T.let이므로 T는 score)
    • 두번째 let : 세이프 콜을 사용하지 않았기 때문에 score이 null이면 str도 null (세이프콜을 사용하지 않더라도 String?으로 추론되어 null이 할당됨
  • score은 외부 변수이므로 클로저 사용됨

 

also() 함수

let() 함수와 역할이 거의 동일하지만, 코드 결과와 상관없이 T인 객체 this를 반환함

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

[ 용도 ] 객체의 속성을 전혀 사용하지 않거나 변경하지 않고 사용하는 경우

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("June", "Kotlin")
    val a = person.let {
        it.skills = "Android"
        "success"
    }
    println(person)   // Person(name=June, skills=Android)
    println("a: $a")  // a: success

    val b = person.also {
        it.skills = "Java"
        "success"
    }
    println(person)   // Person(name=June, skills=Java)
    println("b: $b")  // b: Person(name=June, skills=Java)
}
  • let()은 람다식의 마지막 표현인 "success"를 반환함
  • also()는 람다식의 마지막 표현은 사용되지 않고, T인 객체 person을 반환함

특정 단위의 동작 분리

let()과 also()를 체이닝 형태로 구성해 간결하게 표현할 수 있으

let() 함수는 식의 결과를 반환하고, 그 결과를 다시 also() 함수에 넘기는 것!

    fun makeDir(path: String): File {
        val result = File(path)
        result.mkdirs()
        return result
    }
    
    fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

 

apply() 함수

also()와 같이 호출하는 객체 T를 이어지는 block으로 전달하고 객체인 this를 반환함

특정 객체를 생성하면서 함께 호출해야 하는 초기화 코드가 있는 경우 사용

   public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
   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 }

[ 용도 ] 특정 객체를 초기화하는데 유용

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("June", "Kotlin")
    person.apply { this.skills = "Android" }
    println(person)  // Person(name=June, skills=Android)

    val returnObj = person.apply {
        name = "Lee"
        skills = "Java"
    }

    println(person)     // Person(name=Lee, skills=Java)
    println(returnObj)  // Person(name=Lee, skills=Java)
}
  • also()와의 차이점
    • 다르게 T.()와 같은 표현에서 람다식이 확장함수로 처리됨
    • 객체를 넘겨 받는 방식에서 also()는 it을 사용(생략X)하는 반면, apply()는 this를 사용하여 생략할 수 있음
  • apply()는 확장 함수로 person을 this로 받아 클로저를 사용하는 방식과 같음 → 객체의 프로퍼티를 변경하면 원본 객체에 반영
    fun makeDir(path: String): File {
        val result = File(path)
        result.mkdirs()
        return result
    }
    
    // fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
    val f = File(path).apply { mkdirs() }

해당 예시는 위와 같이 고칠 수 있음

  1. File(path)에 의해 생성된 결과를 this로 받고
  2. File 객체의 mkdirs()를 호출하여 파일 경로를 생성하고
  3. 객체 this를 반환함

 

run() 함수

    public inline fun <R> run(block: () -> R): R = return block()  // 인자 없는 익명함수 형태
    public inline fun <T, R> T.run(block: T.() -> R): R = return block()  // 객체에서 호출하는 형태
  • 인자 없는 익명함수처럼 동작하는 형태
    • 확장함수도 아니고 block에 입력값이 없음 → 객체를 전달받아 속성을 변경하는 형식에 사용하는 함수가 아님
    • 어떤 객체를 생성하기 위한 명령문을 block안에 묶어 가독성을 높이는 역할을 함
  • 객체에서 호출하는 형태
    • T의 확장함수이기 때문에 세이프 콜(.?)을 붙여 non-null인 경우에만 실행할 수 있음
    • 어떤 값을 계산할 필요가 있거나 여러 개의 지역변수 범위를 제한할 때 사용함
  • block이 독립적으로 사용됨
    • 이어지는 block 내에서 처리할 작업을 넣어줄 수 있음
    • 일반 함수와 마찬가지로 값을 반환하지 않거나 특정 값을 반환할 수 있음

apply()와 run() 비교

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("June", "Kotlin")
    val returnObj = person.apply {
        this.name = "Lee"
        this.skills = "Android"
        "success"
    }
    println(person)     // Person(name=Lee, skills=Android)
    println(returnObj)  // Person(name=Lee, skills=Android)

    val returnObj2 = person.run {
        this.name = "Kim"
        this.skills = "C++"
        "success"
    }
    println(person)      // Person(name=Kim, skills=C++)
    println(returnObj2)  // success
}
  • apply()의 결과는 위에서 든 예시와 같이 참조한 원본 객체에 변경이 일어남
  • apply()는 this를 반환한 반면, run()은 마지막 표현식을 반환함
  • 마지막 표현식을 구성하지 않으면 Unit이 반환됨

 

with() 함수

    public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
  • run()과 기능은 거의 동일한데, with()는 receiver로 전달할 객체를 처리하므로 객체의 위치가 달라짐
  • with()는 확장함수 형태가 아니고 단독으로 사용하는 형태
  • 세이프 콜(.?)을 지원하지 않기 때문에 let()과 함께 사용되기도 함 (null이 아닌 경우가 확실하면 with만 사용해도 됨)
fun main() {
    data class User(val name: String, var skills: String, var email: String? = null)
    val user = User("June", "default")

    val result = with (user) {
        skills = "Kotlin"
        email = "xxx@email.com"
    }
    println(user)  // User(name=June, skills=Kotlin, email=xxx@email.com)
    println("result: $result")  // result: kotlin.Unit
}

 

use() 함수

특정 객체는 사용된 후 닫아야 하는 경우가 있는데, use()를 사용하면 객체를 사용하고 close()를 자동으로 호출하여 닫아줄 수 있음

    public inline fun <T: Closeable?, R> T.use(block: (T) -> R): R

 

 

 

*람다식과 고차함수에 대한 내용은 아래 포스팅 참고!

 

[Kotlin] 함수와 함수형 프로그래밍 (1)

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 함수 선언하고 호출하기 함수 : 여러 값을 입력받아 기능을 수행하고 결과값을 반환하는 코드의 모음 (코드의 재사용) fun 함

junyoung-developer.tistory.com

 

[Kotlin] 함수와 함수형 프로그래밍 (2) - 고차함수와 람다식

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 고차 함수와 람다식 다른 함수를 인자로 사용하거나 함수를 결과값으로 반환하는 함수 고차 함수의 형태 일반 함수를 인자

junyoung-developer.tistory.com

 

reference >

반응형

댓글