Kotlin - 확장 람다, StringBuilder, buildString(), 영역 함수, 'inline'


이 포스트에서는 확장 람다와 영역 함수에 대해 알아본다.

소스는 github 에 있습니다.


목차


개발 환경

  • 언어: kotlin 1.9.23
  • IDE: intelliJ
  • SDK: JDK 17
  • 의존성 관리툴: Gradle

1. 확장 람다

확장 람다는 확장 함수와 비슷하다.

확장 함수에 대한 좀 더 상세한 내용은 1. 확장 함수 (extension function) 를 참고하세요.

val va: (String, Int) -> String = { str, n ->
    str.repeat(n) + str.repeat(n)
}

// 확장 람다
// String 파라메터를 괄호 밖으로 옮겨서 String.(Int) 처럼 확장 함수 구문 사용
// 확장 대상 객체(여기서는 String) 이 수신 객체가 되고, this 를 통해서 수신 객체에 접근 가능
val vb: String.(Int) -> String = {
    this.repeat(it) + repeat(it)
}

fun main() {
    val result1 = va("Assu", 2)
    val result2 = "Assu".vb(2)
    val result3 = vb("Assu", 2)

    // 컴파일되지 않음
    // val result4 = "Assu".va(2)

    println(result1) // AssuAssuAssuAssu
    println(result2) // AssuAssuAssuAssu
    println(result3) // AssuAssuAssuAssu
}

위 코드에서 va() 의 호출은 (String, Int) -> String 을 보고 예상할 수 있는 호출 형태이다.

아래 확장 함수 구문을 보자.
다른 람다와 마찬가지로 파라메터가 하나(여기서는 Int) 뿐이면 확장 함수에서도 it 으로 그 유일한 파라메터를 가리킬 수 있다.

// String 파라메터를 괄호 밖으로 옮겨서 String.(Int) 처럼 확장 함수 구문 사용
// 확장 대상 객체(여기서는 String) 이 수신 객체가 되고, this 를 통해서 수신 객체에 접근 가능
val vb: String.(Int) -> String = {
    this.repeat(it) + repeat(it)
}

확장 람다인 위 함수에서 String 을 확장하는 건 파라메터 목록인 (Int) 가 아니라 전체 람다인 (Int) -> String 이다.
따라서 확장 람다는 String.(Int) -> String 에서 (Int) -> String 이 부분이다.

코틀린에서는 확장 람다를 수신 객체가 지정된 함수 리터럴 이라고 한다.
함수 리터럴은 람다와 익명 함수 모두를 포함한다.
따라서 수신 객체가 암시적 파라메터로 추가 지정된 람다라는 사실을 강조하고 싶을 때는 수신 객체가 지정된 람다확장 람다의 동의어로 더 자주 사용한다.


1.1. 여러 파라메터를 받는 확장 람다

확장 함수처럼 확장 람다도 여러 파라메터를 받을 수 있다.

// Int 의 확장 람다
// 파라메터는 아무것도 받지 않고 Boolean 리턴 () -> Boolean
val zero: Int.() -> Boolean = {
    this == 0
}

// 파라메터에 이름을 붙이는 대신 `it` 사용
val one: Int.(Int) -> Boolean = {
    this % it == 0
}

val two: Int.(Int, Int) -> Boolean = { arg1, arg2 ->
    this % (arg1 + arg2) == 0
}

val three: Int.(Int, Int, Int) -> Boolean = { arg1, arg2, arg3 ->
    this % (arg1 + arg2 + arg3) == 0
}

fun main() {
    val result1 = 0.zero()
    val result2 = 10.one(10)
    val result3 = 20.two(10, 10)
    val result4 = 30.three(10, 10, 10)

    println(result1) // true
    println(result2) // true
    println(result3) // true
    println(result4) // true
}

1.2. 함수의 파라메터로 확장 람다 사용

위에선 val 를 선언하여 확장 람다를 이용했지만 아래의 f2() 처럼 함수의 파라메터로 확장 람다를 사용하는 것이 일반적이다.

f1() 보다 f2() 가 람다가 더 간결해진다.

class A {
    fun af() = 1
}

class B {
    fun bf() = 2
}

fun f1(lambda1: (A, B) -> Int) = lambda1(A(), B())

