GDSC HUFS 3기/Android with Kotlin Team 1

[1팀] 11-3. Kotlin을 위한 기본 문법: 스코프 함수

jaesungLeee 2021. 10. 25. 17:24

이 글은 이것이 안드로이드다 with 코틀린(개정판)을 참고하여 작성하였습니다.

작성자 : 이재성

개발환경은 Windows, Android Studio입니다.

 

우선, 개인적으로 운영하는 블로그에 예전에 포스팅한 적이 있어 다수 참고하여 작성하였음을 알립니다.

 

Scope Functions은 Kotlin 표준 라이브러리에서 제공하는 함수들이다. Scope Functions의 함수들은 lambda 식을 이용하여 호출하게 되는데, 이때, 일시적인 Scope가 생기게 되고, 이 Scope 안에서 해당 객체에 대해 'it' 또는 'this'와 같은 Context Object를 통해 접근할 수 있다. 이러한 Scope Functions들은 객체에 접근하는 방법을 쉽게 해 주며 코드가 간결해지고 코드에 대한 가독성이 높아지는 효과를 가져온다.

1. let { }

let 함수는 표준 라이브러리에 아래와 같이 정의되어있다.

 

inline fun <T,R> T.let(block: (T) -> R): R { return block(this) }

 

함수를 호출하는 객체 T를 뒤에 이어지는 block의 인자로 넘기고, block의 결과값 R을 반환하는 형식이다. 즉, this는 객체 T를 가리키게 되는데, lambda 식의 결과를 그대로 반환한다는 의미이다.

 

아래의 예시를 통해 확인할 수 있다.

fun checkScore(score: Int) {
    if (score != null) println("Score : $score")
}
    
fun checkScoreWithLet(score: Int) {
    // 1번
    score?.let { score ->
        println("Score : $score")
    }
    
    // 2번
    val scoreStr = score.let { it.toString() }
    println(scoreStr)
}

fun main() {
    val score: Int? = 32
    
    checkScore(score)
    checkScoreWithLet(score)
}

main에서 score 변수는 Nullable한 정수형 변수로 선언되어있고, 32로 초기화되어있다. checkScore( )는 Nullable 한 변수 score를 검사할 때 일반적으로 사용하는 방식이다.

let을 사용하면 보다 간편하게 Null검사를 할 수 있다. checkScoreWithLet( )에서 1번 코드는 score가 Null이 아닐 경우에만 block안의 코드를 실행한다. 즉, Null일 경우 해당 코드는 컴파일 대상에서 제외된다. 또한, main에서 score 변수를 Nullable 하게 선언했기 때문에 Safe-Call을 사용한다. 기본적으로 let 함수는 lambda 식에 접근하기 위해 'it'을 사용하는데, 'it'은 score 자체를 복사하는 형태로 이해할 수 있다. 예시에서는 score에 대한 Null검사를 진행했지만, 여러 변수에 대한 검사를 하게 될 경우 score ->와 같이 'it'을 Aliasing 할 수 있다.

2번 코드도 마찬가지로 scoreStr 변수를 선언함과 동시에 let을 사용하여 Null검사를 진행한다. 마찬가지로 score를 'it'로 접근하여 String 변환 후 scoreStr에 할당한다.

 

앞서 설명한 것 처럼, let은 Android 개발에 있어 편리한 Null검사를 제공한다.

class MainActivity: AppCompatActivity() {
    
    private val tickingSoundId: Int? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
    
    ...
    
    private fun startCountDown() {
        ...
        tickingSoundId?.let { soundId ->
            soundPool.play(soundId, 1F, 1F, 0, -1, 1F)
        }
    }
}

 

 

2. also { }

also 함수는 아래와 같이 선언되어 있다.

 

inline fun <T> T.also(block: (T) -> Unit): T { block(this): return this }

