스코프 함수 (Scope Function)
스코프 함수(Scope Function)란,
객체의 컨텍스트 내에서 코드 블록을 실행하는 것이 유일한 목적인 여러 함수들 입니다.
개체에서 스코프 함수를 호출하면 람다식으로 임시 범위(스코프)가 형성되며,
이 범위에서는 개체 이름 없이 개체에 액세스할 수 있습니다.
스코프 함수의 종류에는 let, run, with, apply, also가 있습니다.
무슨 말인지 잘 모르겠죠? 사실 저도 첫 문단을 어떻게 작성해야 하나 고민하다가,
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions. There are five of them: let, run, with, apply, and also
코틀린 공식 문서의 설명을 참고하여 작성하였습니다.
간단히 말하자면 스코프 함수는 객체의 이름을 하나씩 쓸 필요 없이,
객체의 컨텍스트 내에서 객체를 접근하는 함수입니다.
늘 그렇듯이, 예제를 보고 오는게 이해가 빠르실겁니다.
class Person(val name: String) {
var age = 30
var height = 170
var weight = 70
var address = "서울"
fun sayHello() {
println("Hello, my name is $name")
}
}
fun main() {
val p = Person("홍길동")
p.run {
println("이름: $name")
println("나이: $age")
println("키: $height")
println("몸무게: $weight")
println("주소: $address")
sayHello()
}
}
// 출력
이름: 홍길동
나이: 30
키: 180
몸무게: 70
주소: 서울
Hello, my name is 홍길동
위와 같이,
p 뒤에 run을 붙여, p를 붙이는 것을 생략하여
println(p.name) → println(name)
p.sayHello() → sayHello() 등과 같이 사용할 수 있었습니다.
객체의 참조 깊이가 깊어질수록, 스코프 함수의 효능은 대단해집니다.
이러한 스코프 함수의 종류에는 apply, run, with, let, also 5가지가 있는데요.
스코프를 생성한다는 점에서 모두 비슷해보이지만, 각자 미묘하게 다른 차이가 있습니다.
하나씩 살펴볼까요?
let
- let은 it을 사용해 객체에 접근한다
- return값은 람다식이다. (코드 블록의 맨 마지막줄이다)
class Person(val name: String) {
var age = 30
var height = 170
var weight = 70
var address = "서울시"
fun sayHello() {
println("Hello, my name is $name")
}
}
fun main() {
val p = Person("홍길동")
p.let {
it.age = 40
it.height = 180
it.weight = 80
it.address = "경기도"
println(it.name) // it 키워드를 이용해 p:Person에 접근
println(it.age)
println(it.height)
println(it.weight)
println(it.address)
it.sayHello()
}
}
// 출력
홍길동
40
180
80
경기도
Hello, my name is 홍길동
let에서는 it 키워드를 통해 객체에 접근할 수 있습니다.
fun main() {
val p = Person("홍길동")
p.let { hongil ->
hongil.age = 40
hongil.height = 180
hongil.weight = 80
hongil.address = "경기도"
println(hongil.name)
println(hongil.age)
println(hongil.height)
println(hongil.weight)
println(hongil.address)
hongil.sayHello()
}
}
또는, 람다식의 인자명을 직접 지정해줄 수도 있습니다.
let은 마지막 줄이 반환값이다
fun main() {
val p = Person("홍길동")
val result = p.let {
it.age = 40
it.height = 180
it.weight = 80
it.address = "경기도"
it.age
}
println(result)
}
let의 return값은 람다식입니다.
코틀린에서의 람다식은 주로 코드블록이 쭉 실행된 후, 마지막줄을 반환합니다.
위 코드에서는 it.age가 반환값이 되어 p의 age인 40이 result에 저장된 모습입니다.
also
- it 키워드를 사용해 객체에 접근한다
- also의 return값은 자기 자신이다
val p = Person("홍길동")
p.also {
it.age = 40
it.height = 180
it.weight = 80
it.address = "경기도"
}
위 코드에서 스코프 함수의 사용은 let과 완전히 동일합니다. it을 사용해 객체에 접근합니다.
let과 also의 차이점은 return값입니다.
also vs let
fun main() {
val p = Person("홍길동")
val alsoReturn = p.also {
it.age = 40
it.height = 180
it.weight = 80
it.address = "경기도" // also는 마지막 줄이 반환되지 않으며, 객체 자기 자신을 반환함
}
val letReturn = p.let {
it.age = 50
it.height = 190
it.weight = 90
it.address = "인천시"
it.address // let은 마지막 줄이 반환됨
}
println(alsoReturn)
println(letReturn)
}
// 출력
Person@4a574795
인천시
위 코드에서 보시다시피, also는 Person 객체 자기 자신을 반환하며,
let은 마지막줄인 it.address를 반환하였습니다.
class Person(val name: String) {
var age = 30
var height = 170
var weight = 70
var address = "서울시"
fun sayHello() {
println("Hello, my name is $name")
}
}
fun main() {
val p = Person("홍길동")
p.also {
it.age = 40
it.height = 180
it.weight = 80
it.address = "경기도" // also는 마지막 줄이 반환되지 않으며, 객체 자기 자신을 반환함
}.sayHello()
}
객체 자기 자신을 반환하므로, 위와 같이 also 직후 메소드 실행과 같은 사용법도 가능하지요.
apply
- apply는 객체 자신을 this로 받습니다
- apply는 return값으로 객체 자신을 반환합니다
class Person(val name: String) {
var age = 30
var height = 170
var weight = 70
var address = "서울시"
fun sayHello() {
println("Hello, my name is $name")
}
}
fun main() {
val p = Person("홍길동").apply {
age = 20
height = 180
weight = 80
address = "경기도"
}
println(p.name)
println(p.age)
println(p.height)
println(p.weight)
println(p.address)
}
// 출력
홍길동
20
180
80
경기도
apply는 리턴값으로 자기 자신을 반환합니다. 따라서 초기화에 주로 쓰이는 스코프 함수입니다.
fun main() {
val calendar = Calendar.getInstance().apply {
this[Calendar.YEAR] = 2023
this[Calendar.MONTH] = 1
this[Calendar.DATE] = 1
this[Calendar.HOUR_OF_DAY] = 0
this[Calendar.MINUTE] = 0
}
}
this를 통해 자기 자신에 접근할 수도 있으며,
저는 자바/코틀린의 Calendar 클래스를 초기화 할 때 특히 유용하게 사용했습니다.
run
- run은 객체 자신을 this로 받습니다
- run은 return값으로 객체 자신을 반환합니다
fun main() {
val p = Person("홍길동")
val runResult = p.run { // run 함수는 블록의 마지막 표현식을 반환한다.
age = 40
height = 180
weight = 80
address = "경기도"
"마지막 줄"
}
println(runResult)
}
// 출력
마지막 줄
also와 let의 공통점은 it 키워드를 통해 객체를 호출한다.
also와 let의 차이점은 also는 객체 자기 자신을, let은 마지막줄을 반환한다. 였습니다.
그렇다면 슬슬 눈치채셨겠죠?
apply와 run의 공통점은 this 키워드를 통해 객체를 호출한다. (생략가능)
apply와 run의 차이점은 apply는 객체 자기 자신을, run은 마지막줄을 반환한다 입니다.
with
- run은 객체 자신을 this로 받습니다
- run은 return값으로 객체 자신을 반환합니다
fun main() {
val p = Person("홍길동")
val withResult = with(p) {
age = 40
height = 180
weight = 80
address = "경기도"
"마지막 줄"
}
println(withResult)
}
// 출력
마지막줄
also, run, let, apply. 사용법은 다 똑같지만, 공통점도 있고, 미묘한 차이점도 있는 녀석들이였습니다.
그런데, 다소 쌩뚱맞은 녀석이 하나 있습니다.
사실 위 4개의 스코프 함수와 사용법이 약간 다를 뿐이지,
run과 기능은 똑같습니다.
다만, 위 코드보다는 아래 코드와 같이 결과가 필요하지 않은 경우에 주로 사용한다고 합니다.
fun main() {
val p = Person("홍길동")
with(p) {
age = 40
height = 180
weight = 80
address = "경기도"
}
}
스펠링도 그렇고, 괄호안에 넣는 것도 그렇고 when이랑 비슷해보이네요.
also, let, apply, run 공통점 및 차이점
이들을 도형을 사용해 간단히 나타내보자면 위와 같이 나타낼 수 있을 것 같네요.
이번편은 자바와는 달리 다소 생소하게 느껴질 수 있는 요소였습니다.
저 역시 이걸 어떻게 설명해야 할까 하면서 코틀린 공식 문서의 설명을 꽤 인용했는데,
예제가 코틀린 람다식이 많이 포함되어 있어,
좀 더 쉬운 예제로 가다듬느냐고 꽤 애먹었던 것 같습니다 ㅎㅎ..
https://kotlinlang.org/docs/scope-functions.html
'코틀린 파헤치기 > 3부. 코틀린 고급' 카테고리의 다른 글
[코파기 3부] 6. 코틀린과 함수형 프로그래밍 (2) : 람다식 (0) | 2023.03.24 |
---|---|
[코파기 3부] 5. 코틀린과 함수형 프로그래밍 (1) : 함수형 프로그래밍이란? (0) | 2023.03.24 |
[코파기 3부] 3. 코틀린과 예외처리 : try - catch, finally (0) | 2023.03.22 |
[코파기 3부] 2. 코틀린과 infix 함수 (0) | 2023.03.17 |
[코파기 3부] 1. 코틀린과 확장함수 (Extension Functions) (0) | 2023.03.17 |