// 함수의 파라메터로 확장 람다 사용
fun f2(lambda2: A.(B) -> Int) = A().lambda2(B())

fun main() {
    val result1 = f1 { aa, bb -> aa.af() + bb.bf() }
    val result2 = f2 { af() + it.bf() }

    println(result1) // 3
    println(result2) // 3
}

1.3. 확장 람다의 반환 타입이 Unit 인 경우

확장 람다의 반환 타입이 Unit 이면 람다 본문이 만들어 낸 결과는 무시된다.
람다 본문의 마지막 식의 값을 무시한다는 의미로써 return 으로 Unit 이 아닌 값을 반환하면 타입 오류가 발생한다.

class A1 {
    fun af() = 1
}

fun unitReturn(lambda1: A1.() -> Unit) = A1().lambda1()

fun nonUnitReturn(lambda1: A.() -> String) = A().lambda1()

fun main() {
    val result1 = unitReturn { "Unit 은 리턴값을 무시하기 때문에 이건 아무것도 할 수 없음" }
    val result2 = unitReturn { 1 } // 임의의 타입
    val result3 = unitReturn { } // 아무 값도 만들어내지 않는 경우
    val result4 = nonUnitReturn { "적절한 타입을 넣어주세요" }

    // 컴파일 오류
    // val result5 = nonUnitReturn { }

    println(result1) // kotlin.Unit
    println(result2) // kotlin.Unit
    println(result3) // kotlin.Unit
    println(result4) // 적절한 타입을 넣어주세요
}

1.4. 일반 람다를 파라메터로 받는 위치에 확장 람다 전달: filterIndexed(), toCharArray()

일반 람다를 파라메터로 받는 위치에 확장 람다를 전달할 수도 있는데 이 때 두 람다의 파라메터 목록을 서로 호환되어야 한다.

// 일반 람다를 파라메터로 받는 String 의 확장 함수
fun String.transform1(
    n: Int,
    lambda: (String, Int) -> String,
) = lambda(this, n)

// 확장 람다를 파라메터로 받는 String 의 확장 함수
fun String.transform2(
    n: Int,
    lambda: String.(Int) -> String,
) = lambda(this, n)

// val 를 선언하여 String 의 확장 람다 이용
val duplicate: String.(Int) -> String = { repeat(it) }

// val 를 선언하여 String 의 확장 람다 이용
val alternate: String.(Int) -> String = {
    toCharArray()
        .filterIndexed { i, _ -> i % it == 0 }
        .joinToString(separator = "")
}

fun main() {
    val result1 = "hello".transform1(5, duplicate).transform2(3, alternate)
    val result2 = "hello".transform2(5, duplicate).transform1(3, alternate)

    println(result1) // hleolhleo
    println(result2) // hleolhleo
}

확장 람다인 duplicatealternatetransform1() 에 넘긴 경우, 두 람다의 내부에서 수신 객체 this 는 첫 번째 인자로 받은 String 객체가 된다.

filterIndexed() 는 element 의 값과 인덱스 값을 이용할 수 있다.

filterIndexed() 시그니처

inline fun <T> Array<out T>.filterIndexed(predicate: (index: Int, T) -> Boolean): List<T>

toCharArray()

문자열을 한 글자씩 분리할 때 문자열.toCharArray() 를 사용하면 문자열을 charArray 로 변환함

문자열.toList() 를 사용하여 List<Char> 를 반환받아서 사용해도 무방함

joinToString() 에 대한 좀 더 상세한 내용은 2.3. joinToString() 을 참고하세요.


1.5. 확장 람다 대신 함수 참조(::) 전달

:: 을 사용하여 확장 람다가 필요한 곳에 함수 참조를 넘길 수도 있다.

fun Int.d1(f: (Int) -> Int) = f(this) * 10

fun Int.d2(f: Int.() -> Int) = f(this) * 10

fun f1(n: Int) = n + 3

fun Int.f2() = this + 3

fun main() {
    val result1 = 11.d1(::f1)
    val result2 = 11.d2(::f1)
    val result3 = 11.d1(Int::f2)
    val result4 = 11.d2(Int::f2)

    println(result1) // 140
    println(result2) // 140
    println(result3) // 140
    println(result4) // 140
}

