모든 내용은 이지스퍼블리싱의 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다.
Kotlin의 표준 함수
Kotlin 표준 라이브러리에서 제공하는 함수를 이용하면 코드를 더 단순화하고 가독성을 높일 수 있음
이 표준 함수는 람다식과 고차 함수를 이용하여 선언되어 있음
람다식
val 변수 이름: 자료형 = { 매개변수[, ...] -> 람다식 본문 }
val sum: (Int, Int) -> Int = { x, y -> x + y }
val mul: { x: Int, y: Int -> x * y}
val add: (Int) -> Int = { it + 1 }
val isPositive: (Int) -> Boolean = {
val isPositive = it > 0
isPositive // 반환값
}
val isPositiveLabel: (Int) -> Boolean = number@ {
val is Positive = it > 0
return@number isPositive // 라벨을 이용해 반환
}
- sum은 익명 함수
- mul은 변수의 자료형 표기가 생략된 형태 : (Int, Int) -> Int 형임을 알 수 있음
- add는 매개변수가 1개인 경우, 매개변수를 생략하고 it으로 표기할 수 있음
- 표현식이 여러 줄일 경우 마지막 줄의 표현식이 반환값으로 처리됨
- return@라벨 형태로 반환할 수도 있음
고차함수
함수의 매개변수로 함수를 받거나 함수 자체를 반환할 수 있는 함수
fun inc(x: Int): Int {
return x+1
}
fun high(name: String, body: (Int)->Int): Int {
println("name: $name")
val x = 0
return body(x)
}
// 아래처럼 사용 가능
val result1 = high("June", { x -> inc(x + 3) })
val result2 = high("June") { inc(it + 3) }
val result3 = high("June", ::inc)
val result4 = high("June") { x -> x + 3 }
val result5 = high("June") { it + 3 }
클로저(Closure)
람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념
이 외부 변수를 람다식 안의 외부 변수는 값을 유지하기 위해 람다식이 포획(Capture)한 변수라고 함
클로저에서는 포획한 변수는 참조가 유지되어 함수가 종료되어도 사라지지 않고 함수의 변수에 접근하거나 수정할 수 있게 해 줌
⭐ 기본적으로 함수 안에 정의된 변수는 지역 변수로서 스택에 저장되어 있다가 함수가 끝나면 같이 사라짐
[ 장점 ]
- 내부 람다식에서 외부 함수의 변수에 접근하여 처리할 수 있기 때문에 효율이 높음
- 완전히 다른 함수에서 변수에 접근하는 것을 제한할 수 있음
[ 조건 ]
- final 변수를 포획한 경우, 변수 값을 람다식과 함께 저장한다
- final이 아닌 변수를 포획한 경우, 변수를 특정 래퍼(wrapper)로 감싸서 나중에 변경하거나 읽을 수 있게 함. 이때 래퍼에 대한 참조를 람다식과 함께 저장한다
👆 Java에서는 외부 변수를 포획할 때 final만 가능!
∴ Kotlin에서 final이 아닌 변수를 포획한 경우에는 내부적으로 변환된 자바 코드에서 final로 지정해 사용함
fun main() {
val calc = Calc()
var result = 0 // 외부 변수
calc.addNum(2, 3) { x, y -> result = x + y } // 클로저
println(result)
}
class Calc {
fun addNum(a: Int, b: Int, add: (Int, Int) -> Unit) {
add(a, b)
}
}
Tools > Kotlin > Show Kotlin ByteCode > Decompile 버튼으로 자바 코드 확인
위에 var result로 선언한 변수 앞에 final이 붙은 것을 확인할 수 있음!
...
public final class ClosureTestKt {
public static final void main() {
Calc calc = new Calc();
final IntRef result = new IntRef();
result.element = 0;
calc.addNum(2, 3, (Function2)(new Function2() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1, Object var2) {
this.invoke(((Number)var1).intValue(), ((Number)var2).intValue());
return Unit.INSTANCE;
}
...
Kotlin의 표준 라이브러리
함수명 | 람다식의 접근 방법 | 반환 방법 |
T.let | it | block 결과 |
T.also | it | T caller (it) |
T.apply | this | T caller (this) |
T.run 또는 run | this | block 결과 |
with | this | Unit |
let() 함수
public inline fun <T, R> T.let(block: (T) -> R): R { ... return block(this) }
[ 용도 ] 객체의 상태를 변경할 수 있음
- 제네릭의 확장 함수 형태 → 어디든 사용 가능
- 매개변수로 람다식 형태의 block이 있고, T를 매개변수로 받아 R을 반환함
- let() 함수 또한 R을 반환함 ( = 람다식 결과 부분을 그대로 반환)
- 이 함수를 호출한 객체를 인자로 받는 형태 → 다른 메서드를 실행하거나 연산을 수행해야 하는 경우 사용
fun main() {
val score: Int? = 20
fun checkScore() {
if (score != null) {
println("Score: $score")
}
}
fun checkScoreLet() {
score?.let { println("Score: $it")}
val str = score.let { it.toString() }
println(str)
}
checkScore()
checkScoreLet()
}
- score는 nullable한 변수 이기 때문에 checkScore() 함수에서는 null 체크를 하고 있음
- checkScoreLet() 함수에서는 세이프 콜(?.)을 사용하여 null인 경우 람다식이 실행되지 않도록 함
- 첫번째 let : T->R: R 이므로 자기 자신의 값 it으로 score를 받아서 처리함(T.let이므로 T는 score)
- 두번째 let : 세이프 콜을 사용하지 않았기 때문에 score이 null이면 str도 null (세이프콜을 사용하지 않더라도 String?으로 추론되어 null이 할당됨
- score은 외부 변수이므로 클로저 사용됨
also() 함수
let() 함수와 역할이 거의 동일하지만, 코드 결과와 상관없이 T인 객체 this를 반환함
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
[ 용도 ] 객체의 속성을 전혀 사용하지 않거나 변경하지 않고 사용하는 경우
fun main() {
data class Person(var name: String, var skills: String)
var person = Person("June", "Kotlin")
val a = person.let {
it.skills = "Android"
"success"
}
println(person) // Person(name=June, skills=Android)
println("a: $a") // a: success
val b = person.also {
it.skills = "Java"
"success"
}
println(person) // Person(name=June, skills=Java)
println("b: $b") // b: Person(name=June, skills=Java)
}
- let()은 람다식의 마지막 표현인 "success"를 반환함
- also()는 람다식의 마지막 표현은 사용되지 않고, T인 객체 person을 반환함
특정 단위의 동작 분리
let()과 also()를 체이닝 형태로 구성해 간결하게 표현할 수 있으
let() 함수는 식의 결과를 반환하고, 그 결과를 다시 also() 함수에 넘기는 것!
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
apply() 함수
also()와 같이 호출하는 객체 T를 이어지는 block으로 전달하고 객체인 this를 반환함
특정 객체를 생성하면서 함께 호출해야 하는 초기화 코드가 있는 경우 사용
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
public inline fun <T> T.apply(block: T.( ) -> Unit): T {block(); return this }
[ 용도 ] 특정 객체를 초기화하는데 유용
fun main() {
data class Person(var name: String, var skills: String)
var person = Person("June", "Kotlin")
person.apply { this.skills = "Android" }
println(person) // Person(name=June, skills=Android)
val returnObj = person.apply {
name = "Lee"
skills = "Java"
}
println(person) // Person(name=Lee, skills=Java)
println(returnObj) // Person(name=Lee, skills=Java)
}
- also()와의 차이점
- 다르게 T.()와 같은 표현에서 람다식이 확장함수로 처리됨
- 객체를 넘겨 받는 방식에서 also()는 it을 사용(생략X)하는 반면, apply()는 this를 사용하여 생략할 수 있음
- apply()는 확장 함수로 person을 this로 받아 클로저를 사용하는 방식과 같음 → 객체의 프로퍼티를 변경하면 원본 객체에 반영
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
val f = File(path).apply { mkdirs() }
해당 예시는 위와 같이 고칠 수 있음
- File(path)에 의해 생성된 결과를 this로 받고
- File 객체의 mkdirs()를 호출하여 파일 경로를 생성하고
- 객체 this를 반환함
run() 함수
public inline fun <R> run(block: () -> R): R = return block() // 인자 없는 익명함수 형태
public inline fun <T, R> T.run(block: T.() -> R): R = return block() // 객체에서 호출하는 형태
- 인자 없는 익명함수처럼 동작하는 형태
- 확장함수도 아니고 block에 입력값이 없음 → 객체를 전달받아 속성을 변경하는 형식에 사용하는 함수가 아님
- 어떤 객체를 생성하기 위한 명령문을 block안에 묶어 가독성을 높이는 역할을 함
- 객체에서 호출하는 형태
- T의 확장함수이기 때문에 세이프 콜(.?)을 붙여 non-null인 경우에만 실행할 수 있음
- 어떤 값을 계산할 필요가 있거나 여러 개의 지역변수 범위를 제한할 때 사용함
- block이 독립적으로 사용됨
- 이어지는 block 내에서 처리할 작업을 넣어줄 수 있음
- 일반 함수와 마찬가지로 값을 반환하지 않거나 특정 값을 반환할 수 있음
apply()와 run() 비교
fun main() {
data class Person(var name: String, var skills: String)
var person = Person("June", "Kotlin")
val returnObj = person.apply {
this.name = "Lee"
this.skills = "Android"
"success"
}
println(person) // Person(name=Lee, skills=Android)
println(returnObj) // Person(name=Lee, skills=Android)
val returnObj2 = person.run {
this.name = "Kim"
this.skills = "C++"
"success"
}
println(person) // Person(name=Kim, skills=C++)
println(returnObj2) // success
}
- apply()의 결과는 위에서 든 예시와 같이 참조한 원본 객체에 변경이 일어남
- apply()는 this를 반환한 반면, run()은 마지막 표현식을 반환함
- 마지막 표현식을 구성하지 않으면 Unit이 반환됨
with() 함수
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
- run()과 기능은 거의 동일한데, with()는 receiver로 전달할 객체를 처리하므로 객체의 위치가 달라짐
- with()는 확장함수 형태가 아니고 단독으로 사용하는 형태
- 세이프 콜(.?)을 지원하지 않기 때문에 let()과 함께 사용되기도 함 (null이 아닌 경우가 확실하면 with만 사용해도 됨)
fun main() {
data class User(val name: String, var skills: String, var email: String? = null)
val user = User("June", "default")
val result = with (user) {
skills = "Kotlin"
email = "xxx@email.com"
}
println(user) // User(name=June, skills=Kotlin, email=xxx@email.com)
println("result: $result") // result: kotlin.Unit
}
use() 함수
특정 객체는 사용된 후 닫아야 하는 경우가 있는데, use()를 사용하면 객체를 사용하고 close()를 자동으로 호출하여 닫아줄 수 있음
public inline fun <T: Closeable?, R> T.use(block: (T) -> R): R
*람다식과 고차함수에 대한 내용은 아래 포스팅 참고!
reference >
'Kotlin > Kotlin 프로그래밍' 카테고리의 다른 글
[Kotlin] Kotlin의 싱글톤 패턴(Singleton Pattern) (2) | 2022.02.05 |
---|---|
[Kotlin/Coroutine] 코루틴(Coroutine)의 개념과 사용 (0) | 2021.06.16 |
[Kotlin/Coroutine] 코루틴(Coroutine) - 동시성 프로그래밍 (0) | 2021.06.03 |
[Kotlin] 코틀린의 시퀀스(Sequence) (2) | 2021.06.02 |
[Kotlin] 컬렉션(Collection)의 확장함수 (0) | 2021.05.27 |
[Kotlin] 컬렉션(Collection) - Set, Map (0) | 2021.05.26 |
[Kotlin] 컬렉션(Collection) - List (0) | 2021.05.24 |
[Kotlin] 문자열(String) (0) | 2021.05.21 |
댓글