본문 바로가기

Android/공부

scope(스코프) 함수

안녕하세요? 닉네임간편입니다. 이번 시간에는 스코프 함수에 대해서 알아보겠습니다.

1. 개요

스코프 함수는 코드를 축약해서 표현할 수 있도록 도와주는 함수입니다. 스코프 함수는 객체의 context 내부에서 코드 블록을 실행하는 단일 목적을 갖고 있습니다. 스코프 함수의 종류에는 run, let, apply, also, with이 있습니다. 기본적으로 이 함수들은 유사한 기능을 수행하는데, 그 기능은 바로 객체의 코드 블록을 실행하는 것입니다.

그러나 이 함수들은 객체의 context를 참조하는 방식과 반환값에 있어서 차이가 있습니다. 따라서 이 차이점을 두고 스코프 함수들을 설명하겠습니다.

2. 차이점 1 - 객체 context 참조 방식

참조 방식에는 크게 this와 it이 있습니다.

this는 람다 리시버로, 현재 객체를 가리킵니다. 즉, MainActivity.this는 MainActivity를 가리킵니다.

it은 람다 인자로, 오직 하나의 파라미터만 갖는 람다 표현식에 흔히 사용됩니다. 아래는 동일하게 작동하는 코드이며, this와 it의 사용법의 차이를 담았습니다.

val title = "Kotlin"
title.apply{
println(this.length)
println(length)// this는 생략 가능합니다.
}
title.let{
println(it.length)
}

1) this를 사용하는 함수 - run, with, apply

this를 사용하는 스코프 함수에는 run, with, apply가 있습니다. 이 경우에는 this는 생략하고 해당 객체의 메서드나 프로퍼티를 사용할 수 있습니다. 아래는 예시 코드이며, 앞서 사용했던 title 변수를 그대로 사용하겠습니다.

val title = "Kotlin"
title.run{
	println(length)
}
with(title){
	println(length)
}

title.apply{
	println(length)
}

그러나 this를 생략할 경우 외부 변수와 구별이 어렵기 때문에 주의해야 하며, 객체의 멤버(메서드와 프로퍼티)를 조작할 때 사용하는 것을 권장하고 있습니다.

2) it을 사용하는 함수 - let, also

it은 this보다 길이가 짧고, it을 사용하면 다른 외부 변수와 구별이 가능하기 때문에 코드 블록 안에 여러 개의 변수가 있을 때 유용하게 사용할 수 있습니다.

val title = "Kotlin"
title.let{
	println(it.length)
}
title.also{name->
	println(name.length)
}

also 부분을 보시면 it 대신 name으로 이름을 변경한 걸 볼 수 있습니다. 이처럼 사용할 이름을 변경할 수도 있습니다.

3. 차이점 2 - 반환값

스코프 함수는 반환값에 있어서도 차이가 있으며, context 객체를 반환하는 함수와 람다식의 결과를 반환하는 함수가 있습니다.

1) 객체 반환 - apply, also

apply와 also는 context 객체, 즉 자기 자신을 반환합니다. 이때 코드 블록 안에서 실행된 결과가 반영되어서 반환이 되며, 아래 예시를 통해 확인할 수 있습니다.

var android =mutableListOf<String>("Java","Kotlin")
val applyAndroid = android.apply{
	add("apply")
    }
println(applyAndroid)
val alsoAndroid = android.also{
    it.add("also")
}
println(alsoAndroid)

[실행 결과]
[Java, Kotlin, apply]
[Java, Kotlin, apply, also]

또한 체인처럼 계속해서 사용할 수도 있습니다.

var android =mutableListOf<String>("Java","Kotlin")
val applyAndroid = android.apply{add("apply")}
	.apply{
    	add("another apply")
     }
println(applyAndroid)

[실행 결과]
[Java, Kotlin, apply, another apply]

 

2) 람다식 결과 반환 - let, run, with

위 함수들은 마지막 람다식의 결과를 반환합니다.

아래 예시를 통해 설명드리겠습니다.

var android =mutableListOf<String>("Java","Kotlin")
val cnt1 = android.let{
    it.add("let")
		it.count()
		}
println(cnt1)
val cnt2 = android.run{
		add("run")
		size
		}
println(cnt2)
val cnt3 = with(android){
		add("with")
    get(0)
		}
println(cnt3)

[실행 결과]
3
4
Java

cnt1의 경우, 마지막 코드가 count() 메서드였기 때문에 요소의 개수를 반환받습니다. 따라서 3의 값을 가집니다.

cnt2의 경우, 마지막 코드가 size였기 때문에 길이를 반환받습니다. 따라서 4의 값을 가집니다.

cnt3의 경우, 마지막 코드가 get() 메서드였기 때문에 0번째 인덱스에 해당하는 값을 가집니다. 따라서 Java를 가집니다.

