Kotlin scope functions

rivermoon·2024년 10월 23일
post-thumbnail

스코프 함수?

Kotlin의 scope functions는 쉽게 말해서 객체의 context(범위) 내에서 코드를 더 간결하고 명확하게 작성할 수 있도록 도와준다.
이 함수들은 람다 표현식을 받아 객체에서 호출되면 임시 스코프를 형성하며, 이 스코프 안에서는 객체의 이름을 명시하지 않고도 접근할 수 있다.
let, run, with, apply, also 이렇게 5가지 scope functions이 있고, 각 함수는 객체를 다루는 방식에 약간의 차이가 있다.

임시 스코프란?
scope function이 객체와 함께 생성한 짧은 컨텍스트로, 이 안에서는 객체를 참조하기 위한 명시적 이름이 필요하지 않은 상태를 말한다.

이해를 돕기 위한 예시 코드

user.run {
    println(name)  // 'user'를 명시하지 않고도 'name'에 접근 가능
    println(age)
}

run 블록 안에서는 임시 스코프가 형성되어, 마치 user 객체가 바로 그 자리에서 사용되는 것처럼 작동한다. 따로 user.name이 아니라 그냥 name으로 접근할 수 있는 것.

scope function chart
이 표는 스코프 함수의 공통점과 차이점을 요약한 것 인데. 정리하자면,

공통점

  • 모든 함수는 객체 내 컨텍스트에서 코드 블록을 실행
  • 람다 표현식을 받음

차이점

  • let, alsoit 키워드로, 나머지는 this 키워드를 사용해 객체에 접근
  • apply, also는 객체 자신을 반환하고, 나머지는 람다 표현식의 결과를 반환
  • let, run(this), apply, also는 확장함수로 사용되지만(객체에서 바로 호출 가능)
    with, run(매개변수 없이 호출)은 확장함수가 아니다.(with은 객체를 인자로 받고 run은 인자 없이 호출)

스코프 함수의 축약적인 내용은 살펴봤고, 이제 각 스코프 함수가 어느 경우에 사용하면 좋은지, 예시 케이스와 설명을 기술해보겠다.


let

let은 객체를 인자로 받아 해당 객체에 대한 여러 작업을 수행하고, 람다의 결과값을 반환한다.
주로 체이닝된 함수 호출 결과를 처리하거나, nullable 객체에 대한 안전한 처리를 위해 사용된다.

KeyPoint

  • 객체 참조: it 키워드를 통해 객체에 접근
  • 반환 값: 람다 표현식의 결과를 반환

📄 사용 예시

1. 체이닝 된 결과 처리

val numbers = listOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
    println(it)  // [5, 5, 4]
}

컬렉션에서 길이가 3보다 큰 항목을 필터링 후 출력

체이닝(Chaining)
여러 메서드나 함수 호출을 연속적으로 이어서 호출하는 방식이다.

2. Method Reference

numbers.map { it.length }.filter { it > 3 }.let(::println)

단일 함수를 호출하는 경우, 람다 대신 메서드 참조를 사용할 수 있다.

3. null-safe 연산

val str: String? = "Hello"
str?.let {
    println("let() called on $it")
    it.length  // 'it'이 null이 아님을 보장
}

letnullable 객체에 대해 안전하게 작업을 수행할 때 빈번히 사용된다.
?.let 구문을 통해 객체가 null이 아닐 때만 작업을 진행할 수 있다.

4. 지역 변수 도입

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
    if (firstItem.length >= 5) firstItem else "!$firstItem!"
}.uppercase()
println(modifiedFirstItem)  // '!ONE!'

특정 블록 내에서만 사용할 임시 변수 도입

let에 대한 결론

  • 체이닝된 함수 호출 결과를 처리하거나, nullable 객체에 대한 안전한 처리를 위해 사용된다.
  • 안드로이드에서는, nullable 객체를 안전하게 처리하거나, View Binding, Intent 처리, 디버깅 등에서 빈번하게 사용될 수 있다.

with

