-
Scope Function BasicKotlin 2023. 3. 6. 21:18
안녕하세요 안드로이드 12기 주혜진입니다~ 회사에서 리뷰 받을 때 주로 제가 스코프 함수를 잘못? 사용해서 권장되는 사용법을 알려주시는 코멘트를 많이 받았었는데요~ 그래서 요번 블로그 주제로 스코프 함수로 정해봤습니다! 이전에 스코프 함수 글이 있긴 하지만! 저는 공식문서를 번역하면서 공부한 글이라 쉽게 쓱 읽기 좋을 것 같아요! 그럼 본론으로 고고!
목적
- Executing a lambda on non-null objects: let
- Introducing an expression as a variable in local scope: let
- Object configuration: apply
- Object configuration and computing the result: run
- Running statements where an expression is required: non-extension run
- Additional effects: also
- Grouping function calls on an object: with
코틀린 스코프 함수는 객체의 context 내에서 코드를 실행하는 유일한 목적이 있습니다. 람다식을 갖는 객체에 대해 이러한 함수를 call 할 때, 임시적인 스코프로 형성합니다. 스코프 안에서 객체를 표현하는 이름 없이 접근할 수 있습니다. 이런 함수를 스코프 함수라 부르고 종류는 5개입니다.
let, run, with, apply, also
기본적으로 함수는 같은 역할을 수행합니다. : 객체위에 코드 블락을 실행함
다른 점은 이 객체가 블록 내부에서 어떻게 사용 가능하게 되는지와 전체 표현식의 결과가 무엇인지입니다.
Person("Alice", 20, "Amsterdam").let { println(it) it.moveTo("London") it.incrementAge() println(it) }
Person(name=Alice, age=20, city=Amsterdam) Person(name=Alice, age=21, city=London)
만약 같은 기능을 let 없이 사용한다면 새로운 변수를 만들고 사용할 때 마다 그 이름을 반복해야 합니다.
val alice = Person("Alice", 20, "Amsterdam") println(alice) alice.moveTo("London") alice.incrementAge() println(alice)
Person(name=Alice, age=20, city=Amsterdam) Person(name=Alice, age=21, city=London)
스코프 함수의 유사한 특성 때문에 이들 사이의 주요 차이점을 아는 것이 중요합니다 주요 차이점은 두 가지가 있습니다.
- context 객체를 참조하는 방법
- 리턴 값
Context Object: this or it
람다 스코프 함수 안에 context 객체는 그것의 실제 이름을 사용하는 대신에 짧은 참조를 이용될 수 있습니다. 각 스코프 함수는 context object에 접근하는 방법인 두 개 중 하나를 사용합니다. 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}") } }
The string’s length: 5 The string’s length is 5
this
run, with 그리고 apply는 context this 키워드로부터 객체를 람다 수신자로 참조합니다. 그러므로 람다 안에서 객체는 일반 클래스 함수 안에서 사용할 수 있습니다. 대부분의 경우 수신 객체의 멤버에 접근할 때 this를 제거할 수 있습니다. 그러나 this는 수신 객체와 외부 객체 또는 함수를 구별하기 어렵습니다. 그래서 context object를 수신 객체인 this로 갖는 것은 주로 객체의 멤버를 계산하는 람다씩(객체 함수의 호출, 또는 프로퍼티에 값 할당)에 추천합니다.
val adam = Person("Adam").apply { age = 20 // same as this.age = 20 or adam.age = 20 city = "London" } println(adam)
Person(name=Adam, age=20, city=London)
it
결국 let, also는 context 객체를 람다 변수로 갖습니다. 만약 변수 이름이 명시되지 않았다면, 객체는 암시적 기본 이름인 it으로 접근할 수 있습니다. it은 this보다 더 짧고 it을 갖은 표현은 읽기 편합니다. 하지만 객체의 함수나 프로퍼티들을 호출할 때 this처럼 암시적으로 이용할 수 있는 객체가 없습니다. 그러나 context 객체를 it으로 갖는 것은 객체가 함수 호출 안에서 변수로 사용될 때 더 좋습니다. it은 또한 코드 블록의 다양한 변수를 사용할 때 더 좋습니다.
fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } } val i = getRandomInt() println(i)
INFO: getRandomInt() generated value 99 99
아래 예제는 it을 사용하여 코드 블록의 다양한 변수를 사용한 경우 입니다.
data class Apple(var weight: Int) class AppleTree(val appleTree: List<Apple>) { fun pick(): Apple { return appleTree[0] } } //여기서 apply를 사용하는 경우 this는 가려지게 되고 this.weight은 fruteBasket이 아닌 apple을 참조한다. class FruitBasket { private var weight = 0 fun addFrom(appleTree: AppleTree) { val apple = appleTree.pick().let { it -> this.weight += it.weight add(it) } } fun add(apple: Apple) { } }
추가로 context object를 변수로 전달할 때 스코프 안에서 context object의 custom name을 전달할 수 있습니다.
fun getRandomInt(): Int { return Random.nextInt(100).also { value -> writeToLog("getRandomInt() generated value $value") } } val i = getRandomInt() println(i)
return value
스코프 함수는 리턴되는 값에 따라 다릅니다
- apply, also는 context 객체를 리턴
- let, run, 그리고 with은 lamda의 결과를 리턴
Context Obejct
apply와 also는 자기자신의 context 객체를 리턴합니다. 그러므로 그것은 call chain에 사이드 스텝으로써 포함될 수 있습니다: 함수 호출 후 동일한 개체에 대한 함수 호출을 계속할 수 있습니다.
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()
또한 context 객체를 리턴하는 함수의 문으로 사용될 수 있습니다.
fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } } val i = getRandomInt()
INFO: getRandomInt() generated value 55
Lambda result
let, run, and with은 람다 결과를 반환합니다. 따라서 변수에 결과를 할당하거나 결과에 대한 연산을 연결하는 등의 작업을 수행할 때 사용할 수 있습니다.
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.")
There are 3 elements that end with e.
추가로 리턴 값을 무시하거나 변수에 대한 임시적인 스코프를 갖는 스코프 함수를 사용할 때 사용합니다.
val numbers = mutableListOf("one", "two", "three") with(numbers) { val firstItem = first() val lastItem = last() println("First item: $firstItem, last item: $lastItem") }
First item: one, last item: three
Funtion
일반적이 사용 스타일을 정의하는 규칙을 아래 예제에서 확인 가능 합니다.
let
호출 체인의 결과에 대해 하나 이상의 함수를 호출할 수 있습니다. 예를 들어, 다음 코드는 컬렉션에 대한 두 가지 작업의 결과를 출력합니다.
val numbers = mutableListOf("one", "two", "three", "four", "five") val resultList = numbers.map { it.length }.filter { it > 3 } println(resultList)
[5, 4, 4]
let 사용 시
val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let { println(it) // and more function calls if needed }
[5, 4, 4]
null 검사에도 사용합니다. null이 될 수 없는 타입을 파라미터로 받는 함수에 널이 될 수 있는 타입의 값을 넘길 수는 없습니다. 따라서 let함수를 통해 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 값으로 바꿔서 전달하기 위해 사용합니다.
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() called on Hello
또한 코드 가독성을 높이기 위해서 제한된 범위의 로컬변수로도 사용합니다. 이를 위해서는 it대신 lambda 변수 이름으로 제공해야 합니다.
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'")
The first item of the list is ‘one’ First item after modifications: ‘!ONE!’
with
비 확장 함수 : context 객체는 인수로 전달되지만, lambda 내부에 수신기로 사용할 수 있습니다. 반환 값은 lambda 결과입니다. 람다 결과를 제공하지 않고 context 객체의 함수, 프로퍼티를 호출하려면 사용하는 것이 좋습니다. 코드에서는 “이 객체를 사용하여 다음을 수행합니다”로 읽습니다.
val numbers = mutableListOf("one", "two", "three") with(numbers) { println("'with' is called with argument $this") println("It contains $size elements") }
‘with’ is called with argument [one, two, three] It contains 3 elements
또 다른 사용 사례는 값을 계산하는 데 사용할 프로퍼티 또는 함수를 사용하는 도우미 객체를 도입하는 것입니다.
val numbers = mutableListOf("one", "two", "three") val firstAndLast = with(numbers) { "The first element is ${first()}," + " the last element is ${last()}" } println(firstAndLast)
The first element is one, the last element is three
run
context 객체를 lambda 수신기로 사용할 수 있습니다. 반환 값은 람다 결과입니다. 실행은 let과 동일한 기능을 수행하지만, 문맥 개체의 확장 함수로 let을 호출합니다. (추가적인 연산을 진행하고 그값을 반환하는…) run은 람다에 객체 초기화 및 반환 값 계산이 모두 포함된 경우에 유용합니다.
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}") }
Result for query ‘Default request to port 8080’ Result for query ‘Default request to port 8080’
확장자가 아닌 함수로 사용할 수 있습니다. 비 확장 실행을 사용하면 식이 필요한 경우 여러 문의 블록을 실행할 수 있습니다.
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) }
+123 -FFFF 88
apply
값을 반환하지 않고 주로 수신기 객체의 멤버에서 작동하는 코드 블록에 대해 apply를 사용합니다. 일반적인 경우는 객체 구성입니다. 이러한 호출은 “객체에 다음 할당을 적용”으로 읽을 수 있습니다. 또한 chain도 적용할 수 있습니다.
val adam = Person("Adam").apply { age = 32 city = "London" } println(adam)
Person(name=Adam, age=32, city=London)
also
객체를 인수로 사용하고 객체를 반환합니다. 객체의 속성 및 기능 대신 참조 객체의 참조가 필요한 작업이나 외부 범위에서 this에 의해 가려지지 않으려면 사용합니다. 코드에서도 “객체에 대해 다음 작업을 수행할 수도 있습니다.”라고 읽을 수 있습니다.
l numbers = mutableListOf("one", "two", "three") numbers .also { println("The list elements before adding new one: $it") } .add("four")
The list elements before adding new one: [one, two, three]
참고
https://kotlinlang.org/docs/scope-functions.html#function-selection
'Kotlin' 카테고리의 다른 글
Kotlin Delegation 분석 (0) 2023.03.06 Kotlinx-Serialization (0) 2023.03.06 확장 함수, 람다 함수, 고차 함수의 기초 (0) 2023.03.06 How do Kotlin coroutines work internally (0) 2023.03.06 Coroutine Shared mutable state and concurrency (0) 2023.03.05