본문 바로가기

코틀린

[코틀린] Scope function (영역 함수)

안드로이드 개발하는 kancho입니다.

이번 포스팅에서는 코틀린의 영역 함수(Scope function)에 대해 알아보고자 합니다.

'코틀린 완벽 가이드' 책을 참고하였습니다.

 

 

코틀린의 표준 라이브러리에는 식을 계산한 값을 임시로 문맥 내부에서 사용할 수 있도록 해주는 함수들이 있다.

 

Scope function(영역 함수)

어떠한 식의 결과값이 들어있는 암시적인 영역을 정의해 코드 작성을 보다 단순하게 만드는 함수이다.

기본적으로 영역 함수는 람다를 간단하게 실행해주는 역할을 한다.

 

5가지 표준 영역 함수

run, let, with, apply, also

 

run

run 함수는 확장 람다를 받는 확장 함수이며 람다의 결과를 돌려준다.

지역 변수의 가시성을 세밀하게 제어할 수 있다.

 

구조

run 함수의 정의를 보면 아래와 같다.

파라미터로 block 이라는 이름의 확장 람다를 받아 결과값으로 람다의 결과 R을 리턴하는 확장 함수인 것을 알 수 있다.

 

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

 

 

사용

User 클래스와 이를 사용하는 코드이다.

emailEmpty 변수는 run 블록 안의 마지막 줄인 emptyEmail의 결과값인 Boolean 값을 리턴 받아 Boolean으로 타입 추론이 된다.

 

class User(
    var firstName: String = "",
    var lastName: String = "",
    var email: String = ""
) {
    fun emptyEmail() = email.isEmpty()
}

/*
만약 run을 사용하지 않으면 
아래와 같이 User 인스턴스를 담는 변수를 하나 만들어야 한다.

val user = User()
val emailEmpty = user.emptyEmail()
*/
fun main() {
    val emailEmpty = User().run { 
        // this는 User 인스턴스
        firstName = "kancho"
        lastName = "Park"
        email = "kancho@tistory.com"
        emptyEmail()	// 반환값
    }
    
    if (emailEmpty.not())
        println("email is not empty")
}

 

 

JVM에서 run 함수가 어떻게 동작하는지 보면 조금 더 쉽게 이해할 수 있다.

run 함수는 내부적으로 User 인스턴스를 담는 변수를 만들고 이 변수를 통해 프로퍼티들을 세팅한다.

뒤에서 알아볼 with, let, apply, also 함수도 이와 같은 방식으로 동작한다.

 

public static final void main() {
   User var1 = new User((String)null, (String)null, (String)null, 7, (DefaultConstructorMarker)null);
   int var3 = false;
   var1.setFirstName("kancho");
   var1.setLastName("Park");
   var1.setEmail("kancho@tistory.com");
   boolean emailEmpty = var1.emptyEmail();
   if (!emailEmpty) {
      String var4 = "email is not empty";
      System.out.println(var4);
   }

}

 

 

문맥이 없는 run 함수도 있다.

이 함수는 확장 함수가 아니며 run()을 오버로딩한 함수이며 파라미터로 람다의 결과값인 R을 반환하는 람다를 받는다.

 

public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

 

 

일반적으로 어떤 식이 필요한 부분에서 사용한다.

 

fun main() {
    val user = run {
        val firstName = readLine() ?: return
        val lastName = readLine() ?: return
        val email = readLine() ?: return
        User(firstName, lastName, email)
    }
    println(user.emptyEmail())
}

 

 

with

with 함수는 run 함수와 비슷하지만 확장 함수 타입이 아닌 일반 함수 타입이다.

일반적으로 식의 멤버 함수와 프로퍼티에 대한 호출을 동일한 영역 내에서 실행할 때 사용한다.

 

 

구조

with 함수의 정의를 보면 아래와 같다.

일반 함수이므로 첫 번째 파라미터로 receiver를 받는다.

두 번째 파라미터로 확장 람다를 받아 결과값으로 람다의 결과 R을 리턴하는 함수인 것을 알 수 있다.

 

public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

 

 

사용

with 함수의 첫 번째 인자로 User 인스턴스를 생성하는 식을 넣어주었다.

두 번째로 람다식을 넣어 String 타입의 값을 리턴 받아 message 변수에 넣어주었다.

 

class User(
    var firstName: String = "",
    var lastName: String = "",
    var email: String = ""
)