with확장함수가 아닌 함수로, 객체를 인자로 받아 그 객체에 여러 작업을 수행할 수 있는 함수다.
람다 블록 안에서 객체는 this로 참조되며, 람다 블록의 결과값이 반환된다.
주로 객체를 변경하거나 메서드를 호출하는데 많이 사용되며, 반환값이 필요 없을 때 권장 된다.

KeyPoint

  • 객체 참조: this 키워드를 통해 객체에 접근
  • 반환 값: 람다 표현식의 결과를 반환
  • 확장 함수가 아님: 객체를 인자로 받으며, 람다 안에서 this로 참조

📄 사용 예시

1. 객체에 대해 작업 수행

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

특정 객체에 대해 여러 작업을 수행할 때 간편하게 사용 가능하다.

  1. 헬퍼 객체 사용
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}, the last element is ${last()}"
}
println(firstAndLast)

위 코드는 number 리스트의 첫 번째와 마지막 요소를 출력하는 문자열을 만드는 예시로, with을 통해 numbers 객체에 대해 연속적인 메서드 호출을 간단히 처리 가능하다.

with에 대한 결론

  • with은 객체에 대해 여러 작업을 수행하면서, 그 객체의 상태를 변경하거나 메서드를 호출할 때 매우 유용한 함수다.
  • 안드로이드에서는 View 속성 설정, RecyclerView ViewHolder, Fragment 초기화 작업에서 많이 사용된다.

run

run객체를 확장함수로 호출하여 특정 작업을 수행하고, 그 결과값을 반환하는 함수다.
with과 비슷하지만, run확장함수이기 때문에 객체에 점(.) 표기법으로 호출할 수 있다.
또한, 초기화 작업과 함께 결과 값을 계산할 때 유용하게 사용된다.

KeyPoint

  • 객체 참조: this 키워드를 사용해 객체에 접근
  • 반환 값: 람다 표현식의 결과를 반환
  • 확장 함수: 객체에 점 표기법을 사용해 호출 가능

📄 사용 예시

1. 객체 초기화 및 결과값 반환

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}
println(result)

네트워크 서비스 객체를 초기화한 후, 쿼리결과를 처리하는 예시코드.
service.runservice객체를 초기화한 후, port값을 수정하고, query( ) 메서드를 호출한 결과를 반환한다.
this 키워드를 통해 service속성과 메서드에 접근하고, 최종적으로 query결과를 리턴한다.

2. let과의 차이

val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

let은 it으로 접근하지만, run은 this로 객체에 접근한다.
run은 초기화 작업과 함께 결과를 계산하는 데 더 적합하다.

3. Non-extension run 사용

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"
    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
}

run은 확장 함수 뿐만 아니라 비확장 함수로도 사용할 수 있다.
이 경우, 객체 없이도 여러 작업을 수행하고 결과값을 반환할 수 있다.
비확장 run은 코드 블록을 실행하고 결과를 계산할 때 유용하게 사용될 수 있다.

run에 대한 결론

  • run은 객체의 초기화 작업과 결과 계산을 함께 처리해야 할 때 유용한 함수다.
  • 비확장 함수로도 사용할 수 있어 코드 블록을 실행하고 그 결과를 반환하는 데에도 유용하다.
  • 안드로이드에서는 객체 초기화, 설정, nullable 객체 처리, 네트워크 호출 결과 처리, 복잡한 계산 처리 등에서 활용될 수 있다.

apply

apply는 객체 자신을 반환하는 함수로, 주로 객체의 여러 속성을 초기화 하거나 설정할 때 사용된다.
this 키워드로 객체에 접근할 수 있고, 객체를 반환하기 때문에 객체의 설정 작업을 간결하게 처리할 수 있다.
주로 객체 구성과 관련된 작업에 자주 사용된다.

KeyPoint

  • 객체 참조: this키워드를 통해 객체에 접근
  • 반환 값: 객체 자신을 반환
  • 사용 목적: 반환 값이 필요없는 객체 설정 작업에 적합하며, 연속적인 객체 설정 작업에 자주 사용됨

