본문으로 건너뛰기

Scope 함수

코틀린 표준 라이브러리에는 객체의 컨텍스트 내에서 코드 블록을 실행하는 것을 유일한 목적으로 하는 여러 함수가 포함되어 있습니다. 제공된 람다 표현식으로 객체에 이러한 함수를 호출하면 임시 스코프가 형성됩니다. 이 스코프에서는 이름을 사용하지 않고 객체에 액세스할 수 있습니다. 이러한 함수를 _스코프 함수_라고 합니다. 이러한 함수에는 let, run, with, applyalso의 다섯 가지가 있습니다.

기본적으로 이러한 함수는 모두 동일한 작업을 수행합니다. 즉, 객체에서 코드 블록을 실행합니다. 다른 점은 이 객체가 블록 내부에서 사용 가능하게 되는 방식과 전체 표현식의 결과입니다.

다음은 스코프 함수를 사용하는 일반적인 예입니다.

data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}

fun main() {

Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}

}

let 없이 동일한 코드를 작성하면 새 변수를 도입하고 사용할 때마다 이름을 반복해야 합니다.

data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}

fun main() {

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

}

스코프 함수는 새로운 기술적 기능을 도입하지 않지만 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다.

스코프 함수 간에 유사점이 많기 때문에 사용 사례에 적합한 함수를 선택하기가 까다로울 수 있습니다. 선택은 주로 프로젝트에서 사용하려는 의도와 일관성에 따라 달라집니다. 아래에서는 스코프 함수 간의 차이점과 규칙에 대한 자세한 설명을 제공합니다.

함수 선택

목적에 맞는 올바른 스코프 함수를 선택하는 데 도움이 되도록 주요 차이점을 요약한 표를 제공합니다.

FunctionObject referenceReturn valueIs extension function
letitLambda resultYes
runthisLambda resultYes
run-Lambda resultNo: called without the context object
withthisLambda resultNo: takes the context object as an argument.
applythisContext objectYes
alsoitContext objectYes

이러한 함수에 대한 자세한 내용은 아래의 전용 섹션에 제공되어 있습니다.

다음은 의도된 목적에 따라 스코프 함수를 선택하기 위한 간단한 가이드입니다.

  • Null이 될 수 없는 객체에서 람다 실행: let
  • 로컬 스코프에서 표현식을 변수로 도입: let
  • 객체 구성: apply
  • 객체 구성 및 결과 계산: run
  • 표현식이 필요한 문 실행: 비확장 run
  • 추가 효과: also
  • 객체에서 함수 호출 그룹화: with

서로 다른 스코프 함수의 사용 사례가 겹치므로 프로젝트 또는 팀에서 사용하는 특정 규칙에 따라 사용할 함수를 선택할 수 있습니다.

스코프 함수를 사용하면 코드를 더 간결하게 만들 수 있지만 과도하게 사용하지 마십시오. 코드를 읽기 어렵게 만들고 오류를 유발할 수 있습니다. 또한 스코프 함수를 중첩하지 않고 연결할 때 주의하는 것이 좋습니다. 현재 컨텍스트 객체와 this 또는 it의 값을 혼동하기 쉽기 때문입니다.

차이점

스코프 함수는 본질적으로 유사하기 때문에 함수 간의 차이점을 이해하는 것이 중요합니다. 각 스코프 함수 간에는 두 가지 주요 차이점이 있습니다.

  • 컨텍스트 객체를 참조하는 방식
  • 반환 값

컨텍스트 객체: this 또는 it

스코프 함수에 전달된 람다 내부에서 컨텍스트 객체는 실제 이름 대신 짧은 참조로 사용할 수 있습니다. 각 스코프 함수는 람다 수신자(this) 또는 람다 인수(it)로 컨텍스트 객체를 참조하는 두 가지 방법 중 하나를 사용합니다. 둘 다 동일한 기능을 제공하므로 다양한 사용 사례에 대한 장단점을 설명하고 사용 권장 사항을 제공합니다.