확장 함수에 대한 참조를 확장 람다의 타입과 같다.

위 예시에서 Int::f2Int.() -> Int 이다.

11.d1(Int::f2) 호출을 보면 일반 람다 파라메터를 요구하는 d1() 에 확장 함수를 넘기고 있다.


1.6. 일반 확장 함수와 확장 람다에서의 다형성

아래는 일반 확장 함수인 Base.g() 와 확장 람다인 Base.h() 모두에서 (수신 객체의 f 호출 시) 다형성이 동작함을 알 수 있다.

다형성에 대한 좀 더 상세한 내용은 3. 다형성 (polymorphism) 을 참고하세요.

open class Base {
    open fun f() = 1
}

class Derived : Base() {
    override fun f() = 2
}

// 일반 확장 함수
fun Base.g() = f()

// 확장 람다
fun Base.h(x: Base.() -> Int) = x()

fun main() {
    val b: Base = Derived() // 업캐스트

    val result1 = b.g()
    val result2 = b.h { f() }

    println(result1) // 2
    println(result2) // 2
}

1.7. 확장 람다 대신 익명 함수 구문 사용

익명 함수에 대한 좀 더 상세한 내용은 2.3. 익명 함수 를 참고하세요.

아래는 익명 람다 위치에 익명 확장 함수를 사용한 예시이다.

fun exec(
    arg1: Int,
    arg2: Int,
    f: Int.(Int) -> Boolean,
) = arg1.f(arg2)

fun main() {
    val result =
        exec(
            10,
            2,
            fun Int.(d: Int): Boolean { // 익명 람다 위치에 익명 확장 함수를 사용
                println("---$this") // 10
                return this % d == 0
            },
        )

    println(result) // true
}

1.8. 확장 람다와 사용하는 StringBuilderbuildString()

코틀린 표준 라이브러리는 확장 람다와 함께 사용하는 함수가 많이 있다.

StringBuildertoString() 을 적용하여 불변 String 을 만들어낼 수 있는 가변 객체이다.

더 개선된 방법으로는 확장 람다를 인자로 받는 buildString() 이 있는데 buildString() 은 자체적으로 StringBuilder 객체를 생성하고, 확장 람다를 생성한 StringBuilder 객체에 적용한 후 toString() 을 호출하여 문자열을 얻는다.

// StringBuilder 로 문자열 생성
private fun messy(): String {
    // StringBuilder 생성
    val built = StringBuilder()

    built.append("ABCs: ")

    // a~x 까지의 문자열을 덧붙임
    ('a'..'x').forEach { built.append(it) }

    // 결과 생성
    return built.toString()
}

// buildString() 으로 문자열 생성
// append() 호출의 수신 책게를 직접 만들고 관리할 필요가 없음
private fun clean(): String =
    buildString {
        append("ABCs: ")
        ('a'..'x').forEach { append(it) }
    }

// joinToString() 으로 문자열 생성
private fun cleaner(): String = ('a'..'x').joinToString(separator = "", prefix = "ABCs: ")

fun main() {
    val result1 = messy()
    val result2 = clean()
    val result3 = cleaner()

    // 모두 동일한 결과
    // ABCs: abcdefghijklmnopqrstuvwx
    println(result1)
    println(result2)
    println(result3)
}

1.9. buildList(), buildMap(): forEachIndexed()

buildString() 처럼 확장 람다를 사용하여 읽기 전용(=불변) List 와 Map 을 만들어주는 buildList(), buildMap() 함수도 있다.

확장 람다 안에서의 List 와 Map 은 가변이지만, buildList(), buildMap() 의 결과는 불변이다.

val characters: List<String> =
    buildList {
        add("Chars: ")
        ('a'..'d').forEach { add("$it") }
    }

val charMap: Map<Char, Int> =
    buildMap {
        ('a'..'d').forEachIndexed { n, ch -> put(ch, n) }
    }

fun main() {
    println(characters) // [Chars: , a, b, c, d]
    println(charMap) // {a=0, b=1, c=2, d=3}
}

1.10. 확장 람다로 빌더 작성 (빌더 패턴)

