본문 바로가기

Kotlin/Basic

[Kotlin] Scope Functions 정리

Scope Function(let, with, run, apply, also) 정리


Scope Function 이라는 함수명에서 알 수 있듯이, 이 함수들을 람다식을 이용해서 호출하면 일시적인 Scope(범위)가 생기게 되고, 이 범위 안에서는 전달된 객체에 대해 “it” 또는 “this”라는 Context Object를 통해서 접근하게 된다.

두 가지 차이점


Scope Function에는 서로 다른 두 가지 주요 차이점이 있다.

  • Context Object를 참조하는 방법(this, it)
  • Return value

Context Object: this or it


Scope Function 람다식 내에서 Context Object는 실제 객체명 대신 짧은 참조명인 “it”또는 “this”키워드로 접근할 수 있다. 그럼 먼저 이 둘의 차이점을 알아보자. 둘 다 동일한 역할을 하지만 다른 상황 별로 각각의 장점과 단점에 대해 알아보고 권장하는 방식을 알아보겠다.

fun main() {
    val str = "Hello"
    // this 
    str.run{
        println("The Receiver string length: $length")
        // 위와 동일한 코드
        // println("The Receiver string lenth: ${this.length}")
    }
    
    // it
    str.let{
        println("The Receiver string length: ${it.length}")
    }
     
}

this : 람다 수신자

run, with, apply는 Context Object를 “this” 키워드로, 람사 수신자로서의 컨텍스트 객체를 참조한다.

따라서, 람다식 안에서는 일반 클래스 멤버처럼 사용할 수 있다.

this는 생략 할 수 있지만, 만약 동일한 이름의 멤버가 있는 경우 둘을 구별할 수가 없기 때문에, 되도록 Context Object에 대해서는 this를 붙여서 사용하는 것이 가독성 측면에서는 선호되며,

수신자(this)로 Context Object를 받는 것은 주로 객체함수를 호출하거나 프로퍼티를 할당하는 것과 같이 객체 멤버를 처리하는 람다에 사용하는 것이 권장된다.

아래 코드를 보며 이해하자.

class Person(var name: String, var age: Int?=null)

fun main(){
		val person = Person("몰름보", 28)
		
		// ⭐ this로 참조한다.
		person.run{
				println("이름: ${this.name}")
        println("이름: ${this.age}")
		}
		// ⭐ 다음과 같은 방법으로도 사용이 가능하다.
		val park = Person("Park").apply{
				age = 28
		}
		park.run{
        println("이름: $name")
				println("나이: $age")
		}
}

it : 람다 인자

let, also는 Context Object를 람다 인자(lambda argument)로 가진다.

따로 전달 인자명을 지정할 수도 있고, 지정하지 않으면 암시적인 기본 이름 “it”으로 접근하게 된다.

it은 this보다 짧으며, 주로 it을 사용한 표현식은 읽기가 쉽다. 또, 객체 함수나 프로퍼티를 호출할 경우, this와 같이 암시적인 객체가 없다. 그러므로 객체가 함수 호출에서 주로 인자로 사용될 때에는 컨텍스트 객체를 it으로 가지는 것이 더 좋다. 더불어 코드 블럭 내에서 여러 개의 변수를 사용한다면, it을 사용하는 것이 더욱 권장된다.

아래 코드를 보며 이해하자22

class Person (var name: String, var age: Int)