also는 함수를 호출하는 객체 T를 block에 전달하는데, 객체 T 자체를 return하는 함수이다. 즉, also는 block 안의 코드 수행 결과와는 상관없이 객체 T를 의미하는 'this'를 반환한다. 간단한 예시를 살펴본다.

var num = 1

num = num.also { it + 3 }
println(num)

block안의 연산을 수행하여 4라고 생각할 수 있지만 return 할 때는 4를 반환하지 않고 원래 1을 반환한다.

아래의 예시에서 let과 also를 비교한다.


fun main() {
    data class GDSC(var name: String, var skill: String)
    
    val gdsc = GDSC("jslee", "Android")
    
    val checkLet = gdsc.let {
        it.skill = "Kotlin"
        "CHECK_LET"
    }
    println("CheckLet : $checkLet")
    println("gdsc : $gdsc")
    
    val checkAlso = gdsc.also {
        it.skill = "Java"
        "CHECK_ALSO"
    }
    println("CheckAlso : $checkAlso")
    println("gdsc : $gdsc")
}

let과 also 모두 'it'로 변수에 접근하여 GDSC의 skill를 변경한다. 하지만, 위에서 언급했듯이, let은 block의 결과값, 즉, block의 마지막 코드를 return 하지만 also는 객체 자체를 return 한다. 따라서, println("CheckAlso : $checkAlso")를 했을 때 "CHECK_ALSO"가 return되지 않고 변수 checkAlso 자체가 return 된다.

3. apply { }

apply 함수는 아래와 같이 선언되어 있다.

inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

also와 매우 유사하다. apply는 also와 마찬가지로 호출하는 객체 T를 block으로 전달하고 객체 자체인 'this'를 반환한다. apply는 특정 객체를 생성함과 동시에 호출해야하는 초기화 코드가 있을 경우 사용할 수 있다. also와 다른 점은 lambda 식이 확장 함수로 처리된다는 것이다.

아래의 예시를 살펴본다.

fun main() {
    data class GDSC(var name: String, var skill: String)
    
    val gdsc = GDSC("jslee", "Android")
    
    // 1번
    gdsc.apply { this.skill = "Swift" }
    println(gdsc)    // GDSC(name=jslee, skill=Android)
    
    // 2번
    val returnObject = gdsc.apply {
        name = "JaesungLeee"
        skill = "JavaScript"
    }
    println(gdsc)    // GDSC(name=JaesungLeee, skill=JavaScript)
    println(returnObject)    // GDSC(name=JaesungLeee, skill=JavaScript)
}

1번 코드에서 apply는 gdsc 자체를 'this'를 이용하여 접근하고 skill을 변경한다. 중요한 점은 객체 자체를 변경하고 특정 값을 반환하는 것이 아니라 gdsc 객체 자체를 반환하게 된다. 이때, 2번 코드와 같이 'this'는 생략될 수 있다.

2번 코드에서 returnObject라는 변수에 할당하고 있고 'this'를 생략하여 gdsc 객체를 변경하고 있다.

 

Android 개발 시 apply를 활용하여 가독성 좋은 코드를 작성할 수 있다.

아래 예시 코드에서 1번 코드는 2번처럼 리팩토링할 수 있다.

// 1번
val intent = Intent(this, SubActivity::class.java)
intent.putExtra("TAG1", "CONTENT1")
intent.putExtra("TAG2", "CONTENT2")
intent.putExtra("TAG3", "CONTENT3")
startActivity(intent)

// 2번
val intent = Intent(this, SubActivity::class.java).apply {
    putExtra("TAG1", "CONTENT1")
    putExtra("TAG2", "CONTENT2")
    putExtra("TAG3", "CONTENT3")
}
startActivity(intent)

 

4. apply { }

run 함수는 아래와 같이 선언되어 있다.

inline fun <R> run(block: () -> R): R = return block()
inline fun <T, R> T.run(block: T.() -> R): R = return block()