이론적으로는 필요한 모든 설정의 객체 생성자를 모두 선언할 수 있지만, 경우의 수가 너무 많으면 생성자만으로는 코드가 너무 복잡해진다.

빌더 패턴의 장점은 아래와 같다.

  • 여러 단계에 걸쳐 객체를 생성하므로 객체 생성이 복잡할 때 유용함
  • 동일한 기본 생성 코드를 사용하여 다양한 조합의 객체 생성 가능

확장 람다를 사용하여 빌더를 구현하면 도메인 특화 언어(DSL: Domain Specific Language) 를 만들 수 있다는 장점도 있다.

DSL 의 목표는 프로그래머가 아닌 도메인 전문가에게 더 편하고 이해하기 쉬운 문법을 제공하는 것이다.
이를 통해 DSL 을 둘러싼 언어의 아주 일부분만 알아도 도메인 전문가가 직접 잘 동작하는 해법을 만들어낼 수 있다.

아래는 여러 종류의 샌드위치를 조리하기 위한 재료와 절차를 담는 시스템의 예시이다.

// ArrayList<RecipeUnit> 을 상속
open class Recipe : ArrayList<RecipeUnit>()

open class RecipeUnit {
    override fun toString() = "${this::class.simpleName}"
}

open class Operation : RecipeUnit()
class Toast : Operation()
class Grill : Operation()
class Cut : Operation()

open class Ingredient : RecipeUnit()
class Bread : Ingredient()
class Ham : Ingredient()
class Swiss : Ingredient()
class PeanutButter : Ingredient()
class Mustard : Ingredient()

open class Sandwich : Recipe() {
    fun action(op: Operation): Sandwich {
        add(op)
        return this
    }

    fun grill() = action(Grill())
    fun toast() = action(Toast())
    fun cut() = action(Cut())
}

// fillings 확장 람다는 호출자가 Sandwich 를 여러 가지 설정으로 준비할 수 있도록 해줌
fun sandwich(fillings: Sandwich.() -> Unit): Sandwich {
    val sandwich = Sandwich()
    sandwich.add(Bread())
    sandwich.toast()
    sandwich.fillings()
    sandwich.cut()
    return sandwich
}

fun main() {
    val result1 =
        sandwich {
            add(Ham())
            add(Mustard())
        }

    val result2 =
        sandwich {
            add(Swiss())
            add(PeanutButter())
            grill()
        }

    println(result1)
    println(result2)
}

fillings() 확장 람다는 호출자가 Sandwich 를 여러 가지 설정으로 준비할 수 있도록 해주며, 사용자는 각각의 설정을 만들어내는 생성자에 대해서는 알 필요가 없다.

result1, result2 를 보면 이 코드가 어떻게 DSL 로 사용되는지 알 수 있다.
사용자는 sandwich() 를 사용해서 Sandwich 를 만드는 문법만 이해하면 된다.


2. 영역 함수 (Scope Function): let(), run(), with(), apply(), also()

영역 함수객체의 이름을 사용하지 않아도 그 객체에 접근할 수 있는 임시 영역을 만들어주는 함수오로지 코드의 가독성을 위해 사용되며, 다른 추가 기능은 제공하지 않는다.

영역 함수는 단지 가독성을 높이려는 목적으로 만들어진 것이므로 영역 함수를 내포시키는 것은 좋지 않는 방식이다.

영역 함수는 let(), run(), with(), apply(), also(), 총 5개로 각각 람다와 함께 사용된다.

각 영역 함수는 문맥 객체it 으로 다루는지 this 로 다루는지각 함수가 어떤 값을 반환하는지에 따라 달라진다.

with() 만 다른 호출 문법을 사용하고, 나머지는 모두 동일한 호출 문법을 사용한다.

 this 문맥 객체it 문맥 객체
람다의 마지막 식의 값을 반환with(), run()let()
수신 객체를 반환 (변경된 객체를 다시 반환)apply()also()

run() 은 확장 함수이고, with() 는 일반 함수인 부분을 제외하면 두 함수는 같은 일을 한다.
수신 객체가 null 이 될 수 있거나, 연쇄 호출이 필요한 경우엔 run() 을 사용하는 것을 권장한다.

