Kotlin

[Kotlin 문법] apply / run / with / let / also 확장함수

gangmini 2024. 7. 1. 02:39
반응형

확장함수란?

  • 상속없이 클래스를 확장하는 것
  • 객체.확장함수 형태 
  • 해당 클래스/객체에서 사용할 함수를 만들 수 있음 -> 실제 클래스에 이런 함수가 추가되는 것 X
class Person {
    val age = 29
    val name = "똘이"
}

예를 들어 위와 같은 형태의 클래스는 age, name 두개의 프로퍼티로만 이루어졌고, 따로 함수는 가지고 있지 않다.

// 내가 만든 확장함수
fun Person.inCrease() {
    println("$age")
}

fun main() {

    val person = Person()
    person.inCrease() 
    
}

그런데 이렇게 내가 Person 이라는 클래스에 대해 inCrease() 라는 확장함수 를 만들어 마치 실제 Person 클래스의 함수처럼 사용 가능하다. 하지만 실제로 Person 클래스에 이 함수가 생긴 것은 아니다. 

fun main() {

    val person = Person()
 
    // run 확장함수
    person.run {
    	age = 29
    }

}

 

오늘 학습하게 될 run / let / apply / also / with 확장함수들은 이미 kotlin 에서 지원하고 있는 확장함수이고, 각자 고유한 특징을 가지고 있기 때문에 여기에 맞게 사용할 수 있는 것이다. 

위 예제에서는 실제 Person 클래스에 run() 이라는 함수는 없지만 kotlin 에 의해 자동으로 run 이라는 확장함수를 사용할 수 있게 된 것! (확장함수로 선언된 부분은 없지만 내부적으로 사용할 수 있게끔 처리되는 것 같다...!)


 

run / let / apply / also / with 

나는 이 함수들의 차이를 이해하기 위해서는 아래 2가지에 집중해보았다.

  1. 수신객체 → 어떤 객체를 받는가?
  2. 반환값 → 무엇을 반환하는가? (받았던 객체를 그대로 반환하는가? 아님 다릉 return 값이 있는가?)

apply run / with let also

람다식 내부에서 하는 일 자신의 프로퍼티 셋팅 수신객체에 대해 특정한 동작 수신객체에 대해 특정한 동작 자신의 프로퍼티 셋팅, 수신객체에 대해 특정한 동작
수신객체 접근하는 법 this this it it
return 값 자기자신 Block의 마지막 Block의 마지막 자기자신

** this : 수신객체를 람다의 수신객체로 전달하기 때문

** it : 수신객체를 람다의 파라미터로 전달하기 때문

 

예제 학습을 위해 데이터 클래스를 하나 만들어준다.

// 예시 클래스 1
data class Person(
    var name: String = "",
    var age: Int = 0,
    var temperature: Float = 36.5f
)

// 예시 클래스 2
data class Person(
    var name: String = "",
    var age: Int = 0,
    var temperature: Float = 36.5f
) {
    fun isSick(): Boolean = temperature > 37.5f
}

 

apply

  • ‘수신’ 이라는 것을 쉽게 말해서 ‘받는다’ 라는 뜻이다. 즉, T 타입의 객체를 apply 함수가 받고, 다시 이 T 타입의 객체를 람다 함수가 받는다는 의미이다.
  • 같은 객체를 받기 때문에 람다에 수신 객체는 굳이 명시해주지 않아도 된다. (하고 싶다면 this → 이렇게)
val person = Person().apply {  // this -> 
    name = "DevCho" // this.name = "DevCho"
    age = 29
    temperature = 36.2f
}
  • 내부 프로퍼티를 변경할 때 사용한다. 그리고 반환 타입을 보면 T 즉, 받았던 객체를 다시 반환한다. Person() 객체를 받아서 인자들을 변경시키고 다시 Person() 객체를 반환해주기 때문이다.

 

run

  • run이 T 타입의 객체를 받고, 람다식도 T타입의 객체를 받는다. 동일한 객체를 받기 때문에 역시나 람다식에는 수신객체를 명시할 필요는 없다.
val person = Person(name = "Devcho", age = 29, temperature = 36.5f)

val isPersonSick = person.run { // this -> 
       temperature = 37.2f
       
       temperature // return값 (return은 생략)
  }
  • 이번에는 반환값이 수신객체와 동일한 T 가 아니라 R 이다. 즉, 다른 값을 반환 한다는 의미이고, run은 Block 의 가장 마지막 줄을 반환한다.
  • 자동으로 자기자신(수신객체)가 반환되던 apply와는 달리 run 은 무엇을 반환할지 개발자가 지정해줘야 한다. 만약 자기 자신을 반환하고 싶다면 this 를 반환하면 된다
  • return 은 생략한다. 어차피 block 의 맨마지막 줄을 명시적으로 반환하기 때문이다.

 

