java에는 없고 kotlin에만 있는 함수 : let, also, apply, run
java에는 없고 kotlin에만 있는 함수로 let, also, apply, run 이 있다. 하나씩 살펴보자.
let
fun main() {
// let
val now : LocalDateTime?= null
// 타입에 대한 제네릭을 받아서 리턴해줌 -> map이랑 비슷
/**
* @kotlin.internal.InlineOnly
* public inline fun <T, R> T.let(block: (T) -> R): R {
* contract {
* callsInPlace(block, InvocationKind.EXACTLY_ONCE)
* }
* return block(this)
* }
*/
val kst = now?.let{
// 엘비스 연산자를 통해 있을 때만 처리하도록 함
println(it)
}
// 안에서 사용되는 지역 변수를 설정함
// 타입도 지정이 가능
val kst2 = now?.let{localDateTime ->
println(localDateTime)
}
val kst3 = now?.let{localDateTime:LocalDateTime ->
println(localDateTime)
}
// let의 특징 - > 마지막에 있는 것이 리턴됨
// 스코프 안에서
// return을 적지 않음
val kst4 = now?.let {localDateTime ->
println("")
"let scope"
} ?: LocalDateTime.now()
// let -> 특정한 객체를 받아서 마지막 라인을 리턴, default는 it으로 쓰지만 변수명을 쓸 수 있음
}
fun logic(userDto: UserDto?): UserResponse? {
return userDto?.let {
println(it)
UserEntity(
name = it.name
)
}?.let { userEntity ->
println(userEntity)
UserResponse(
name = userEntity.name
)
}
}
data class UserDto(
var name:String?=null,
)
data class UserEntity(
var name:String?=null
)
data class UserResponse(
var name:String?=null
)
// map 이랑 let 차이 ?
// map -> collection, let -> 모든 객체
let 함수란?
let
함수는 Kotlin의 고차 함수 중 하나로, 호출한 객체를 인자로 받아 원하는 작업을 수행하고 마지막 표현식의 결과를 반환한다. 주로 null이 아닐 경우에만 특정 로직을 실행하거나, 람다 표현식 내부에서 사용되는 지역 변수를 명시적으로 지정할 때 사용된다.
기본 사용법
기본적으로 let
함수는 객체가 null이 아닐 때 람다 표현식을 실행한다.
val now: LocalDateTime? = null
val kst = now?.let {
println(it)
}
지역 변수 설정
let
함수 내부에서 사용하는 변수의 이름을 직접 지정할 수 있다.
val kst2 = now?.let { localDateTime ->
println(localDateTime)
}
타입 지정
또한 변수의 타입을 명시적으로 지정할 수도 있다.
val kst3 = now?.let { localDateTime: LocalDateTime ->
println(localDateTime)
}
반환값
let
함수는 마지막 표현식의 결과를 반환한다. 따라서 원하는 값을 반환하려면 마지막 줄에 그 값을 적어주면 된다.
val kst4 = now?.let { localDateTime ->
println("")
"let scope"
} ?: LocalDateTime.now()
let과 map의 차이
let
은 모든 객체에 사용할 수 있는 반면, map
함수는 주로 컬렉션에서만 사용된다.
fun logic(userDto: UserDto?): UserResponse? {
return userDto?.let {
UserEntity(
name = it.name
)
}?.let { userEntity ->
UserResponse(
name = userEntity.name
)
}
}
also
Kotlin의 also
함수는 let
과 유사하게 객체를 인자로 받아 원하는 작업을 수행할 수 있다. 그러나 also
의 주된 특징은 작업이 끝나면 그대로 원래의 객체를 반환한다는 것이다. also
함수의 동작 방식과 사용 케이스에 대해 살펴보자.
fun main() {
// also -> 또한
// 넘어온 객체를 그대로 리턴 -> map이랑 동일한 부분은 키워드를 it으로 받되 변경 가능
// 타입도 지정 가능
// let이랑 차이 ? -? let은 마지막 라인을 리턴
// also는 받은 파라미터를 그대로 리턴한다.
/**
* @kotlin.internal.InlineOnly
* @SinceKotlin("1.1")
* public inline fun <T> T.also(block: (T) -> Unit): T {
* contract {
* callsInPlace(block, InvocationKind.EXACTLY_ONCE)
* }
* block(this)
* return this
* }
*/
UserDto(
name = "홍길동"
).also {
println(it)
// let 이랑 차이는 받은 것을 그대로 리턴하므로 아래 코드를 리턴하지 않음
UserDto(name = "유관순")
}
// 그대로 넘기므로 로깅 등에 사용
}
also 함수의 정의와 특징
also
함수는 람다 표현식을 인자로 받아 이 블록 안에서 원하는 로직을 수행한다. 그 후, 받은 객체를 그대로 반환한다. 이를 통해 메서드 체이닝(method chaining)이나 로깅 같은 작업이 용이해진다.
public inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
기본 사용법
also
함수를 이용하여 객체를 생성과 동시에 그 객체에 대한 추가 작업을 할 수 있다.
UserDto(
name = "홍길동"
).also {
println(it)
}
반환값
also
함수는 작업이 끝나면 원래의 객체를 반환한다. 이 특징은 let
과는 다르다. let
은 마지막 표현식의 결과를 반환하기 때문에, 두 함수의 반환값에 차이가 생긴다.
// also는 원래 객체를 그대로 반환
val user = UserDto(name = "홍길동").also {
UserDto(name = "유관순")
}
println(user.name) // 출력: "홍길동"
로깅 및 디버깅
also
함수는 로깅 또는 디버깅에 자주 사용된다. 이는 작업 중간에 상태를 확인하거나 로그를 출력하고 싶을 때 유용하다.
val list = mutableListOf(1, 2, 3).also {
println("리스트 초기 상태: $it")
}.apply {
add(4)
}.also {
println("리스트 최종 상태: $it")
}
apply
Kotlin에서는 객체의 초기화와 동시에 다양한 설정을 쉽게 할 수 있도록 apply
함수를 제공한다.
fun main() {
// apply, 생성자 패턴
// t 라는 제네릭에 함수가 있음 -> t 제네릭 확장 함수
// block이라는 람다식이 넘어가게 되어 있음
// 반환형이 없는 Unit이 매개변수로 넘어감
// also 랑 비교 ? -> also의 경우 타입을 넣어줌 -> it.name으로 접근이 가능
// apply는 특정 클래스에 function을 추가 -> 확장 함수 람다
// 람다식이 넘어가는데 그것이 확장함수
// apply는 초기화를 시킬 때 많이 사용하는데 키워드가 this로 넘어옴 -> 수신 받은 객체 본인
/**
* @kotlin.internal.InlineOnly
* public inline fun <T> T.apply(block: T.() -> Unit): T {
* contract {
* callsInPlace(block, InvocationKind.EXACTLY_ONCE)
* }
* block()
* return this
* }
*/
var userDto = UserDto().apply {
this.name="홍길동"
name = "홍길동"
""
}
// 수신 받은 객체를 리턴하는데, this를 변경할 수는 없음
// 초기화 블록
// 마지막 라인을 리턴하는 게 아니고 수신 받은 객체를 그대로 리턴하므로 변경 x
println(userDto)
// also 와 유사 -> 수신 받은 객체를 그대로 전달
// 생성자때 많이 사용한다.
}
fun String.myStr(){
// 본인에게 접근할 때 this로 접근
}
fun <T> T.myStr():Unit{
// 즉, T에다가 본인 자신 접근 -> this.
// 모든 제네릭에다가 apply 적용 모든 제네릭 클래스들이 가지고 있는 기본 메서드 -> 넘겨주는 확장 함수는 이 안에서만 쓸 수 있다.
// 본인을 던져주기 때문에
// 즉 apply에 넘어가는 것은 확장 함수가 넘어가면 됨
}
fun UserDto.myUserDto(){
println(this.name)
}
apply 함수의 정의와 특징
apply
함수는 수신 객체(this)의 람다에서 호출되는 확장 함수이다. 이 함수는 초기화 작업이나 값을 변경할 때 주로 사용된다.
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
기본 사용법
apply
함수는 객체 생성과 동시에 해당 객체의 필드를 쉽게 초기화할 수 있다.
val userDto = UserDto().apply {
name = "홍길동"
}
이 예시에서는 UserDto
객체를 생성하고, apply
함수를 이용해 이름을 초기화한다. 이렇게 하면, 생성자에서 이런 작업을 별도로 해주지 않아도 된다.
apply와 also의 차이
apply
함수는 수신 객체를this
로 참조. 따라서, 수신 객체의 멤버에 바로 접근할 수 있다.also
함수는 수신 객체를it
로 참조.
// apply 사용 예
val user1 = UserDto().apply {
name = "홍길동"
}
// also 사용 예
val user2 = UserDto().also {
it.name = "유관순"
}
확장 함수의 사용
apply
는 수신 객체의 멤버 뿐만 아니라 수신 객체를 이용한 확장 함수도 호출할 수 있다.
fun UserDto.init() {
name = "초기 이름"
}
val userDto = UserDto().apply {
init()
}
수신 객체를 this
로 참조하여 람다 블록 내에서 자유롭게 멤버나 함수를 호출할 수 있다. 이 함수는 객체 생성과 초기화를 한 번에 수행하므로 코드를 간결하게 만들어 준다.
run
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
/**
* @author : Rene
* @since : 2023/10/22
*/
fun main(){
// run , 지역 scope
/**
* @kotlin.internal.InlineOnly
* public inline fun <T, R> T.run(block: T.() -> R): R {
* contract {
* callsInPlace(block, InvocationKind.EXACTLY_ONCE)
* }
* return block()
* }
*/
// let 함수와 동일하게 마지막에 있는 게 리턴된다.
// 확장 함수를 전달 받아 안에 있는 것을 바로 접근 가능하다.
val userDto = UserDto("").run {
name = "홍길동"
"empty"
}
println("result $userDto")
val x = 10
// 주로 지역 스코프를 만들 때 많이 사용된다.
// 안에 있는 x를 통해서 연산이 됨
val sum = run {
val x = 2
val y = 3
x + y
}
println(sum)
val now: LocalDateTime? = null
val n = now?.let { it }?: LocalDateTime.now()
val d = now?.let {
val minusTime = it.minusDays(1)
minusTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
// 특정 스코프를 만들어 주어야 할 때
// 위에서는 로직이 필요 없음 -> 로직을 만들어야 한다면 스코프를 만들어야 하기 때문에 run을 만들고 사용하고자 하는 로직을 넣는다.
}?: run {
val now = LocalDateTime.now()
val minusTime = now.minusDays(1)
minusTime.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
}
println("result $d")
}
run 함수의 정의와 특징
run
함수는 let
과 유사하게 마지막 라인을 리턴하는 특징이 있다. 다만, run
은 수신 객체를 this
로 참조하기 때문에, 람다 블록 내에서 수신 객체의 멤버에 바로 접근할 수 있다.
public inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}
기본 사용법
val userDto = UserDto("").run {
name = "홍길동"
"empty"
}
println("result $userDto") // 출력: "result empty"
지역 스코프의 생성
run
함수는 주로 지역 스코프를 생성할 때 사용된다.
val sum = run {
val x = 2
val y = 3
x + y
}
println(sum) // 출력: 5
여기에서 run
내부의 x
와 y
는 run
블록 외부에서 접근할 수 없다. 이렇게 지역 스코프를 생성하여 변수의 범위를 제한할 수 있다.
조건부 로직에 활용
run
은 특정 조건에 따라 다른 로직을 실행할 때 유용하게 사용될 수 있다.
val d = now?.let {
it.minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
} ?: run {
LocalDateTime.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"))
}
println("result $d")
이 예시에서는 now
값이 null
인지 아닌지에 따라 다른 로직을 실행한다. null
이 아니면 let
을, null
이면 run
을 사용하여 로직을 처리한다.
이처럼 Kotlin의 run
함수는 주로 지역 스코프를 만들거나, 조건부로 다른 로직을 실행할 때 유용하게 사용된다. 수신 객체를 this
로 참조하여 람다 블록 내에서 자유롭게 멤버나 함수를 호출할 수 있어 매우 유용한 함수이며, let과 함께 많이 사용된다.
reference
- fastcampus 시그니처 백엔드 path 초격차 패키지 course 5
- https://kotlinlang.org/spec/introduction.html#reference