문맥 객체를 this 로 접근 가능한 영역 함수인 run(), with(),apply() 를 사용하면 영역 블록 안에서 가장 깔끔한 구문 사용 가능하고, 문맥 객체를 it 으로 접근할 수 있는 영역 함수인 let(), also() 는 람다 인자에 이름을 붙일 수 있다.

**<각 영역="" 함수는="" 아래와="" 같은="" 상황에="" 따라="" 골라서="" 사용="">**

  • 결과를 만들어야 하는 경우 람다의 마지막 식의 값을 돌려주는 영역 함수인 let(), run(), with() 사용
  • 객체에 대한 호출 식을 연쇄적으로 사용해야 하는 경우 변경한 객체를 돌려주는 영역 함수인 apply(), also() 사용
data class Tag(var n: Int = 0) {
  var s: String = ""

  fun incr() = ++n
}

fun main() {
  // let() 사용 (this 로 객체 접근 불가)
  // 객체를 it 으로 접근하고, 람다의 마지막 식의 값을 반환
  val result1 =
    Tag(1).let {
      it.s = "let: ${it.n}"
      it.incr()
    }

  // let() 을 사용하면서 람다 인자에 이름을 붙임
  val result2 =
    Tag(2).let { tag ->
      tag.s = "let: ${tag.n}"
      tag.incr()
    }

  // run() 사용 (it 으로 객체 접근 불가)
  // 객체를 this 로 접근하고, 람다의 마지막 식의 값을 반환
  val result3 =
    Tag(3).run {
      s = "run: $n" // 암시적 this
      incr()
    }

  // with() 사용 (it 으로 객체 접근 불가)
  // 객체를 this 로 접근하고, 람다의 마지막 식을 반환
  val result4 =
    with(Tag(4)) {
      s = "with: $n"
      incr()
    }

  // apply() 사용 (it 으로 객체 접근 불가)
  // 객체를 this 로 접근하고, 변경된 객체를 다시 반환
  val result5 =
    Tag(5).apply {
      s = "apply: $n"
      incr()
    }

  // also() 사용 (this 로 객체 접근 불가)
  // 객체를 it 으로 접근하고, 변경된 객체를 다시 반환
  val result6 =
    Tag(6).also {
      it.s = "also: $it.n"
      it.incr()
    }

  // also() 에서도 람다의 인자에 이름을 붙일 수 있음
  val result7 =
    Tag(7).also { tag ->
      tag.s = "also: $tag.n"
      tag.incr()
    }

  println(result1) // 2
  println(result2) // 3
  println(result3) // 4
  println(result4) // 5
  println(result5) // Tag(n=6)
  println(result6) // Tag(n=7)
  println(result7) // Tag(n=8)
}

2.1. 안전한 호출(?.) 로 영역 함수 사용: Random.nextBoolean(), removeSuffix()

안전한 호출(?.) 에 대한 좀 더 상세한 내용은 2.1. 안전한 호출 (safe call): ?. 을 참고하세요.

안전한 호출인 ?. 을 사용하면 영역 함수를 null 이 될 수 있는 수신 객체에도 적용할 수 있다.
안전한 호출을 사용하면 수신 객체가 null 이 아닌 경우에만 영역 함수가 호출된다.

import kotlin.random.Random

fun gets(): String? = if (Random.nextBoolean()) "str!" else null

fun main() {
    val result =
        gets()?.let {   // gets() 의 반환값이 null 이 아닐 때만 let 이 호출됨
            // let 에서 null 이 될 수 없는 수신 객체는 람다 내부에서 null 이 될 수 없는 it 이 됨
            it.removeSuffix("!") + it.length
        }

    println(result) // str4 or null
}

문맥 객체에 대해 안전한 호출 ?. 을 적용하면 영역 함수의 영역에 들어가기에 앞서 null 검사를 수행하는데, 만일 안전한 호출을 하지 않는다면 영역 함수 안에서 개별적으로 null 검사를 해야한다.

class Gnome(val name: String) {
    fun who() = "Gnome $name"
}