📄 사용 예시

1. 객체 초기화 및 설정

val adam = Person("Adam").apply {
    age = 32
    city = "London"
}
println(adam)

객체의 여러 속성을 한꺼번에 설정.
apply를 사용해 adam객체의 속성인 agecity를 간결하게 설정 가능하다.
그리고 최종적으로 adam객체가 반환된다.

2. UI 요소 초기화

val button = Button(this).apply {
    text = "Click Me"
    textSize = 16f
    setOnClickListener {
        // 클릭 리스너 처리
    }
}

Button 객체의 여러 속성을 한 번에 설정하고, 최종적으로 button 객체를 반환.

3. 객체 구성 후 연속적인 호출

val config = Config().apply {
    url = "https://example.com"
    timeout = 5000
}.run {
    // run으로 추가 작업 처리
    fetchData(this)
}

apply속성을 통해 Config객체의 속성을 설정하고, 그 후 run을 통해 추가 작업을 진행 가능 하다. apply는 객체를 반환하므로, 연속적인 호출을 이어서 할 수 있음.

apply에 대한 결론

  • apply는 객체 구성 작업에서 매우 유용하며, 여러 속성을 설정한 후 객체를 반환하는 작업에 유용하다.
  • 안드로이드에서는 UI 초기화, 객체의 복잡한 설정에서 자주 사용되며, 반환값이 필요 없고 객체 설정에 집중해야 하는 상황에서 효과적이다.

also

also는 객체를 인자로 받아 여러 작업을 수행하고, 최종적으로 객체 자체를 반환하는 함수다.
객체에 대한 참조를 유지하며 부수적인 작업을 할 때 유용하다.
주로 디버깅, 로그 출력, 또는 객체를 처리하면서 추가적인 작업을 수행할 때 사용된다.

KeyPoint

  • 객체 참조: it 키워드를 통해 객체에 접근
  • 반환 값: 객체 자신을 반환
  • 사용 목적: 객체 참조를 유지하면서 부수적인 작업을 수행

📄 사용 예시

1. 부수적인 작업과 객체 반환

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding a new one: $it") }
    .add("four")
println(numbers)

리스트에 새로운 요소를 추가하기 전에 리스트의 상태를 출력하는 작업 수행.

2. 객체를 그대로 유지하면서 추가 작업 수행

val user = User("John", 30)
user.also {
    println("User details: $it")  // 객체를 그대로 출력하면서 로깅
}

user객체를 그대로 유지하면서 로그를 출력.

3. 메서드 체이닝에서 부수적인 작업 처리

val result = mutableListOf(1, 2, 3)
    .also { println("Before filtering: $it") }
    .filter { it > 1 }
    .also { println("After filtering: $it") }

필터링 작업을 하기 전에 리스트의 상태를 출력 하고, 필터링 후에도 리스트의 상태를 출력.

also에 대한 결론

  • also는 객체의 참조를 유지한 채 부수적인 작업을 수행해야 할 때 유용하다.
  • 안드로이드에서는 로그 출력, 디버깅, API 호출 전후 처리, 뷰 업데이트등의 상황에서 사용될 수 있다.

결과 표

함수객체 참조 방법반환 값확장 함수 여부주요 사용 목적
letit람다 결과 반환확장 함수객체 변환, null-safe 처리, 체이닝된 호출의 결과 처리
runthis람다 결과 반환확장 함수객체 초기화 및 결과 계산, 블록 내에서 코드 실행 후 결과 반환
withthis람다 결과 반환비확장 함수객체에 여러 작업을 수행할 때, 반환 값이 필요 없을 때
applythis객체 자신 반환확장 함수객체의 설정 및 초기화, 객체 구성 후 다시 반환
alsoit객체 자신 반환확장 함수부수적인 작업 (로깅, 디버깅), 객체 참조 유지

References

https://kotlinlang.org/docs/scope-functions.html

profile
Android Developer

1개의 댓글

comment-user-thumbnail
2024년 10월 26일

왜이렇게 잘생기셨나요...?

답글 달기