with

 val person = Person(name = "Devcho", age = 29, temperature = 36.5f)
    val isPersonSick = with(person) {
        temperature = 38.0f
        temperature // return 값
    }
  • run 과 완전히 동일하게 동작한다고 한다. 차이점은 run은 객체.run() 이런식으로 확장함수 로 사용하지만 with는 수신객체를 파라미터 사용해 with(객체) 이런식으로 사용한다. 실제로 run 이 더 깔끔하게 사용하기 좋기 때문에 with는 잘 안 쓴다고 한다

이제 슬슬 수신객체가 무엇인지, 반환값이 어떻게 다른지 감이 잡힌다.

계속해서 let 과 also 를 살펴보자.

 

let

 val person = Person(name = "Devcho", age = 29, temperature = 36.5f)
 
 val isPersonSick = person.let { it -> 
        it.temperature = 38.0f
        temperature // return 값
    }
  • 수신객체를 받아 람다식에서 사용하고, 반환값으로는 Block의 마지막줄을 반환하게 된다. 즉, run이나 with와 거의 유사하게 사용된다. 수신객체를 람다식에서 접근할 때, this가 아닌 it을 사용한다는 차이가 있다

 

하지만 실제로 개발할 때 let은 조금 다르게 사용되곤 한다.

  • null 체크를 해서 null 이 아닐 경우에 let block을 실행하는 것이다.
  • null 체크에 사용되는 ? 연산자를 함께 사용해주면 된다.
 val person = Person(name = "Devcho", age = 29, temperature = 36.5f)
 
 val isPersonSick = person?.let { it -> // person이 null 이 아닌 경우 실행
        it.temperature = 38.0f
        
        isSick() // return 값
    }

그렇다면 null 인 경우에 실행을 하고 싶다며 어떻게 사용할까?

바로 run 을 block을 실행하면 된다. 

 val person = Person(name = "Devcho", age = 29, temperature = 36.5f)
 
 
 val isPersonSick = person?.let { // person이 null 이 아닌 경우 실행
        it.temperature = 38.0f
        
        isSick() // return 값
        
    } ?: run {
		    // person이 null인 경우 실행
    
    }

나도 자연스럽게 이런 방식을 사용하게 되었는데, 실무에서도 let 과 run 을 사용해 nullable 객체에 대해 서로 다른 실행문을 동작시키는 방식을 운영한다고 한다.

 

also (상당히 짜즈ㅇ....)

  • apply처럼 수신객체를 받아 프로퍼티 셋팅을 해주고 추가적으로 객체 자신에 대한 작업을 수행할 수도 있다. 그 다음 수신객체 자신을 반환한다.
    • also 의 반환 값은 자기자신이다.
    • 수신객체의 프로퍼티를 변경하게 되면 return 값도 프로퍼티가 변경된 수신객체가 반환
    • 객체를 자체를 다시 생성해 할당하는 경우에는 반환값에 영향 X
  • 람다식에서 수신객체를 사용하기 위해서는 it을 사용한다.
var number = 3;

fun getAndIncreaseNumber1() = number.also {
    number++
}

var person = Person("Devcho", 29, 36.2f);

fun getAndIncreaseNumber2() = person.also { 
    // 수신객체 프로퍼티 직접 변경(반환할 수신객체에도 영향을 미침
    person.age = it.age + 1
}

fun getAndIncreaseNumber3() = person.also {
    // 새로운 객체를 생성 -> 반환할 수신객체는 원래의 person
    // 이후에 새로운 Person 객체가 person에 셋팅
    person = person.copy(age = it.age + 1)
}

fun main() {
    println("first number ${getAndIncreaseNumber1()}")  // 3
    println("second number ${getAndIncreaseNumber1()}") // 4
    
    println("first number ${getAndIncreaseNumber2()}")  // 30
    println("second number ${getAndIncreaseNumber2()}") // 31
    
    println("first number ${getAndIncreaseNumber3()}")  // 29
    println("second number ${getAndIncreaseNumber3()}") // 30
}

 

 

[최고의 출처]

https://kotlinworld.com/255#범위 지정 함수(Scope function)란%3F-1

 

 

반응형