fun main() {
val str = "Hello"
// this
str.run {
println("The string's length: $length")
//println("The string's length: ${this.length}") // does the same
}

// it
str.let {
println("The string's length is ${it.length}")
}
}

this

run, withapply는 컨텍스트 객체를 키워드 this로 람다 수신자로 참조합니다. 따라서 람다에서 객체는 일반적인 클래스 함수에서와 같이 사용할 수 있습니다.

대부분의 경우 수신자 객체의 멤버에 액세스할 때 this를 생략하여 코드를 더 짧게 만들 수 있습니다. 반면에 this를 생략하면 수신자 멤버와 외부 객체 또는 함수를 구별하기 어려울 수 있습니다. 따라서 컨텍스트 객체를 수신자(this)로 사용하는 것은 해당 함수를 호출하거나 속성에 값을 할당하여 객체의 멤버에서 주로 작동하는 람다에 권장됩니다.

data class Person(var name: String, var age: Int = 0, var city: String = "")

fun main() {

val adam = Person("Adam").apply {
age = 20 // same as this.age = 20
city = "London"
}
println(adam)

}

it

반면에 letalso는 컨텍스트 객체를 람다 인수로 참조합니다. 인수 이름을 지정하지 않으면 객체는 암시적 기본 이름 it로 액세스됩니다. itthis보다 짧고 it을 사용하는 표현식은 일반적으로 읽기가 더 쉽습니다.

그러나 객체의 함수 또는 속성을 호출할 때 this처럼 암시적으로 사용할 수 있는 객체가 없습니다. 따라서 객체가 함수 호출에서 인수로 주로 사용되는 경우 it을 통해 컨텍스트 객체에 액세스하는 것이 좋습니다. 코드 블록에서 여러 변수를 사용하는 경우에도 it이 더 좋습니다.

import kotlin.random.Random

fun writeToLog(message: String) {
println("INFO: $message")
}

fun main() {

fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}

val i = getRandomInt()
println(i)

}

아래 예제에서는 인수 이름 value를 사용하여 람다 인수로 컨텍스트 객체를 참조하는 방법을 보여 줍니다.

import kotlin.random.Random

fun writeToLog(message: String) {
println("INFO: $message")
}

fun main() {

fun getRandomInt(): Int {
return Random.nextInt(100).also { value `->`
writeToLog("getRandomInt() generated value $value")
}
}

val i = getRandomInt()
println(i)

}

반환 값

스코프 함수는 반환하는 결과가 다릅니다.

  • applyalso는 컨텍스트 객체를 반환합니다.
  • let, runwith는 람다 결과를 반환합니다.

코드에서 다음에 수행하려는 작업을 기반으로 원하는 반환 값을 신중하게 고려해야 합니다. 이렇게 하면 사용할 최상의 스코프 함수를 선택하는 데 도움이 됩니다.

컨텍스트 객체

applyalso의 반환 값은 컨텍스트 객체 자체입니다. 따라서 호출 체인에 _사이드 스텝_으로 포함될 수 있습니다. 동일한 객체에서 함수 호출을 차례로 계속 연결할 수 있습니다.

fun main() {

val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") }
.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") }
.sort()

println(numberList)
}

컨텍스트 객체를 반환하는 함수의 반환 문에서 사용할 수도 있습니다.

import kotlin.random.Random

fun writeToLog(message: String) {
println("INFO: $message")
}

fun main() {

fun getRandomInt(): Int {
return Random.nextInt(100).also {
writeToLog("getRandomInt() generated value $it")
}
}

val i = getRandomInt()

}

람다 결과

let, runwith는 람다 결과를 반환합니다. 따라서 결과를 변수에 할당하거나 결과에 대한 작업을 연결하는 등의 경우에 사용할 수 있습니다.

