Scope functions
코틀린 표준 라이브러리는 객체의 컨텍스트 내의 코드 블록을 실행하는 것이 유일한 목적인 몇몇 함수들을 가지고 있다.
이러한 함수들을 람다식과 함께 객체에 호출하면 일시적인 범위를 생성한다.
이 범위 안에서는 객체의 이름 없이 객체에 접근할 수 있다.
이러한 함수들을 scope functions라고 부른다.
let, run, with, apply, also가 있다.
기본적으로 이런 함수들은 공통적으로 객체에 대한 코드 블록을 실행한다.
다른 점은 블록내에서 객체를 어떻게 사용 가능한지와 전체 expression의 결과가 무엇인지이다.
data class Person(var name: String, var age: Int, var city: String) {
fun moveTo(newCity: String) { city = newCity }
fun incrementAge() { age++ }
}
fun main() {
// scope function let 사용
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
// scope function let 미사용
val alice = Person("Alice", 20, "Amsterdam") // 새로운 변수를 선언하고
println(alice)
alice.moveTo("London")// 사용할 때마다 이름을 써줘야 함.
alice.incrementAge()
println(alice)
}
scope functions은 새로운 기술력을 소개하지 않지만 코드를 더 간결하고 읽기 쉽게 할 수 있다.
scope functions간 공통점이 많기 때문에 알맞은 것을 고르는 것이 어려울 수 있다.
주로 의도와 프로젝트 내 사용의 일관성에 따라 선택하면 된다.
Function selection
함수 | 객체 참조 | 반환 값 | extension function인지? |
let | it | 람다식 결과 | 그렇다 |
run | this | 람다식 결과 | 그렇다 |
run | - | 람다식 결과 | 아니다: 컨텍스트 객체 없이 호출함 |
with | this | 람다식 결과 | 아니다: 컨텍스트 객체를 인자로 갖는다. |
apply | this | 컨텍스트 객체 | 그렇다 |
also | it | 컨택스트 객체 | 그렇다 |
목적에 따른 scope function 선택 가이드
- non-null 객체에 람다식을 실행할 때: let
- 지역 범위에서 expression을 변수로 도입할 때: let
- 객체 구성: apply
- 객체 구성과 결과 계산: run
- expression이 필요한 곳에서 명령문 실행: 표에서 두번째 run (extension이 아닌 run)
- 객체에 함수호출을 모아서 할 때: with
다른 스코프함수들의 사용하는 경우가 겹치기 때문에 특정한 관습이나 규칙에 따라서 함수들을 골라 사용할 수 있다.
스코프 함수가 코드를 간단하게 해주지만 너무 남용하면 안 된다.. 코드를 읽기 어렵게 만들고 오류를 유발할 수 있다. 현재 컨텍스트 객체와 this나 it이 무엇을 가리키는지 헷갈리므로 스코프함수를 겹쳐 쓰는 것을 피하고 연달아 쓸때 주의해야 한다.
Distinctions
스코프함수끼리 비슷하기 때문에 차이점들을 알고 있어야 한다. 주로
- 컨텍스트 객체 참조 방법
- 반환값
이 두가지가 다르다.
Context object: this or it
스코프함수로 넘어간 람다 내부에서 컨텍스트 객체는 실제 이름 대신 짧은 참조로 쓸 수 있다.
각 스토프 함수는 컨텍스트 객체를 사용하는 두 가지 방법 중 하나를 사용한다.
lambda receiver(this)나 lambda agrument(it)
리시버
출처: https://jaeyeong951.medium.com/kotlin-lambda-with-receiver-5c2cccd8265a
[Kotlin] Lambda with receiver
Lambda with receiver : 수신 객체 지정 람다
jaeyeong951.medium.com
block: T.() -> R
객체 T를 receiver로 이용하여 객체 R 반환.
this: 객체 외부가 아니라 객체 내부에 있는 듯한 인상 (생략 가능)
block: (T) -> R
여기서 객체 T는 리시버가 아니라 람다 파라미터로 받는다.
it: 객체 외부에서 해당 객체에 접근한다는 인상
this
run, with, apply는 lambda receiver로 컨텍스트 객체를 참조한다. : this
따라서 람다 내부에서 객체를 평범한 클래스 함수 내부에 있는 것처럼 쓸 수 있다.
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").apply {
age = 20
// this는 생략 가능. this.age = 20와 같은 의미.
city = "London"
}
println(adam)
}
this를 생략했을 때: 리시버 멤버와 외부 객체나 함수와 구분이 어려울 수 있음.
따라서 멤버 함수를 호출하거나 프로퍼티에 값을 할당함으로써 주로 객체의 멤버들을 작동하는 람다에 this를 사용하는 것이 좋다. (내 해석: 주로 객체 멤버를 조작한다면 람다에 this를 쓰자)
it
람다식이 하나의 파라미터만 가질 때가 많다.
컴파일러가 다른 파라미터 없이 this signature(?)을 파싱할 수 있을 때, 파라미터는 선언되지 않아도 되고, ->도 생략될 수 있다.
해당 파라미터는 it으로 암시적으로 선언된다.
https://kotlinlang.org/docs/lambdas.html#it-implicit-name-of-a-single-parameter
let과 also는 lambda argument로 컨텍스트 객체를 참조한다.
만약 인자 이름이 지정되지 않으면, 객체에 암시적인 기본 이름 it으로 접근할 수 있다.
it이 this보다 짧고, it을 사용한 expressions이 보통 더 읽기 쉽다.
그러나 객체의 함수나 프로퍼티를 호출할 때 this처럼 암시적으로 사용 가능한 객체가 없다.
따라서 객체가 주로 함수 호출의 인자로 사용될 때는 it으로 컨텍스트 객체에 접근하는 것이 좋다.
(실험을 해 봤는데...
it으로 프로퍼티에 접근할 수 있다.
data class Person(var name: String, var age: Int = 0, var city: String = "")
fun main() {
val adam = Person("Adam").also {
it.age = 20 // it으로 프로퍼티에 접근 가능하다. 다만 it 생략 불가
it.city = "London"
}
println(adam)
// Person(name=Adam, age=20, city=London)
}
)
코드 블럭 내에서 여러 변수를 사용할 때 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")
}
/*
return Random.nextInt(100).also { value -> // it 대신 이름을 직접 지정할 수 있다.
writeToLog("getRandomInt() generated value $value")
}
*/
}
val i = getRandomInt()
println(i)
}
Return value
스코프 함수는 반환 결과에 따라 다르다
- apply와 also는 컨텍스트 객체를 반환한다.
- let, run, with은 람다 결과값을 반환한다.
코드의 다음에 무엇이 오길 바라는지에 따라 어떤 것을 반환값으로 할지 신중하게 결정해야 한다.
Context object
apply, also는 컨텍스트 객체 자신을 반환한다.
따라서 연쇄 호출에 side steps으로 포함될 수 있다.
같은 객체에 대해 연속적으로 함수들을 호출할 수 있다.
fun main() {
val numberList = mutableListOf<Double>()
numberList.also { println("Populating the list") } // also로 문구 출력.. numberList 반환
.apply { // apply로 함수 호출하여 객체 numberList 조작.. numberList 반환
add(2.71)
add(3.14)
add(1.0)
}
.also { println("Sorting the list") } // also로 문구 출력.. numberList 반환
.sort() // numberList 정렬
println(numberList) // numberList 출력
}
/*
Populating the list
Sorting the list
[1.0, 2.71, 3.14]
*/
컨텍스트 객체를 반환하는 함수의 return 문에 사용할 수 있다. 위 위 코드 참조.
Lambda result
let, run, with은 람다 결과값을 반환한다.
람다 반환값?
람다의 반환 타입이 Unit이 아니라면, 람다식 내부의 마지막 expression이 람다의 반환값이 된다.
https://kotlinlang.org/docs/lambdas.html#lambda-expression-syntax
결과를 변수에 할당할 때나 결과에 계속해서 이어서 연산을 할 때 쓸 수 있다.
fun main() {
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four") // numbers에 "four" 추가
add("five") // numbers에 "five" 추가
count { it.endsWith("e") } // "e"로 끝나는 단어 개수 세기
}// "e"로 끝나는 단어 개수가 저장됨.
println("There are $countEndsWithE elements that end with e.")
// There are 3 elements that end with e.
}
추가적으로, 반환값을 무시할 수 있고, 스코프 함수를 로컬 변수를 위한 일시적인 범위를 만드는 데 쓸 수 있다.
fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
val firstItem = first() // this 생략 가능.. this.first()
val lastItem = last() // this 생략 가능.. this.last()
println("First item: $firstItem, last item: $lastItem")
// First item: one, last item: three
}
}
Functions
기술적으로 스코프 함수들은 많은 경우에 서로 대체될 수 있기 때문에 다음 예시들은 스코프 함수를 사용하는 데 있어 관습을 보여준다.
let
- 컨텍스트 오브젝트는 인수 it으로 사용할 수 있다.
- 반환 값은 람다 결과값이다.
호출 체인의 결과에 대해 하나 이상의 함수를 호출하는 데 사용할 수 있다.
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
// let 사용
numbers.map { it.length }.filter { it > 3 }.let {
// 1. 각 요소들의 길이를 구하고
// 2. 3보다 큰 수만 남김.
println(it) // let을 이용해 그 결과를 출력
// [5, 4, 4]
}
/*
let 미사용
val resultList = numbers.map { it.length }.filter { it > 3 }
// 1. 각 요소들의 길이를 구하고
// 2. 3보다 큰 수만 남겨서 resultList에 저장
println(resultList)
// [5, 4, 4]
*/
}
fun main() {
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)
// let이 it을 인자로 갖는 하나의 함수를 포함한다면 method reference ::을 람다 인자 대신 사용할 수 있다.
}
fun processNonNullString(str: String) {}
fun main() { // let은 코드 블럭이 non-null 값들을 포함할 때 자주 쓴다.
val str: String? = "Hello"
//processNonNullString(str) // compilation error: str can be null
val length = str?.let { // safe call operator ?.을 사용한다. str이 null 값을 가지면 let 내부 코드는 실행되지 않고length에는 null이 저장됨.
println("let() called on $it")
// let() called on Hello
processNonNullString(it) // it은 null이 아니다.
it.length
}
}
fun main() {
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem -> // it 대신 다른 이름을 쓸 수 있다.
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은 extension 함수가 아니기 때문에 컨텍스트 객체는 인자로 넘겨지지만, 람다식 내부에서 리시버 this로 사용할 수 있다.
반횐된 결과를 사용할 필요가 없을 경우, 컨텍스트 객체에 대해 함수를 부를 때 with을 사용하는 것을 추천한다.
코드상에서 with은, "이 객체로, 다음을 수행해라"라고 읽을 수 있다.
fun main() {
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
// 'with' is called with argument [one, two, three]
println("It contains $size elements")
// It contains 3 elements
}
}
fun main() {
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
// 값을 계산하는 데 쓰일 프로퍼티나 함수를 갖는 helper object를 with으로 도입할 수 있다.
"The first element is ${first()}," + // this.first()로 해도 됨.
" the last element is ${last()}" // this.last()로 해도 됨.
}
println(firstAndLast)
// The first element is one, the last element is three
}
run
- 컨텍스트 객체는 리시버 this로 쓸 수 있다.
- 반환값은 람다 결과값이다
run은 with과 같은 동작을 하지만 extension 함수로 구현되었다.
따라서 let처럼 dot notation을 사용하여 컨텍스트 객체에 대해 run을 호출할 수 있다.
람다가 객체를 초기화하고 반환값을 계산할 때 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 // service의 port 값을 8080으로 수정
query(prepareRequest() + " to port $port") // query 함수의 반환값을 result에 저장
}
// the same code written with let() function:
val letResult = service.let {
it.port = 8080 // service의 port값을 8080으로 수정
it.query(it.prepareRequest() + " to port ${it.port}") // query 함수의 반환값을 result에 저장
}
println(result)
// Result for query 'Default request to port 8080'
println(letResult)
// Result for query 'Default request to port 8080'
}
run을 extension이 아닌 함수로 사용할 수 있다.
이 경우, 컨텍스트 객체는 없지만 여전히 람다 결과를 반환한다.
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)
/*
+123
-FFFF
88
*/
}
}
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와 city의 기본값이 설정되어 있지 않으면 오류 발생.
age = 32 // this.age = 32와 같은 의미.
city = "London"
}
println(adam)
// Person(name=Adam, age=32, city=London)
}
also
- 컨텍스트 객체는 인자 it으로 쓸 수 있다.
- 반환 값은 객체 그 자체다.
컨텍스트 객체를 인자로 취하는 작업을 수행할 때 유용하다.
객체의 프로퍼티나 함수보단 객체를 참조할 필요가 있는 작업이나 외부에서 this 참조를 숨기고 싶지 않은 경우에 also를 사용한다.
코드에서 also는, "그리고 다음의 작업도 객체에 대해 수행하라"라고 읽을 수 있다.
fun main() {
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding new one: $it") } // The list elements before adding new one: [one, two, three]
.add("four")
println(numbers)
// [one, two, three, four]
}
takeIf and takeUnless
스코프 함수에 더해서, 표준 라이브러리에는 takeIf와 takeUnless 함수가 있다.
이러한 함수를 사용하여 호출 체인에 객체 상태 확인을 포함시킬 수 있다.
객체를 predicate(술어... 라는데 {}를 말하는 것 같다.)와 함께 호출했을 때 해당 객체가 주어진 predicate를 만족한다면 객체를 반환한다. 그렇지 않다면 null을 반환한다. 따라서, takeIf는 하나의 객체에 대한 필터링 함수이다.
takeUnless는 takeIf와 반대다. 객체를 predicate와 함께 호출했을 때, 주어진 객체가 predicate를 만족한다면 null을 반환한다. predicate를 만족하지 않는다면 해당 객체를 반환한다.
import kotlin.random.*
fun main() {
val number = Random.nextInt(100) // 랜덤으로 숫자를 뽑는다.
val evenOrNull = number.takeIf { it % 2 == 0 } // 2로 나눈 나머지가 0이면 number 반환.
val oddOrNull = number.takeUnless { it % 2 == 0 }// 2로 나눈 나머지가 0이면 null 반환.
println("even: $evenOrNull, odd: $oddOrNull")
// even: 88, odd: null
// even: null, odd: 39
}
fun main() {
val str = "Hello"
// takeIf와 takeUnless 다음에 함수를 연속해서 호출할 경우, null check나 safe call(?.)을 해야 한다.
// null 값을 반환할 수 있기 때문.
val caps = str.takeIf { it.isNotEmpty() }?.uppercase()
//val caps = str.takeIf { it.isNotEmpty() }.uppercase() //compilation error.. str 값에 따라 null을 반환할 수 있기 때문.
println(caps)
// HELLO
}
takeIf와 takeUnless는 스코프 함수와 함께 쓸 때 특히 유용하다.
예를 들어, takeIf와 takeUnless 다음에 let을 사용하여 주어진 조건에 맞는 객체에 대해 코드 블럭을 실행시킬 수 있다.
이를 위해 takeIf를 객체에 대해 호출하고, 그 다음에 safe call(?.)과 함께 let을 호출한다.
조건을 만족하지 않는 객체들에 대해서 takeIf는 null을 반환하고, let은 호출되지 않는다.
fun main() {
fun displaySubstringPosition(input: String, sub: String) {
input.indexOf(sub).takeIf { it >= 0 }?.let {//sub가 input에 포함되어 있을 때 let 실행
println("The substring $sub is found in $input.")
println("Its start position is $it.")
}
}
displaySubstringPosition("010000011", "11")
// The substring 11 is found in 010000011.
// Its start position is 7.
displaySubstringPosition("010000011", "12")
// "12"가 "010000011"에 포함되지 않으므로 let은 실행되지 않는다.
}
fun main() { // takeIf나 let을 쓰지 않았을 때
fun displaySubstringPosition(input: String, sub: String) {
val index = input.indexOf(sub) // sub가 input의 어디에서 발견되는지 인덱스 저장
if (index >= 0) { // sub가 input에 포함되어 있으면 아래 코드 실행
println("The substring $sub is found in $input.")
println("Its start position is $index.")
}
}
displaySubstringPosition("010000011", "11")
// The substring 11 is found in 010000011.
// Its start position is 7.
displaySubstringPosition("010000011", "12")
}
'Kotlin 공부' 카테고리의 다른 글
kotlinlang.org 내가 보려고 정리 - Coroutines basics (0) | 2023.08.21 |
---|---|
kotlinlang.org 내가 보려고 정리 - Sequences (0) | 2023.08.08 |
kotlinlang.org 내가 보려고 정리 - Object expressions and declarations (0) | 2023.08.04 |
kotlinlang.org 내가 보려고 정리 - Sealed classes interfaces (0) | 2023.08.03 |
kotlinlang.org 내가 보려고 정리 - Data classes (0) | 2023.08.02 |