fun main(){
		val person = Person("몰름보", 28)

		// ⭐ it으로 참조한다.
		person.let{
				println("이름: ${it.name}")
		
		// ⭐ 전달 인자명을 지정해서 참조한다.
		person.let{ value ->
				println("이름: ${value.name}")
		}
}

it의 경우, 팀단위 프로젝트를 진행할 때 되도록 사용을 권장하지는 않는다. 명확한 코드가 아니기 때문에, 가독성을 해치기 때문이다. 그렇기 때문에, it이 무엇인지 가능한 대체할 수 있는 전달 인자명을 찾아서 대체하도록 하는 습관을 기르자.

반환 값(Return value)


apply, also는 Context Object를 반환하고, let, run, with은 람다식 결과를 반환한다.

두 옵션은 다음에 어떤 코드를 작성할 것이지에 따라 적절한 함수를 선택할 수 있도록 한다.

자 그럼, 이제 Context Object와 람다식 결과(Lambda Result)를 알아보자.

Context Object

apply, also가 반환하는 값은 Context Object 객체 그 자신이다.

그렇기 때문에, 체인 형식으로 계속적인 호출이 가능하다.

fun main() {
    val DoubleList = mutableListOf<Double>()
    numberList.also { println("Populating the list") }
        .apply {
            add(2.71)
            add(3.14)
            add(1.0)
        }
        .also { println("Sorting the list") }
        .sort()
}

Context Object를 반환하는 함수의 return문에도 사용할 수 있다.

import kotlin.random.Random

fun writeToLog(message: String) {
    println("INFO: $message")
}

fun getRandomInt(): Int{
    return Random.nextInt(100).also{
        writeToLog("getRandomInt() generrated value $it")
    }
}

fun main() {
    val tmp = getRandomInt()
    println(tmp)
}

람다 결과(lambda result)

let, run, with은 람다 결과를 반환한다.

따라서 이 Scope함수들은 변수에 연산 결과를 할당할 때나, 연산 결과에 대해 추가적인 작업을 수행할 때 등의 상황에서 사용할 수 있다.

fun main() {
    val numbers = mutableListOf("one","two","three")
    
    val countEndWithE = numbers.run{
        add("four")
        add("five")
        count{
            it.endsWith("e")
        }
    }
    println("There are $countEndWithE elements that end with e.")
}

또한, 반환 값을 무시하고 바로 람다식을 사용하여, 임시 범위를 만들어서 사용할 수도 있다.

fun main() {
   val numbers = mutableListOf("one","two","three")
   with(numbers){
       val firstItem = first()
       val lastItem = last()
       println("첫번째 항목: $firstItem, 마지막 항목: $lastItem")
   }
}

Scope 함수들


먼저 Scope함수 간의 주요한 차이점을 표를 통해 확인하자.

Scope 함수객체 참조 방법반환 값확장 함수 여부
letit람다 결과O
runthis람다 결과O
run-람다 결과X → 컨텍스트 객체 없이 호출됨
withthis람다 결과X → 컨택스트 객체를 인자로 사용함
applythis컨텍스트 객체O
alsoit컨텍스트 객체O

아래는 각 함수에 대한 간단한 설명이다.

  • let : null이 아닌 객체에 람다를 실행할 때, local(지역) scope에서 표현식(expression)을 변수로 선언할 때 사용한다.
  • apply: 객체를 선언(configuration)하는 경우 사용한다.
  • run: 객체 선언(configuration)과 결과를 계산할 때 사용한다.
  • (non_extension) run : 비 확장 run은, 표현식(expresson)이 필요한 곳에 구문(statements)를 실행할 때 사용한다.
  • also : 부가적인 실행을 할 때 사용한다.
  • with : 객체에 대한 그룹 함수를 호출할 때 사용한다.

상황에 맞는 scope함수를 선택할 수 있도록 권장되는 방식의 사용 예시와 함께 조금 더 자세한 설명을 작성하겠다.

함수는 상황에 따라 교체될 수 있다. 그렇기 때문에 예시들은 일반적인 사용 방식을 정의한다.

let

  • Context Object: it
  • Return Value: lambda result

객체의 결과값에 하나 이상의 함수를 호출(invoke)하는 경우 사용한다.

예를 들자면, 다음 예제는 컬렉션의 두 연산의 결과를 출력한다.

fun main() {
		//⭐ let을 사용하지 않은 경우
    val numbers = mutableListOf("one","two","three","four","five")
    val resultList = numbers.map{ it.length }.filter{ it>3 }
    println(resultList)

		//⭐ let을 사용한 경우
		val numbers2 = mutableListOf("one","two","three","four","five")
    numbers2.map{ it.length }.filter {it>3}.let{
        println(it)
        // 🔎 필요 시, 더 많은 함수를 호출할 수 있다.
    }
}
// >>> [5, 4, 4]

만약 코드 블럭에 it을 인자로 갖는 단일 함수가 존재한다면, 람다 대신 메서드 참조(::)를 사용할 수도 있다.

val numbers2 = mutableListOf("one","two","three","four","five")
numbers2.map{ it.length }.filter {it>3}.let(::println)

또한, let은 null이 아닌 값으로만 코드 블록을 실행시키고 싶을 때 자주 사용된다.

null이 아닌 객체에 대해 작업을 수행하려면 안전한 호출 연산자(?.)를 let에 사용하도록 한다.

val str:String? = "Hello"
val length = str?.let{ //⭐ Safe Call(안전한 호출)
		println("let() invoke $it")
}

let을 사용하는 다른 방법은 코드 가독성을 높이기 위해 제한된 scope 내에서의 지역변수를 사용하는것이다. 컨텍스트 객체를 위해 새로운 변수를 정의하기 위해서는 기본적으로 사용되는 it 대신에 람다의 인자로 변수 이름을 넣어서 사용하면 된다. 개인적으로 이 방법을 추천한다.

fun main() {
    val numbers = listOf("one","two","three","four")
    val modifiedFirstItem = numbers.first().let{ firstItem ->
    	println("The first item of the list is '$firstItem'")
        if(firstItem.length >= 5) firstItem else "!" + firstItem +"!"
    }.toUpperCase()
    println("First item after modifications: '$modifiedFirstItem'")
}

// >>> The first item of the list is 'one'
// >>> First item after modifications: '!ONE!'

with

  • Context Object: this
  • Return Value: lambda result

non-extension function(비 확장 함수)로, 컨텍스트 객체는 인자로 전달되지만, 람다 안에서는 수신자인 this로 사용 가능하다.

with은 람다 결과 없이, 이미 생선된 Context Object 객체를 인자로 받아서 사용하는 것이 효율적인 경우에 사용할 것으로 추천한다.

fun main() {
  val numbers = mutableListOf("one","two","three")
  with(numbers){ //⭐ 여기서의 with은 "이 객체로 다음을 수행하라"는 뜻으로 읽을 수 있다.
      println("'with' is called with argument $this")
      println("It contains $size elements")
  }
}

with의 또 다른 사용법은 값을 계산하는 데에 사용되는 헬퍼 객체(helper object)의 프로퍼티나 함수를 선언하는 데에 사용하는 것이다.

fun main() {
  val numbers = mutableListOf("one","two","three")
  val firstAndLast = with(numbers){
      "The first elements is '${first()}', "+
      "The last elements is '${last()}'"
  }
  println(firstAndLast)
}

run

  • Context Object: this
  • Return Value: lambda result

with과 비슷한 역할로, 이미 생성된 Context Object객체를 사용할 때 호출하며, with과는 전달받는 위치가 다르다.

그리고, with과 가장 중요한 차이점은, 앞에 Safe Call(?.)을 붙여서 null 체크까지 할 수 있기 때문에, with보다는 run이 더 자주 사용된다.

run은 람다가 객체 초기화와 리턴 값 연산을 모두 포함하고 있는 경우에 유용하다.

// normal code
    val point = Point()
    val width = point.x * 0.5
    
    // run() codes
    val width = run{
        val point = Point()
        point.x * 0.5
    }
    
    val point = Point()
    val width = point.run{
        x * 0.5
    }
    
    imageView.layoutParams.run{
        width = 400
        height = 200
    }

apply

  • Context Object : this
  • Return Value: context Object

apply는 보통 객체 초기화 시에 가장 많이 사용된다.

val park = Person("Park").apply{
		age = 28
}

also

  • Context Object: it
  • Return Value : context object

also는 기존 객체를 수정하거나 변경하지 않고, 디버깅을 위한 로깅 등의 추가적인 부가 작업을 하려고 할 때 사용한다.

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("새 항목 추가하기 전 리스트 요소들: $it") }
    .add("four")

정말 길고 긴 글이었다.

다음 사진을 마지막으로 이만 글을 마무리 짓도록하겠다.


Uploaded by N2T