출처: https://www.boostcourse.org/mo132/lecture/59989?isDesc=false
람다식과 고차함수에 대한 자세한 내용은 이전 포스트 참고하기!
https://velog.io/@jxlhe46/Kotlin-4-1
https://velog.io/@jxlhe46/Kotlin-5-2
val sum: (Int, Int) -> Int = { x, y -> x + y }
val mul = { x: Int, y: Int -> x * y }
val add: (Int) -> Int = { it + 1 } // {a->a+1} 매개변수가 하나일 때는 화살표 대신 it로 대체 가능
val isPositive: (Int) -> Boolean = {
val isPositive = it > 0
isPositive // 마지막 표현식 반환
}
val isPositiveLabel: (Int) -> Boolean = number@ {
val isPositive = it > 0
return@number isPositive // 람다식에서 리턴문을 사용하려면 라벨을 붙여야 함.
}
함수의 매개변수로 함수를 받거나 함수 자체를 반환할 수 있는 함수
package chap06.section1
fun high(name: String, body: (Int) -> Int): Int {
println("name: $name")
val x = 0
return body(x) // 0을 인자로 전달
}
fun inc(i: Int): Int {
return i + 1 // 1을 증가시킨 값을 반환
}
fun highNoArg(body: (Int) -> Int): Int {
val x = 0
return body(x) // 0을 인자로 전달
}
fun main() {
// 일반 함수 inc를 이용한 람다식
val result = high("Sean", { x -> inc(x + 3) }) // inc(3) 호출
println(result) // 4
// 마지막 인자로 사용된 람다식을 소괄호 밖으로 빼내기
val result2 = high("Sean") { x -> inc(x + 3) }
println(result2) // 4
// 일반 함수 참조에 의한 호출
val result3 = high("Sean", ::inc) // inc(0) 호출
println(result3) // 1
// 람다식 자체를 넘겨주는 경우
val result4 = high("Sean") { x -> x + 3 }
println(result4) // 3
// 매개변수가 한 개인 경우, 화살표 생략하고 it로 대체
val result5 = high("Sean") { it + 3 }
println(result5) // 3
// 일반 매개변수가 없고 람다식이 유일한 인자인 경우, 소괄호 생략
val result6 = highNoArg { it + 3 }
println(result6) // 3
}
name: Sean
4
name: Sean
4
name: Sean
1
name: Sean
3
name: Sean
3
3
코틀린은 실행 시점에서 람다식의 모든 참조가 포함된 닫힌(closed) 객체를 람다 코드와 함께 저장한다. 이때 이러한 데이터 구조를 클로저(closure)라고 부른다. 기본적으로 함수 안에 정의된 변수는 로컬 변수로, 스택에 저장되어 있다가 함수가 끝나면 같이 사라지게 된다. 하지만 클로저라는 개념에 의해 포획된 변수는 참조가 유지되어서 람다 함수가 종료되어도 사라지지 않으며, 접근 및 수정할 수 있다.
package chap06.section1
class Calc {
fun addNum(a: Int, b: Int, add: (Int, Int) -> Unit) {
add(a, b) // 람다식 add 자체는 반환 값이 없음.
}
}
fun main() {
val calc = Calc()
var result = 0 // 람다식 외부의 변수
calc.addNum(2, 3) { x, y ->
result = x + y // 외부 변수 result를 포획 (capture)하여 값 변경
}
println(result) // 변경된 값이 유지되어 5가 출력됨.
}
위 코드에서 result는 람다식 내부에서 재할당 되어 사용되는데 이때 할당된 값은 유지되어 출력문에서 사용할 수 있게 된다. 클로저에 의해 독립된 복사본을 갖고 사용되는 것이다!
함수에서는 다음과 같이 매개변수를 이용할 수도 있다.
package chap06.section1
// 길이가 일치하는 이름만 반환
fun filteredNames(length: Int){
val names = arrayListOf("Kim", "Hong", "Go", "Hwang", "Jeon")
val filterResult = names.filter {
it.length == length // 바깥의 length (함수의 매개변수)에 접근
}
println(filterResult)
}
fun main() {
filteredNames(4)
}
[Hong, Jeon]
이 경우에는 함수 자체를 같이 포획해 해당 매개변수에 접근한다고 이해할 수 있다. 이처럼 클로저를 사용하면, 람다식의 내부 함수에서 외부 변수에 접근할 수 있고 처리의 효율성을 높일 수 있다. 그리고 완전히 다른 함수에서 변수에 접근하는 것을 제한할 수도 있다.
람다식을 사용하는 코틀린의 표준 라이브러리에서 let(), apply(), with(), also(), run() 등 여러가지 표준함수를 제공하고 있다. 이 함수들을 이용하면 기존의 복잡한 코드를 단순화하고 효율적으로 만들 수 있다!
표준함수들은 대략 확장 함수 형태의 람다식으로 구성되어 있다.
함수명 | 람다식의 접근 방법 | 반환 방법 |
---|---|---|
T.let | it | block 결과 |
T.also | it | T caller(it) |
T.apply | this | T caller(this) |
T.run 또는 run | this | block 결과 |
with | this | Unit |
T는 형식 매개변수이며, 어떤 타입으로도 사용될 수 있다는 걸 의미한다. 위의 표에 있는 표준 함수들에 대해 차근차근 알아보자!
let()은 함수를 호출하는 객체 T를 이어지는 block의 인자로 넘기고, block의 결과값 R을 반환한다.
public inline fun <T, R> T.let(block: (T) -> R): R { ... return block(this) }
return block(this)
은 람다식의 결과를 그대로 반환한다는 뜻 package chap06.section1
fun main() {
val score: Int? = 32
//var score = null
// 일반적인 널 검사
fun checkScore(){
if(score != null){
println("Score: $score")
}
}
// let을 사용해 null 검사를 제거
fun checkScoreLet(){
score?.let { println("Score: $it") } // 널이 아닐 때만 블록 실행
val str = score.let { it.toString() } // int에서 string으로 변환
println(str)
}
checkScore()
checkScoreLet()
}
Score: 32
Score: 32
32
package chap06.section1
fun main() {
var a = 1
val b = 2
a = a.let { it + 2 }.let { // a의 값 1을 it에 복사
println("a = $a") // 1
val i = it + b // 3 + 2
i // 마지막 식 반환
}
println(a) // 5
}
a = 1
5
package chap06.section1
fun main() {
val x = "Kotlin!"
x.let { outer ->
outer.let { inner ->
// 이때는 it를 사용하지 않고 명시적 이름을 사용
print("Inner is $inner and outer is $outer")
}
}
}
Inner is Kotlin! and outer is Kotlin!
package chap06.section1
fun main() {
val x = "Kotlin!"
// 반환값은 바깥쪽의 람다식에만 적용됨.
val result = x.let { outer ->
outer.let { inner ->
println("Inner is $inner and outer is $outer")
"Inner String" // 이것은 반환되지 않음.
}
"Outer String" // 이 문자열이 반환되어 result에 할당됨.
}
println(result)
}
Inner is Kotlin! and outer is Kotlin!
Outer String
let을 세이프 콜(?.)과 함께 사용하면, if(null != obj)와 같은 null 검사 부분을 대체할 수 있다.
var obj: String? // nullable 변수
...
if(null != obj){ // obj가 널이 아닐 때만 작업 수행
Toast.makeText(applicationContext, obj, Toast.LENGTH_LONG).show()
}
👇
obj?.let { // Safe Call과 let() 함수 사용
Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}
val firstName: String?
var lastName: String
...
if(null != firstName) {
print("$firstName $lastName")
}else {
pring("$lastName")
}
👇
firstName?.let { print("$it $lastName") } ?: print("$lastName")
also()는 함수를 호출하는 객체 T를 이어지는 block에 전달하고, 객체 T 자체를 반환한다.
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun T.also(block: (T) -> Unit): T { block(this); return this }
also는 블록 안의 코드 수행 결과와 상관없이 객체 T 자체를 반환한다.
var m = 1
m = m.also { it + 3 }
println(m) // 원본 값 1
also는 let과 역할이 거의 동일해 보이지만, 자세히 보면 반환 값이 다르다! let은 마지막으로 수행된 코드 블록의 결과를 반환하지만, also는 블록 안의 코드 수행 결과와 상관없이 객체 T 자체를 반환한다.
package chap06.section1
fun main() {
data class Person(var name: String, var skills: String)
val person = Person("Kildong", "Kotlin")
val a = person.let {
it.skills = "Android"
"success" // 마지막 문장을 결과로 반환
}
println(person)
println("a: $a") // String
val b = person.also {
it.skills = "Java"
"success" // 마지막 문장은 사용되지 않음.
}
println(person)
println("b: $b") // Person의 객체 b
}
Person(name=Kildong, skills=Android)
a: success // 코드 블록의 결과 반환
Person(name=Kildong, skills=Java)
b: Person(name=Kildong, skills=Java) // 객체 자체를 반환
import java.io.File
// 디렉토리 생성 함수
fun makeDir(path: String): File{
val result = File(path)
result.mkdirs()
return result
}
fun makeDir2(path: String) = path.let{ File(it) }.also{ it.mkdirs() }
let은 식의 결과를 반환하고 그 결과를 다시 also를 통해 넘긴다. 이때 중간 결과가 아니라 넘어온 결과만 반환한다.
apply() 함수는 also() 함수와 마찬가지로 호출하는 객체 T를 이어지는 block으로 전달하고, 객체 자체인 this를 반환한다.
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun T.also(block: (T) -> Unit): T { block(this); return this }
public inline fun T.apply(block: T.() -> Unit): T { block(); return this }
package chap06.section1
fun main() {
data class Person(var name: String, var skills: String)
val person = Person("Kildong", "Kotlin")
// 여기서 this는 person 객체를 가리킴.
person.apply { this.skills = "Swift" }
println(person)
val returnObj = person.apply {
name = "Sean" // this는 생략할 수 있음.
skills = "Java" // this 없이 객체의 멤버에 여러번 접근
}
println(person)
println(returnObj)
}
Person(name=Kildong, skills=Swift)
Person(name=Sean, skills=Java)
Person(name=Sean, skills=Java)
📌 also()와 apply() 비교
person.also { it.skills = "Java" ] // it으로 받고 생략할 수 없음.
person.apply { skills = "Swift" } // this로 받고 생략 가능
apply()를 사용하면, 객체 자체인 this가 반환되므로 위와 같이 param 객체의 이름을 매번 써주지 않고도 멤버 변수의 값을 바꿀 수 있다. 이것은 사실상 클로저를 사용하는 방식과 같다. 따라서 블록 내에서 객체의 속성을 변경하면 원본 객체에도 반영되고, 또한 이 객체는 this로 반환된다.
마찬가지로, File(path)로 생성된 객체 자체를 apply()는 this로 받아서 블록 내부로 전달한다.
public inline fun run(block: () -> R): R = return block()
public inline fun <T, R> T.run(block: T.() -> R): R = return block()
package chap06.section1
fun main() {
var skills = "Kotlin"
println(skills) // Kotlin
val a = 10
skills = run { // 인자가 없는 익명 함수 형태
val level = "Kotlin Level: $a"
level// 마지막 표현식 반환
}
println(skills) // Kotlin Level: 10
}
package chap06.section1
fun main() {
// apply와 run 비교
data class Person(var name: String, var skills: String)
val person = Person("Kildong", "Kotlin")
val returnObj = person.apply {
this.name = "Sean" // this 생략 가능
this.skills = "Java"
"success" // The expression is unused
}
println(person)
println("returnObj: $returnObj") // 객체 자체를 반환
val returnObj2 = person.run {
this.name = "Dooly" // this 생략 가능
this.skills= "C#"
"success"
}
println(person)
println("returnObj2: $returnObj2") // 마지막 표현식이 반환됨.
}
Person(name=Sean, skills=Java)
returnObj: Person(name=Sean, skills=Java)
Person(name=Dooly, skills=C#)
returnObj2: success
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
supportActionBar?.let { // supportActionBar가 null이 아니면
with(it) { // 그 객체를 with의 리시버로 받아온다.
setDisplayHomeAsUpEnabled(true) // this 생략 가능
setHomeAsUpIndicator(R.drawable.ic_clear_white)
}
}
supportActionBar?.run { // 객체 supportActionBar 자체가 this로 넘어옴.
setDisplayHomeAsUpEnabled(true) // this 생략 가능
setHomeAsUpIndicator(R.drawable.ic_clear_white)
}
let과 with를 같이 사용하는 건 결국 run과 동일하다! 위의 예시에서 알 수 있듯이, 널 처리를 할 때는 run을 확장 함수 형태로 사용하는 게 더 좋다.
package chap06.section1
fun main() {
data class User(val name: String, var skills: String,
var email: String? = null)
val user = User("Kildong", "default")
val result = with(user){
skills = "Kotlin" // this 생략 가능
email = "kildong@example.com"
}
println(user)
println("result: $result")
}
User(name=Kildong, skills=Kotlin, email=kildong@example.com)
result: kotlin.Unit
with() 함수의 람다식에서 객체의 속성만 변경하고 반환되는 값이 없으면 위의 결과처럼 Unit이 반환된다.
즉, with()는 기본적으로 Unit을 반환하지만 필요에 따라 다음과 같이 마지막 표현식을 반환할 수도 있다.
val result = with(user){
skills = "Java"
email = "kildong@example.com"
"success" // 마지막 표현식 반환
}
함수명 | 람다식의 접근 방법 | 반환 방법 |
---|---|---|
T.let | it | block 결과 |
T.also | it | T caller(it) |
T.apply | this | T caller(this) |
T.run 또는 run | this | block 결과 |
with | this | Unit |
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun T.also(block: (T) -> Unit): T { block(this); return this }
public inline fun T.apply(block: T.() -> Unit): T { block(); return this }
let()은 함수를 호출하는 객체 T를 이어지는 block의 인자로 넘기고, block의 결과값 R을 반환한다.
also()는 함수를 호출하는 객체 T를 이어지는 block에 전달하고, 객체 T 자체를 반환한다. 즉, also()는 블록 안의 코드 수행 결과와 상관없이 객체 T 자체를 반환하는 것이다.
also는 let과 역할이 거의 동일해 보이지만, 자세히 보면 반환 값이 다르다! let은 마지막으로 수행된 코드 블록의 결과를 반환하지만, also는 블록 안의 코드 수행 결과와 상관없이 객체 T 자체를 반환한다.
apply() 함수는 also() 함수와 마찬가지로, 호출하는 객체 T를 이어지는 block으로 전달하고, 객체 자체인 this를 반환한다. 특정 객체를 생성하면서 함께 호출해야 하는 초기화 코드가 있을 때 사용할 수 있다.
public inline fun run(block: () -> R): R = return block()
public inline fun <T, R> T.run(block: T.() -> R): R = return block()
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
이렇게만 보면 복잡해보이지만, 위에서 설명했던 예시들처럼 실제로 사용을 많이 많이 해보면서 코틀린의 표준함수에 익숙해지도록 하자! 이 함수들은 우리를 괴롭히는 존재가 아니라, 우리가 개발을 더 편하게 할 수 있도록 만들어진 고마운 존재들이다! 🤗
use()를 사용하면, 객체를 사용한 후 close() 등을 자동으로 호출하여 닫아준다.
// 표준 함수의 정의
public inline fun <T: Closable?, R> T.use(block: (T) -> R): R 또는
public inline fun <T: AutoClosable?, R> T.use(block: (T) -> R): R
private String readFirstLine() throws FileNotFoundException {
BufferedReader reader = new BufferedReader(new FileReader("test.file"));
try{
return reader.readLine();
}catch (IOException e){
e.printStackTrace();
}finally {
try {
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
return null;
}
private fun readFirstLine(): String {
BufferedReader(FileReader("test.file")).use { return it.readLine() }
}
코드가 훨씬 더 간결해졌다! BufferedReader가 생성한 객체를 use()가 받아서 람다식의 it에 넘기면 끝이다! 코틀린의 표준 라이브러리인 use()의 구현부를 확인해보면, 내부적으로 예외 처리를 하고 있다는 걸 알 수 있다!
package chap06.section1
import java.io.FileOutputStream
import java.io.PrintWriter
fun main() {
PrintWriter(FileOutputStream("d:\\test\\output.txt")).use {
it.println("hello")
}
}
// 표준 함수의 정의
public inline fun T.takeIf(predicate: (T) -> Boolean): T?
= if (predicate(this)) this else null
// 기존 코드
if(someObject != null && someObject.status) {
doThis()
}
// 세이프 콜로 개선한 코드
if(someObject?.status == true){
doThis()
}
// takeIf를 사용한 코드 (람다식이 true일 때만 doThis 실행)
someObject?.takeIf { it.status }?.apply { doThis() }
val input = "Kotlin"
val keyword = "in"
// 입력 문자열에 키워드가 있으면 그 인덱스를 반환, 없으면 에러 문구 출력
input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")
// takeUnless를 사용한다면
input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")
코드의 실행 시간을 측정하고 싶을 때는, kotlin.system 패키지에 있는 measureTimeMillis(), measureNanoTime() 함수를 이용할 수 있다.
val executionTime = measureTimeMillis {
// 측정할 작업 코드
}
println("Execution Time: $executionTime ms")
자바의 java.util.Random을 이용할 수도 있지만 이는 JVM에만 특화된 난수를 생성하기 때문에, 코틀린에서는 멀티 플랫폼에서도 사용 가능한 kotlin.random.Random을 제공한다.
package chap06.section1
import kotlin.random.Random
fun rand(from: Int, to: Int): Int{
return Random.nextInt(to - from) + from
}
fun main() {
// 0~4 사이의 정수 출력 (5는 제외)
val num = Random.nextInt(5)
println(num)
// 5~9 사이의 정수 출력 (10은 제외)
println(rand(5, 10))
}