fun main() {

val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.")

}

또한 반환 값을 무시하고 스코프 함수를 사용하여 로컬 변수에 대한 임시 스코프를 만들 수 있습니다.

fun main() {

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first()
val lastItem = last()
println("First item: $firstItem, last item: $lastItem")
}

}

함수

사용 사례에 맞는 올바른 스코프 함수를 선택하는 데 도움이 되도록 자세히 설명하고 사용 권장 사항을 제공합니다. 기술적으로 스코프 함수는 여러 경우에 상호 교환 가능하므로 예제에서는 해당 함수를 사용하는 규칙을 보여 줍니다.

let

  • 컨텍스트 객체는 인수(it)로 사용할 수 있습니다.
  • 반환 값은 람다 결과입니다.

let은 호출 체인의 결과에 대해 하나 이상의 함수를 호출하는 데 사용할 수 있습니다. 예를 들어 다음 코드는 컬렉션에서 두 작업의 결과를 인쇄합니다.

fun main() {

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

}

let을 사용하면 목록 작업의 결과를 변수에 할당하지 않도록 위의 예제를 다시 작성할 수 있습니다.

fun main() {

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it)
// and more function calls if needed
}

}

let에 전달된 코드 블록에 it을 인수로 사용하는 단일 함수가 포함된 경우 람다 인수 대신 메서드 참조(::)를 사용할 수 있습니다.

fun main() {

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

}

let은 일반적으로 null이 아닌 값을 포함하는 코드 블록을 실행하는 데 사용됩니다. null이 아닌 객체에 대해 작업을 수행하려면 안전 호출 연산자 ?.를 사용하여 let을 해당 람다의 작업과 함께 호출합니다.

fun processNonNullString(str: String) {}

fun main() {

val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let {
println("let() called on $it")
processNonNullString(it) // OK: 'it' is not null inside '?.let { }'
it.length
}

}

let을 사용하여 코드를 더 읽기 쉽게 만들 수 있는 제한된 범위의 로컬 변수를 도입할 수도 있습니다. 컨텍스트 객체에 대한 새 변수를 정의하려면 해당 이름을 람다 인수로 제공하여 기본 it 대신 사용할 수 있도록 합니다.

fun main() {

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem `->`
println("The first item of the list is '$firstItem'")
if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")

}

with

  • 컨텍스트 객체는 수신자(this)로 사용할 수 있습니다.
  • 반환 값은 람다 결과입니다.

with는 확장 함수가 아니므로 컨텍스트 객체는 인수로 전달되지만 람다 내부에서는 수신자(this)로 사용할 수 있습니다.

반환된 결과를 사용할 필요가 없는 경우 컨텍스트 객체에서 함수를 호출하는 데 with를 사용하는 것이 좋습니다. 코드에서 with는 "이 객체를 사용하여 다음을 수행합니다."로 읽을 수 있습니다.

fun main() {

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

}

with를 사용하여 값을 계산하는 데 사용되는 속성 또는 함수가 있는 도우미 객체를 도입할 수도 있습니다.

fun main() {

val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}," +
" the last element is ${last()}"
}
println(firstAndLast)

}

run

  • 컨텍스트 객체는 수신자(this)로 사용할 수 있습니다.
  • 반환 값은 람다 결과입니다.

runwith와 동일한 작업을 수행하지만 확장 함수로 구현됩니다. 따라서 let과 마찬가지로 점 표기법을 사용하여 컨텍스트 객체에서 호출할 수 있습니다.

run은 람다가 객체를 초기화하고 반환 값을 계산하는 경우에 유용합니다.

class MultiportService(var url: String, var port: Int) {
fun prepareRequest(): String = "Default request"
fun query(request: String): String = "Result for query '$request'"
}

fun main() {

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

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

// the same code written with let() function:
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}

println(result)
println(letResult)
}

