코틀린

Do it! 코틀린 프로그래밍, 10-1 코틀린 표준 함수

조요피 2021. 9. 28. 16:24

람다식과 고차 함수 복습

람다식

// val 변수 이름: 자료형 선언 = { 매개변수[,...] -> 람다식 본문 }
val sum: (Int, Int) -> Int = { x, y -> x + y }
val mul = { x: Int, y: Int -> x * y }

매개변수가 1개인 경우, 매개변수를 생략하고 it으로 표기할 수 있다.

val add: (Int) -> Int = {it + 1}

만일 추론된 반환 자료형이 Unit이 아닌 경우에는 본문의 마지막 표현식이 반환값으로 처리됨.

    val isPositive: (Int) -> Boolean = {
        val isPositive = it > 0
        isPositive // 마지막 표현식이 반환됨
    }

    val isPositiveLabel: (Int) -> Boolean = number@{
        val isPositive = it > 0
        return@number isPositive // 라벨을 사용해 반환됨
    }

고차 함수

고차 함수는 함수의 매개변수로 함수를 받거나 함수 자체를 반환할 수 있는 함수.

func 함수의 두번째 매개변수 param은 람다식 함수를 받을 수 있다.

아래는 다양한 형태의 고차 함수 표현법이다.

fun func1(x: Int): Int = x * x

fun func2(y: Int, param: (Int) -> Int): Int {
    return param(y)
}

fun main() {
    val result1 = func2(3, { x -> func1(x + 2) }) // 함수를 이용한 람다식
    val result2 = func2(3) { func1(it + 2) } // 위와 동일
    val result3 = func2(3, ::func1) // 함수의 이름만 사용
    val result4 = func2(3) { x -> x + 2 } // 람다식 사용
    val result5 = func2(3) { it + 2 } // 매개변수가 1개인 경우 생략 
}

클로저

람다식을 사용하다 보면 내부 함수에서 외부 변수를 호출하고 싶을 때가 있다.

클로저(Closure)란 람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념을 말한다.

이때 람다식 안에 있는 외부 변수는 값을 유지하기 위해 람다식이 포획(Capture)한 변수라고 부른다.

기본적으로 함수 안에 정의된 변수는 지역 변수로 스택에 저장되어 있다가 함수가 끝나면 같이 사라진다.

하지만 클로저 개념에서는 포획한 변수는 참조가 유지되어 함수가 종료되어도 사라지지 않고 함수의 변수에 접근하거나 수정할 수 있게 해준다.

클로저의 조건은 다음과 같다.

  • final 변수를 포획한 경우 변수 값을 람다식과 함께 저장.
  • final이 아닌 변수를 포획한 경우 변수를 특정 래퍼(wrapper)로 감싸서 나중에 변경하거나 읽을 수 있게 한다. 이때 패러에 대한 참조를 람다식과 함께 저장.

자바에서는 외부의 변수를 포획할 때 final만 포획할 수 있다.

따라서 코틀린에서 final이 아닌 변수를 사용하면 내부적으로 변환된 자바 코드에서 배열이나 클래스를 만들고 final로 지정해 사용된다.

fun main() {
    val calc = Calc()
    var result = 0 // 외부 변수
    calc.addNum(2, 3) { x, y -> result = x + y } // 클로저
    println(result) // 값을 유지하여 5 출력
}

class Calc {
    fun addNum(a: Int, b: Int, add: (Int, Int) -> Unit) {
        add(a, b)
    }
}
 // 5

result는 var로 선언됐다. Calc 클래스의 addNum()이 호출되면 result는 자신의 유효 범위를 벗어나 삭제되어야 하지만 클로저의 개념에 의해 독립된 복사본을 가진다. (이해 안됨.., javascript의 클로저 개념이랑 조금 다른 듯?)

코틀린의 표준 라이브러리

확장 함수의 람다식 접근 방법

함수 이름 람다식의 접근 방법 반환 방법
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() 함수 활용하기

fun main() {
//    val score: Int? = 32
    var score = null

    // 일반적인 null 검사
    fun checkScore(){
        if(score != null) {
            println(score)
        }
    }

    fun checkScoreLet(){
        score?.let { println("$it") } // score가 null이라면 수행되지 않음
        val str = score.let { it.toString() } // 세이프콜(?.)을 사용하지 않아도 str은 String?으로 추론됨
        println(str)
    }

    checkScore()
    checkScoreLet()
}

 // null
 //
  1. 변수를 한번만 사용한다면 let 함수를 사용하여 람다식 안에서 처리할 수 있음.
  2. if( obj != null) 같은 null 체크를 대신할 수 있음.
    1. if-else 구문을 변경하려면 다음과 같이 변경할 수 있음. 엘비스 연산자(?:) 사용
    2. firstName.let { println("$it $lastName") } ?: println("$lastName")
  3. let 함수는 체이닝 가능!