fun main() {
    val message = with(User("kancho", "Park")) {
        "userInfo: $firstName, $lastName"
    }
    println(message)
}

/*
만약 with를 사용하지 않았다면
User 인스턴스를 담을 user 변수를 만들고
User의 프로퍼티를 호출할 때마다 user 변수를 작성했어야 한다

fun main() {
    val user = User("kancho", "Park")
    val message = "userInfo: ${user.firstName}, ${user.lastName}"
    println(message)
}
*/

 

let

let 함수는 인자가 하나인 함수 타입의 람다를 받아 람다의 결과값을 반환하는 확장 함수이다.

일반적으로 외부 영역에 새로운 변수를 도입하는 일을 피하고 싶을 때 사용한다.

 

 

구조

let 함수의 정의를 보면 아래와 같다.

파라미터로 block 이라는 인자(T)가 하나인 함수 타입의 람다를 받아 결과값으로 람다의 결과 R을 리턴하는 것을 볼 수 있다.

 

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

 

 

사용

User 객체를 생성하면서 let 함수의 파라미터로 User 타입의 인자를 가진 람다를 넣어주었다.

가독성을 높이기 위해 파라미터에 원하는 이름을 지정할 수 있다.

 

class User(
    var firstName: String = "",
    var lastName: String = "",
    var email: String = ""
) {
    fun userInfo() = "$firstName, $lastName, $email"
}

fun main() {
    User().let { user ->
        user.firstName = "kancho"
        user.lastName = "Park"
        user.email = "kancho@tistory.com"

        println("firstName : ${user.firstName}")
        println(user.userInfo())
    }
}

 

 

또한 let 함수는 nullable한 값을 안전성 검사를 통해 null이 될 수 없는 함수에 전달할 수 있다.

 

class User(
    var firstName: String = "",
    var lastName: String = "",
    var email: String? = null
) {
    fun userInfo() = "$firstName, $lastName, $email"
}

// User 인스턴스에 email을 set 해주지 않아 default로 null이 된
fun main() {
    val user = User("kancho", "Park")
    
    // let을 사용하지 않는 방법
    if (user.email != null) {
        println(user.email)
    }
    
    // let을 사용한 방법
    user.email?.let {
        println(it)
    }
}

 

 

apply

apply 함수는 확장 람다를 받는 확장 함수이며 자신의 수신 객체를 반환한다.

일반적으로 run 함수와 달리 반환값을 만들지 않고 객체의 상태를 설정하는 경우에 사용한다.

 

 

구조

apply 함수의 정의를 보면 아래와 같다.

파라미터로 block 이라는 확장 람다를 받으며 결과값으로 자신의 수신 객체인 T를 리턴하는 것을 볼 수 있다.

run 함수에서는 block()을 리턴하지만 apply 함수에서는 block()을 실행하고 this를 리턴하는 것을 볼 수 있다.

 

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

 

 

사용

 

class User(
    var firstName: String = "",
    var lastName: String = "",
    var email: String = ""
) {
    fun userInfo() = println("$firstName, $lastName, $email")
}

fun main() {
    User().apply {
        firstName = "kancho"
        lastName = "Park"
        email = "kancho@tistory.com"
    }.userInfo()
}

 

also 

also 함수는 인자가 하나인 람다를 파라미터로 받는 확장 함수이며 자신의 수신 객체를 반환한다.

 

 

구조

also 함수의 정의를 보면 아래와 같다.

파라미터로 block 이라는 인자(T)가 하나인 람다를 받으며 결과값으로 자신의 수신 객체인 T를 리턴하는 것을 볼 수 있다.

apply 함수에서는 block()을 실행하고 this를 리턴하지만 also 함수는 block(this)를 실행하고 this를 리턴한다.

 

public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

 

 

사용

 

class User(
    var firstName: String = "",
    var lastName: String = "",
    var email: String = ""
) {
    fun userInfo(message: String) = println(message)
}

fun main() {
    val message = readLine() ?: return

    User().also {
        it.firstName = "kancho"
        it.lastName = "Park"
        it.email = "kancho@tistory.com"
    }.userInfo(message)
}

'코틀린' 카테고리의 다른 글

[코틀린] Enum 클래스  (0) 2022.10.21
[코틀린] 람다와 고차함수  (0) 2022.10.17
[코틀린] 확장 (Extension)  (0) 2022.10.10
[코틀린] Lazy 초기화  (0) 2022.10.09
코틀린 정리 - (3)  (0) 2022.10.03