run을 비확장 함수로 호출할 수도 있습니다. 비확장 run 변형에는 컨텍스트 객체가 없지만 여전히 람다 결과를 반환합니다. 비확장 run을 사용하면 표현식이 필요한 위치에서 여러 문의 블록을 실행할 수 있습니다. 코드에서 비확장 run은 "코드 블록을 실행하고 결과를 계산합니다."로 읽을 수 있습니다.

fun main() {

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)
}

}

apply

  • 컨텍스트 객체는 수신자(this)로 사용할 수 있습니다.
  • 반환 값은 객체 자체입니다.

apply는 컨텍스트 객체 자체를 반환하므로 값을 반환하지 않고 주로 수신자 객체의 멤버에서 작동하는 코드 블록에 사용하는 것이 좋습니다. apply의 가장 일반적인 사용 사례는 객체 구성입니다. 이러한 호출은 "객체에 다음 할당을 적용합니다."로 읽을 수 있습니다.

data class Person(var name: String, var age: Int = 0, var city: String = "")

fun main() {

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

}

apply의 또 다른 사용 사례는 더 복잡한 처리를 위해 여러 호출 체인에 apply를 포함하는 것입니다.

also

  • 컨텍스트 객체는 인수(it)로 사용할 수 있습니다.
  • 반환 값은 객체 자체입니다.

also는 컨텍스트 객체를 인수로 사용하는 일부 작업을 수행하는 데 유용합니다. 속성 및 함수가 아닌 객체에 대한 참조가 필요하거나 외부 스코프에서 this 참조를 섀도 처리하지 않으려는 경우 also를 사용합니다.

코드에서 also가 표시되면 "객체를 사용하여 다음도 수행합니다."로 읽을 수 있습니다.

fun main() {

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

}

takeIf 및 takeUnless

스코프 함수 외에도 표준 라이브러리에는 함수 takeIftakeUnless가 포함되어 있습니다. 이러한 함수를 사용하면 호출 체인에 객체 상태 검사를 포함할 수 있습니다.

객체에서 조건자와 함께 호출되면 takeIf는 지정된 조건자를 충족하는 경우 이 객체를 반환합니다. 그렇지 않으면 null을 반환합니다. 따라서 takeIf는 단일 객체에 대한 필터링 함수입니다.

takeUnlesstakeIf와 반대되는 논리를 가지고 있습니다. 객체에서 조건자와 함께 호출되면 takeUnless는 지정된 조건자를 충족하는 경우 null을 반환합니다. 그렇지 않으면 객체를 반환합니다.

takeIf 또는 takeUnless를 사용하는 경우 객체는 람다 인수(it)로 사용할 수 있습니다.

import kotlin.random.*

fun main() {

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

}

takeIftakeUnless 뒤에 다른 함수를 연결할 때 반환 값은 null이 될 수 있으므로 null 검사를 수행하거나 안전 호출(?.)을 사용하는 것을 잊지 마십시오.

fun main() {

val str = "Hello"
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
//val caps = str.takeIf { it.isNotEmpty() }.uppercase() //compilation error
println(caps)

}

takeIftakeUnless는 특히 스코프 함수와 함께 사용하면 유용합니다. 예를 들어 takeIftakeUnlesslet과 연결하여 지정된 조건자와 일치하는 객체에서 코드 블록을 실행할 수 있습니다. 이렇게 하려면 객체에서 takeIf를 호출한 다음 안전 호출(?)로 let을 호출합니다. 조건자와 일치하지 않는 객체의 경우 takeIfnull을 반환하고 let은 호출되지 않습니다.

fun main() {

fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

}

비교를 위해 아래는 takeIf 또는 스코프 함수를 사용하지 않고 동일한 함수를 작성할 수 있는 방법에 대한 예입니다.

fun main() {

fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub)
if (index >= 0) {
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}

displaySubstringPosition("010000011", "11")
displaySubstringPosition("010000011", "12")

}