also() 함수 활용하기

let() 함수와 기능적으로 동일하지만 반환하는 값이 다르다.

let() 함수는 마지막으로 수행된 코드 블록의 결과를 반환하고

also() 함수는 블록 안의 코드 수행 결과와 상관없이 T인 객체 this를 반환한다.

var m = 1
m = m.also { it + 3 }
println(m) // 원본값 1

let()와 also()함수의 활용

// 디렉터리 생성 함수
fun makeDir(path: String): File {
	val result = File(path)
    result.mkdirs()
    return result
}

// also()와 let()을 사용하여 개선
fun makeDir(path: String) = path.let { File(it) }.also{ it.mkdirs() }

apply() 함수 활용하기

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("young", "java")
    person.apply { this.skills = "kotlin" } // 여기서 this는 person 객체를 가리킴
    println(person)

    val person2 = person.apply {
        name = "jo" // this 생략 가능
        skills = "c++" // this 없이 객체의 멤버에 여러번 접근
    }

    println(person)
    println(person2)
}

// Person(name=young, skills=kotlin)
// Person(name=jo, skills=c++)
// Person(name=jo, skills=c++)

also() 함수는 it으로 받고 생략할 수 없지만 apply() 함수는 this로 받고 생략 가능.

// 디렉터리 생성 함수
fun makeDir(path: String): File {
	val result = File(path)
    result.mkdirs()
    return result
}

// apply()
File(path).apply{ mkdirs() }

File(path)에 의해 생성된 결과를 람다식에서 this로 받는다.

run() 함수 활용하기

var skills = "java"
println(skills) // java

val a = 10
skills = run {
	val level = "java level: " + a
    level // 마지막 표현식이 반환됨
}
println(skills) // java level: 10

apply() 함수는 this에 해당하는 객체를 반환하는 반면

run() 함수는 마지막 표현식을 반환한다.

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("young", "java")
    val apply = person.apply {
        name = "jo"
        skills = "c"
        "hi"
    }
    println(person)
    println(apply)

    val run = person.run {
        name = "k"
        skills = "kill"
        "bye"
    }
    println(person)
    println(run)
}

// Person(name=jo, skills=c)
//Person(name=jo, skills=c)
//Person(name=k, skills=kill)
//bye

with() 함수 활용하기

fun main() {
    val b = true
//    val b = false

    data class Person(var name: String? = null, var skills: String? = null)
    var person = if(b) Person("young", "java") else null

    // 아래와 같이 with을 사용할 수 있지만
    person?.let {
        with(it){
            name = "jo"
            skills = "c++"
            println("let")
        }
    }
    println(person)

    // 사실 run을 사용하는 것이 더 낫다
    person?.run {
        name = "k"
        skills = "kill"
        println("run")
    }
    println(person)

}

//let
//Person(name=jo, skills=c++)
//run
//Person(name=k, skills=kill)

with() 함수는 마지막 표현식을 반환할 수 있다.

use() 함수 활용하기

use() 함수를 사용하면 객체를 사용한 후 close() 함수를 자동적으로 호출해 닫아 줄 수 있다.

대표적으로 파일 객체를 사용한 뒤에 닫을 때 사용한다.

기타 함수의 활용

takeIf() 함수와 takeUnless() 함수

takeIf() 함수는 람다식이 true이면 결과를 반환

takeUnless() 함수는 람다식이 false이면 결과를 반환

// 기존
    if (obj != null && obj.status) doThis()

// 개선
    if (obj?.status == true) doThis()

// takeIf() 개선
    obj?.takeIf { it.status }?.apply { doThis() }

시간 측정

import kotlin.system.measureNanoTime
import kotlin.system.measureTimeMillis

fun main() {
    fun forloop() {
        var y: Int = 0
        for (x in 1..100000000) {
            y += x
        }
    }

    println(measureNanoTime { forloop() })
    println(measureTimeMillis { forloop() })
}

//31993700
//30

난수 생성

import kotlin.random.Random

fun main() {
    for (x in 1..10) println(Random.nextInt())
}

//-756173766
//907357534
//1426995272
//-15472866
//2012265281
//-1373295774
//44480151
//-432598844
//-1942765195
//1028166197