fun whatGnome(gnome: Gnome?) {
    gnome?.let { it.who() }
    gnome.let { it?.who() } // 영역 함수 안에서 개별적으로 null 검사

    gnome?.run { this.who() }
    gnome.run { this?.who() } // 영역 함수 안에서 개별적으로 null 검사

    gnome?.apply { who() }
    gnome.apply { this?.who() } // 영역 함수 안에서 개별적으로 null 검사

    gnome?.also { it.who() }
    gnome.also { it?.who() } // 영역 함수 안에서 개별적으로 null 검사

    // 문맥 객체인 gnome 이 null 인지 검사할 방법이 없음
    with(gnome) { this?.who() } // 영역 함수 안에서 개별적으로 null 검사
}

let(), run(), apply(), also() 에 대해 안전한 호출 ?. 을 사용하면 수신 객체가 null 인 경우 전체 영역이 무시된다.

class Gnome1(val name: String) {
    fun who() = "Gnome $name"
}

fun whichGnome(gnome: Gnome1?) {
    println(gnome?.name)
    gnome?.let { println(it.who()) }
    gnome?.run { println(who()) }
    gnome?.apply { println(who()) }
    gnome?.also { println(it.who()) }
}

fun main() {
    // Assu
    // Gnome Assu
    // Gnome Assu
    // Gnome Assu
    // Gnome Assu
    whichGnome(Gnome1("Assu"))

    // null
    whichGnome(null)
}

2.2. Map 을 검색한 결과에 영역 함수 적용

Map 의 key 에 해당하는 원소를 찾을 수 있다는 보장이 없기 때문에 Map 에서 객체를 읽어오는 함수의 반환값도 null 이 될 수 있다.

아래는 Map 을 검색한 결과에 대해 다양한 영역 함수를 적용한 예시이다.

data class Toy(var id: Int)

fun display(iMap: Map<String, Toy>) {
    println("display: $iMap")

    val toy1: Toy =
        iMap["main"]?.let {
            it.id += 10
            it
        } ?: return // map 의 key 에 main 이 없으면 display() 함수를 종료시키므로 아래 로직을 실행되지 않음
    println("toy1: $toy1")

    val toy2: Toy? =
        iMap["main"]?.run {
            id += 10
            this
        }
    println("toy2: $toy2")

    val toy3: Toy? =
        iMap["main"]?.apply {
            id += 10
            this // 변경된 객체를 다시 반환하므로 의미없는 구문
        }
    println("toy3: $toy3")

    val toy4: Toy? =
        iMap["main"]?.apply {
            id += 10
        }
    println("toy4: $toy4")

    val toy5: Toy? =
        iMap["main"]?.also {
            it.id += 10
        }
    println("toy5: $toy5")
}

fun main() {
    // display: {main=Toy(id=1)}
    // toy1: Toy(id=11)
    // toy2: Toy(id=21)
    // toy3: Toy(id=31)
    // toy4: Toy(id=41)
    // toy5: Toy(id=51)
    display(mapOf("main" to Toy(1)))

    // display: {none=Toy(id=1)}
    display(mapOf("none" to Toy(1)))
}

엘비스 연산자 ?: 에 대한 좀 더 상세한 내용은 2.2. 엘비스(Elvis) 연산자: ?: 를 참고하세요.


2.3. 연쇄 호출에서 null 이 될 수 있는 타입에 영역 함수 사용: takeUnless()

영역 함수는 연쇄 호출에서 null 이 될 수 있는 타입과 함께 사용할 수 있다.

val functions =
    listOf(
        // 익명 함수
        fun(name: String?) {
            name
                ?.takeUnless { it.isBlank() } // name 이 빈 값이 아니면 name 리턴
                ?.let { println("$it in let()") }
        },
        fun(name: String?) {
            name
                ?.takeUnless { it.isBlank() }
                ?.run { println("$this in run()") }
        },
        fun(name: String?) {
            name
                ?.takeUnless { it.isBlank() }
                ?.apply { println("$this in apply()") }
        },
        fun(name: String?) {
            name
                ?.takeUnless { it.isBlank() }
                ?.also { println("$it in also()") }
        },
    )

fun main() {
    // 아무것도 출력되지 않음
    functions.forEach { it(null) }

    // 아무것도 출력되지 않음
    functions.forEach { it(" ") }

    // AHAHAHA in let()
    // AHAHAHA in run()
    // AHAHAHA in apply()
    // AHAHAHA in also()
    functions.forEach { it("AHAHAHA") }
}

