본문 바로가기
Kotlin

(Kotlin) Scope 함수

by jaesungLeee 2021. 9. 9.

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

이번 포스팅은 Kotlin의 대표적인 5가지 Scope Functions에 대한 포스팅이다.

1. let { }

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

 

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

 

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

 

아래의 예제를 살펴본다.

 

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

 

score라는 변수는 Nullable한 Int형 변수로 선언되어있고, 32로 초기화되어있다. checkScore( ) 함수는 일반적으로 우리가 Null 검사를 할 때 사용할 수 있다.

checkScoreWithLet( ) 함수의 1번째 코드는 변수 score가 Null이 아닐 때 block안의 코드를 실행하는 형태이다. 우리가 초기에 score를 선언할 때 Nullable 하게 선언했으므로 Safe-Call을 사용한다. 여기서 lambda 식에 접근하기 위해 사용되는 'it'은 score 자체를 복사하는 형태로 이해할 수 있다. 따라서, score는 Null이 아니기 때문에 32가 출력될 것이다.

2번째 코드는 변수 scoreStr을 선언함과 동시에 let을 호출한다. 마찬가지로 score를 'it'으로 접근하여 String으로 변환 후 scoreStr에 할당한다.  

 

Android에서도 let을 이용하여 아래와 같이 Null 검사를 할 수 있다.

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

 

추가로, Elvis Expression과 함께 사용하여 Null 검사를 할 수 있다. 

 

fun main() {
    val firstName: String? = null
    var lastName: String = "lee"
    
    fun checkName() = if (firstName != null) println("$firstName $lastName") else println("$lastName")
    
    fun checkNameWithLet() = firstName?.let { println("$it $lastName") } ?: println("$lastName")
	
    checkName()
    checkNameWithLet()
}

 

checkName( ) 함수는 일반적인 Null 검사 코드이다.

checkNameWithLet( ) 함수는 let과 함께 Elvis Expressoin을 사용한다. firstName이 Null이 아닐 경우 block안의 코드를 실행하고, Null일 경우 ?: 뒤의 코드를 실행한다.

 


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)        // 1

 

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

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

 

fun main() {
    data class Person(var name: String, var skill: String)
    
    var person = Person("jslee", "Android")
    
    val a = person.let {
        it.skills = "Kotlin"
        "let success"
    }
    
    println("a : $a")            // a : let success
    println("person : $person")  // person : Person(name=jslee, skill=Kotlin)
    
    val b = person.also {
        it.skills = "Java"
        "also success"
    }
    
    println("b : $b")            // b : Person(name=jslee, skill=Java)
    println("person : $person")  // person : Person(name=jslee, skill=Java)
}

 

let과 also 모두 'it'을 사용하여 변수에 접근하여 Person의 skills를 변경한다. 하지만, 위에서 언급했듯이, let은 block의 결과값, 즉, block의 마지막 코드를 return 하지만 also는 객체 자체를 return 한다. 따라서, println("b : $b")를 했을 때 "also succes"가 return되지 않고 변수 b 자체가 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 Person(var name: String, var skill: String)
    
    var person = Person("jslee", "Android")
    
    person.apply { this.skill = "Swift" }    // 1
    
    println(person)            // Person(name=jslee, skill=Swift)
    
    val returnObject = person.apply {    // 2
        name = "Jaesung"
        skill = "JavaScript"
    }
    
    println(person)            // Person(name=Jaesung, skill=JavaScript)
    println(returnObject)      // Person(name=Jaesung, skill=JavaScript)
}

 

첫 번째 apply에서는 person자체를 'this'를 사용하고, skill을 호출하여 변경한다. 여기서 객체 자체를 변경하고 어떤 값을 반환하는게 아니라 person 자체를 반환하게 된다. 이때, 'this'는 생략될 수 있다.

두 번째 apply에서는 returnObject라는 변수에 할당을 하고 있고 'this'를 생략하여 person 객체를 변경하고 있다. 또한, person이 가지고 있는 멤버에 여러번 접근이 가능하다. 

 

Android에서도 apply를 활용하여 가독성 좋은 코드를 작성할 수 있다.

 

val intent = Intent(this, SubActivity::class.java)
intent.putExtra("TAG1", "String1")
intent.putExtra("TAG2", "String2")
intent.putExtra("TAG3", "String3")

startActivity(intent)

 

위의 코드를 아래와 같이 변경할 수 있다.

 

val intent = Intent(this, SubActivity::class.java).apply {
    putExtra("TAG1", "String1")
    putExtra("TAG2", "String2")
    putExtra("TAG3", "String3")
}

startActivity(intent)

 


4. run { }

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 a = "High"
    skills = run {
        val level = "Kotlin Level :" + a
        level
    }
    
    println(skills)            // Kotlin Level : High
}

 

위에서 언급한 것처럼, run의 block안의 마지막 표현식이 return 되는 것을 알 수 있다.

 

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

 

fun main() {
    data class Person(var name: String, var skill: String)
  
    var person = Person("jslee", "Android")
    
    val returnObject = person.apply {
        name = "Jaesung"
        skill = "JavaScript"
    }
    
    println(person)            // Person(name=Jaesung, skill=JavaScript)
    println(returnObject)      // Person(name=Jaesung, skill=JavaScript)
    
    val returnObject2 = person.run {
        name = "Lee"
        skills = "C#"
        "Success"
    }
    
    println(person)            // Person(name=Lee, skill=C#)
    println(returnObject2)     // Success
}

 

우선, 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 User(var name: String, var skill: String, var email: String? = null)
  
    var user = User("jslee", "Default")
    
    val result = with (user) {
        skill = "Kotlin"
        email = "jslee@xxx.com"
    }
    
    println(user)            // User(name=jslee, skill=Kotlin, email=jslee@xxx.com)
    println(result)          // kotlin.Unit
}

 

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

 

References

https://kotlinlang.org/docs/scope-functions.html#also

 

Scope functions | Kotlin

 

kotlinlang.org

 

 

 

 

'Kotlin' 카테고리의 다른 글

(Kotlin) sealed class  (0) 2022.07.27
(Kotlin) open class, abstract class  (0) 2022.07.27
(Kotlin) Class 정의  (0) 2021.10.04
객체 지향 프로그래밍 (OOP)  (0) 2021.10.04
(Kotlin) 배열과 컬렉션  (0) 2021.08.02