run은 단순히 run을 호출하는 형식, 확장함수 형태로 run을 호출하는 형식인 두 가지 형식으로 나뉜다. 첫 번째 형식은 block 내용을 수행 후 block의 결과를 return 하는 형식이다. 두 번째 형식은 확장 함수 형태로 나온 결과를 return 한다.

아래의 예시를 살펴본다.

fun main() {
    var skills = "Kotlin"
    println(skills)    // Kotlin
    
    val level = "High"
    skills = run {
        val returnLevel = "Kotlin Level : $level"
        returnLevel
    }
    
    println(skills)    // Kotlin Level : High
}

run의 block안의 마지막 표현식이 return되는 것을 알 수 있다.

아래의 예시는 apply와 run을 비교한다.

 

fun main() {
    data class GDSC(var name: String, var skill: String)
    
    val gdsc = GDSC("jslee", "Android")
    
    // 1번
    val returnObject = person.apply {
        name = "JaesungLeee"
        skill = "javaScript"
    }
    println(gdsc)    // GDSC(name=JaesungLeee, skill=JavaScript)
    println(returnObject)    // GDSC(name=JaesungLeee, skill=JavaScript)
    
    // 2번
    val returnObject2 = gdsc.run {
        name = "Leee"
        skills = "C#"
        "RUN_DONE"
    }
    println(gdsc)    // GDSC(name=Leee, skill=C#)
    println(returnObject2)    // RUN_DONE
}

우선, apply와 run 모두 'this'를 이용하여 참조하기 때문에 생략이 가능하다. 하지만, 위에서 언급한 것처럼, apply는 객체 자체를 반환하기 때문에 returnObject를 출력하면 person 객체가 출력된다.

반면, run을 사용하게 되면 person의 멤버들은 변경되지만 block의 마지막 표현식인 "Success"가 returnObject2에 할당되어 반환된다.

 

5. with { }

with 함수는 아래와 같이 선언되어 있다.

 

inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()


with는 인자로 받는 객체를 뒤에 이어지는 block의 receiver로 전달하며 결과를 반환한다. 하지만, with는 Safe-Call을 지원하지 않기 때문에 Null 처리를 위해 let과 함께 사용한다. 

아래에 Android 코드 예시를 통해 확인한다.

 

supportActionBar?.let {
    with(it) {
        setDisplayHomeAsUpEnabled(true)
        setHomeAsUpIndicator(R.drawable.ic_clear_white)
    }
}

Safe-Call을 사용하여 Null이 아닌 경우 let의 block을 수행하는데, 'it'이 supportActionBar를 receiver로 받아와 block을 수행하게 된다. run과 비슷하게 'this'를 생략하여 사용할 수 있다. 필요에 따라 return 하고 싶은 표현식이 있으면 block의 마지막 줄에 추가할 수 있다.

fun main() {
    data class GDSC(var name: String, var skill: String, var email: String? = null)
    
    val gdsc = GDSC("jslee", "Any")
    
    val result = with(gdsc) {
        skill = "Kotlin"
        email = "jslee@xxx.com"
    }
    
    println(gdsc)    // GDSC(name=jslee, skill=Kotlin, email=jslee@xxx.com)
    println(result)  // Kotlin.Unit
}

gdsc를 with에 receiver로써 직접 전달하고 있다. 이때, 'this'가 생략되어 gdsc의 멤버들을 변경하게 된다. 하지만, gdsc에 대한 변경만 하고 block에 아무런 표현식이 없으면 result는 Unit을 반환하게 된다.

 

6. Reference

https://jslee-tech.tistory.com/22?category=964953 

 

Kotlin Scope Functions : let, also, apply, run, with

Java와는 달리 Kotlin의 표준 라이브러리에서 제공하는 함수들이 있다. Scope Functions의 함수들은 lambda 식을 이용하여 호출하게 되는데, 이때, 일시적인 Scope가 생기게 되고, 이 Scope 안에서 해당 객체

jslee-tech.tistory.com