익명 함수에 대한 좀 더 상세한 내용은 2.3. 익명 함수 를 참고하세요.

takeUnless() 는 predicate 가 true 이면 null 을 반환하고, false 이면 자기 자신인 this 를 반환한다.

takeUnless() 시그니처

public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
    return if (!predicate(this)) this else null
}

2.4. 영역 함수와 자원 해제 use()

자원 해제에 대한 좀 더 상세한 내용은 1. 자원 해제: use() 를 참고하세요.

영역 함수는 자원 해제 use() 와 비슷한 자원 해제를 제공하지는 못한다.

// AutoCloseable 인터페이스 구현
data class Blob(val id: Int) : AutoCloseable {
    override fun toString() = "Blob($id)"

    override fun close() = println("Close $this")

    fun show() = println("$this")
}

fun main() {
    Blob(1).let { it.show() } // Blob(1)
    Blob(2).run { show() } // Blob(2)
    with(Blob(3)) { show() } // Blob(3)
    Blob(4).apply { show() } // Blob(4)
    Blob(5).also { it.show() } // Blob(5)

    // Blob(6)
    // Close Blob(6)
    Blob(6).use { it.show() }

    // 영역 함수를 사용하면서 자원 해제를 보장하고 싶다면 영역 함수를 use() 람다 안에 사용해야 함
    // Blob(7)
    // Close Blob(7)
    Blob(7).use { it.run { show() } }

    // 명시적으로 close() 호출
    // Blob(8)
    // Close Blob(8)
    Blob(8).apply { show() }.also { it.close() }

    // 명시적으로 close() 호출
    // Blob(9)
    // Close Blob(9)
    Blob(9).also { it.show() }.apply { close() }

    // apply() 를 사용하고 그 결과를 use() 에 전달
    // 결과를 전달받은 use() 를 람다가 끝날 때 close() 호출
    // Blob(10)
    // Close Blob(10)
    Blob(10).apply { show() }.use {}
}

use()it 문맥 객체를 사용하는 let(), also() 와 비슷하지만, let(), also() 와 달리 람다에서 반환을 허용하지 않음


2.5. 영역 함수의 인라인: inline

람다를 인자로 전달하면 람다 코드를 외부 객체에 넣기 때문에 일반 함수 호출에 비해 실행 시점의 부가 비용이 좀 더 발생하지만, 람자가 주는 신뢰성과 코드 구조 개선에 비하면 이런 부가 비용은 크게 문제가 되지 않는다.

영역 함수를 inline 으로 만들면 모든 실행 시점의 부가 비용을 없앨 수 있다.

컴파일러는 inline 함수 호출을 보면 함수 호출 식을 함수의 본문으로 치환하며, 이 때 함수의 모든 파라메터를 실제 제공된 인자로 바꿔준다.

함수 실행 비용보다 함수 호출 비용이 큰 작은 함수의 경우 inline 이 효과적이다.
반면, 함수가 커질수록 전체 호출을 실행하는데 걸리는 시간에서 함수 호출이 차지하는 비중이 줄어들기 때문에 inline 의 가치가 하락하고, 함수가 크면 모든 함수 호출 지점에 함수 본문이 삽입되므로 컴파일된 전체 바이트 코드의 크기도 늘어난다.

인라인 함수가 람다를 인자로 받으면 컴파일러는 인라인 함수의 본문과 함께 람다 본문을 인라인해준다.
따라서 인라인 함수에 람다를 전달하는 경우 클래스나 객체가 추가로 생기지 않는다.

원하는 모든 함수에 inline 을 적용할 수 있지만, 일반적으로 inline 의 목적은 아래와 같다.

  • 함수 인자로 전달되는 람다를 인라이닝(예-영역 함수) 하거나 실체화한 제네릭스를 정의하는 것

제네릭스 정의에 대한 내용은 좀 더 상세한 내용은 Kotlin - 제네릭스 (타입 정보 보존, 제네릭 확장 함수, 타입 파라메터 제약, 타입 소거, ‘reified’, 타입 변성 애너테이션 (‘in’/’out’), 공변과 무공변) 을 참고하세요.


참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 브루스 에켈, 스베트라아 이사코바 저자의 아토믹 코틀린을 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10