[TIL] Scope Function (2)

박봉팔·2024년 1월 12일
0

Scope Function 완전 정복

본 내용은 Scope function에 대해 정확하게 공부하기 위해 kotlin 공식문서의 Scope function의 내용을 번역한 글로 Scope function(1)에서는 각 함수별로 어떤 차이가 있는지에 대한 내용을 알아봤다.
이 글에서는 각 함수가 어떤 용도로 사용되는게 적절한지 알아본다.

[TIL] Scope Function (1) - 보러가기


각 Scope Function 알아보기

함수별로 자세한 설명을 통해 어떤 경우에 해당 함수를 사용하는게 적절한지 알아본다.
Scope function은 서로 대체해서 사용하는게 가능하기 때문에 예제를 통해 어떤경우에 해당 함수를 사용하는게 옳은지 확인해보자.


let

  • 객체를 참조할때 인자로 참조한다. (it으로 참조)
  • 람다식의 결과값을 반환한다.

let은 함수를 연속적으로 호출한뒤 해당 결과 값을 대상으로 하나 혹은 이상의 함수를 호출해야 하는 경우 사용할 수 있다.

예를들어 다음과 같은 코드는 MutableList에 대해 2가지 작업을 수행한다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }

println(resultList) 

하지만 let함수를 사용하면 결과값을 변수에 할당하지 않아도 위와 같은 코드를 작성하는게 가능하다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // println말고도 다른 함수를 더 넣을수도 있다.
} 

만약 let안에 사용된 코드가 함수 단 한개일때, 그리고 인자로 해당 객체만을 받는 경우에는 람다 인자로 사용하지 않고 참조함수를 사용하는것도 가능하다. (::)

val numbers = mutableListOf("one", "two", "three", "four", "five")

numbers.map { it.length }.filter { it > 3 }.let(::println)

또한 letnon-null값으로 실행되야 하는 함수를 사용하기 위해 쓰기도 한다. non-null값으로 작업을 수행하기 위해서 safe call연산자인 ?.를 사용해 let을 호출하고 람다식을 작성한다.

val str: String? = "Hello"   
//processNonNullString(str)       // 만약 그냥 실행하면 컴파일 에러가 난다.
val length = str?.let { 	  	  // error: str can be null
    println("let() called on $it")        
    processNonNullString(it)      // ?.let을 통해 널체크가 되었기 때문에 에러없이 실행된다.
    it.length
}

가독성을 위해 참조한 객체의 이름을 바꿔서 letScope내부에서만 사용되는 새로운 이름의 지역변수처럼 사용할수도 있다. 이 경우 람자인자로 전달받은 객체에 이름을 명시해 기본값인 it대신 사용할 수 있도록 해야한다.

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem -> // Scope 내부에서 numbers.first()를
    println("The first item of the list is '$firstItem'")  // firstItem으로 사용
    if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")

with

  • 객체를 참조할때 수신자로 참조한다. (this으로 참조)
  • 람다식의 결과값을 반환한다.

with는 확장함수가 아니기 때문에 객체가 인자로 전달된다. 하지만 람다 내부에서는 수신자로 참조되어 this로 참조한다.

with의 경우 람다의 반환값을 사용할 필요가 없는 경우 사용하는 것이 권장된다. 코드내에 with가 사용된경우 "이 객체를 사용해 다음 코드들을 실행해라."라고 해석할 수 있다.

val numbers = mutableListOf("one", "two", "three")

with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

또한 결과값을 변수에 대입할때 해당 결과값의 계산에 객체의 프로퍼티나 함수를 사용해야하는경우 withhelper object로 사용해 가독성을 높일 수 있다.

val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," + // 결과값 계산에 number에 대한 함수를 사용
    " the last element is ${last()}"
}

println(firstAndLast)

run

  • 객체를 참조할때 수신자로 참조한다. (this으로 참조)
  • 람다식의 결과값을 반환한다.

run은 기본적으로 with와 기능이 비슷하다. 하지만 확장함수이기 때문에 let처럼 .사용해 객체에서 직접 호출할 수 있다.

run은 람다내부에서 객체를 초기화 한 뒤 결과값을 계산할 경우 사용한다.

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080		// service의 port프로퍼티를 초기화
    query(prepareRequest() + " to port $port") // 초기화 된 port를 사용해 결과값을 result에 대입
}

// 똑같은 코드를 let으로 사용한 경우
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

또한 run은 비확장 함수로도 호출이 가능하다. 비확장 함수로 run을 호출할 경우 run은 참조할 객체는 없지만, 여전히 람다결과값을 반환한다.

따라서 표현식을 사용할때 여러줄의 코드를 작성하고 싶은 경우 run을 비확장 함수로 사용하면 된다.

비확장함수로 사용되는 run"람다 내부의 코드들을 실행하고 결과를 계산해라."라고 해석이 가능하다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+") // Regex에 위의 변수들을 사용해 결과를 계산
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
}

apply

  • 객체를 참조할때 수신자로 참조한다. (this으로 참조)
  • 참조한 객체 자체를 반환한다.

apply는 참조한 객체 자체를 반환하기 때문에 주로 참조한 객체의 멤버에 접근하는 코드를 사용하는게 좋다.
(결과값을 반환하지 않는 코드)

보통 apply는 객체를 구성(초기화)할 때 사용한다. 이렇게 사용할경우 "객체 멤버들에 다음과 같은 내용들을 할당한다." 라고 해석하는게 가능하다.

val adam = Person("Adam").apply { // 결과값을 계산하지 않고 Person객체 내부의 프로퍼티를 할당
    age = 32
    city = "London"        
}

println(adam)

또한 연속적으로 함수가 호출될 경우 여러 작업을 수행하고 결과적으로 수정된 객체를 반환하게 해 복잡한 작업을 보다 보기 쉽게 만들 수 있다.

data class Car(var make: String, var model: String, var year: Int)

val car = Car("Toyota", "Camry", 2022).apply {
    year = 2023     // apply로 year를 초기화
}.run {
    make = make.toUpperCase()    // run으로 make를 대문자로 변환
    this	 // 수정된 객체를 this로 반환
}.apply {
    model = "$model Sport" // apply로 model을 변경한 뒤 객체를 반환
}

println("Final: $car") // Final: Car(make=TOYOTA, model=Camry Sport, year=2023)

also

  • 객체를 참조할때 인자로 참조한다. (it으로 참조)
  • 참조한 객체 자체를 반환한다.

also는 참조 객체를 매개변수로 전달받아야하는 코드를 사용할 경우 유용하다.

참조한 객체의 프로퍼티나 메서드에 접근할 필요없이 객체 자체에 대한 참조만 필요할 경우나,
this를 사용할 경우 Scope외부에 있는 참조와 겹칠때 이를 방지하기위해 사용할 수 있다.

코드에 also가 사용됐을경우 "이 객체를 사용해 다음의 코드도 실행한다"라고 해석할 수 있다.


추가적으로 takeIftakeUnless에 대한 내용이 나오지만 심화적인 부분이라고 판단되어 따로 해석하진 않았다.
(나중에 이 함수들의 필요성이 느껴질 때 번역해야겠다.)


오늘은 어땠나요?

profile
개발 첫걸음! 가보자구!

0개의 댓글