확장함수란?
- 상속없이 클래스를 확장하는 것
- 객체.확장함수 형태
- 해당 클래스/객체에서 사용할 함수를 만들 수 있음 -> 실제 클래스에 이런 함수가 추가되는 것 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가지에 집중해보았다.
- 수신객체 → 어떤 객체를 받는가?
- 반환값 → 무엇을 반환하는가? (받았던 객체를 그대로 반환하는가? 아님 다릉 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
'Kotlin' 카테고리의 다른 글
[Kotlin] 고급 문법 (0) | 2022.07.13 |
---|---|
[Kotlin] 기본 문법 (0) | 2022.07.12 |