4. 주요 사용 방식

각 함수는 서로 비슷한 기능을 하면 구별하지 않고 사용할 수 있습니다. 그러나 흔히 사용하는 방법이 있으며, 이는 다음과 같습니다.

5. let

객체 context를 참조할 때 it을 사용하며, 마지막 람다식을 반환합니다. let 함수는 마치 체인처럼 계속 메서드를 호출할 때 사용할 수 있습니다.

    val language = mutableListOf("Kotlin", "Java", "Python", "C")
    val result = language.map { it.length }.filter { it > 4 }
    println(result)
    // let 함수 사용
    language.map { it.length }.filter { it > 4 }.let {
        println(it)
    }
    // it을 인자로 사용하는 하나의 함수만 존재
    language.map { it.length }.filter { it > 4 }.let(::println)

위 예시처럼 let을 사용하면 연속해서 필요한 함수를 호출할 수 있습니다. 만일 it을 인자로 사용하는 하나의 함수만 코드 블록에 있다면, :: 을 사용할 수도 있습니다.

또한 let은 오직 non-null 값을 갖는 코드 블록을 실행할 때 사용합니다. 따라서 아래 코드처럼 오직 non-null 값을 가져야만 하는 코드를 실행하는 경우에도 사용할 수 있습니다.

fun processNonNull(title: String) {
	오직 non-null한 값만 가지고 처리하는 메서드입니다.
}

val str: String? ="Kotlin"
//processNonNull(str)   // 이 경우엔 srt이 null일 수 있기 때문에 실행되지 않습니다.
val length = str?.let{
    processNonNull(it)
}

6. with

확장(extension) 함수가 아니므로 일반 함수처럼 사용합니다. 객체 context를 참조할 때 this를 사용하며, 마지막 람다식 결과를 반환합니다. 람다식의 결과를 반환받을 필요가 없이 context 객체를 참조해야할 때 사용하는 것을 권장하고 있습니다. 아래 코드는 그 예시입니다. println() 메서드는 굳이 반환받을 필요가 없으며 this로 참조해야 할 부분이 있으므로 with을 사용했습니다.

    val language = mutableListOf("Kotlin", "Java", "Python", "C")
    with(language) {
        println("I can make program with $language")
        println("I have $size skills")
    }
    
    [실행결과]
    I can make program with [Kotlin, Java, Python, C]
	I have 4 skills

또한 어떤 값의 계산 결과를 반환받을 때도 사용할 수 있습니다. 추가적으로 let도 사용했습니다.

    val language = mutableListOf("Kotlin", "Java", "Python", "C")
    val languageChain = with(language) {
        "I use mainly ${first()}, " +
                "but also use ${last()}"
    }.let { println(it) }
    
    [실행 결과]
    I use mainly Kotlin, but also use C

7. run

객체 context를 참조할 때 this를 사용하며, 마지막 람다식 결과를 반환합니다. run은 객체의 초기화와 반환값의 계산을 동시에 해야할 때 유용합니다.

    val people = People().run {
        name = "kmight"
        age = 100
    }

run은 비확장함수, 즉 일반 함수처럼 사용할 수도 있습니다. 이 경우 run은 객체를 참조하지 않으며 코드 블록 내부의 표현식을 실행합니다.

    val nameRegex = run {
        val name = "ko"
        Regex(".*${name}")
    }
    val result = nameRegex.containsMatchIn("kotlin").let(::println)
    val result2 = nameRegex.containsMatchIn("java").let(::println)
    
    [실행결과]
    true
    false

8. apply

객체 context를 참조할 때 this를 사용하며, 객체 context를 반환합니다. 주로 객체의 멤버를 조작하고 값을 반환하지 않을 때 사용합니다.

val kmight = People().apply { 
        name = "kmight"
        age  = 100
    }

9. also

객체 context를 참조할 때 it을 사용하며, 객체 context를 반환합니다. 객체 그 자체보단 그것의 프로퍼티와 함수를 참조할 필요가 있을 때 주로 사용합니다.

    val kmight = People().apply {
        name = "kmight"
        age = 100
    }
    kmight.also { println("my name is ${it.name}") }
    
    [실행 결과]
    my name is kmight

10. 마무리

이번 시간에는 스코프(Scope) 함수에 대해서 알아보았습니다.

스코프 함수는 사용했을 때 크게 성능적으로 개선한다기 보다는 개발자가 코드를 작성함에 있어서 더 편리하게 해준다는 이점이 있습니다. 저도 스코프 함수를 사용함으로써 코드를 줄이고 더 깔끔한 코드를 작성할 수 있었는데요, 이번 기회에 잘 정리되었으면 좋겠습니다.

728x90
반응형