[Kotlin] 6장. 코틀린의 표준함수

leeeha·2022년 7월 14일
0

코틀린

목록 보기
15/28
post-thumbnail

출처: https://www.boostcourse.org/mo132/lecture/59989?isDesc=false

람다식과 고차함수 복습

람다식과 고차함수에 대한 자세한 내용은 이전 포스트 참고하기!

https://velog.io/@jxlhe46/Kotlin-4-1
https://velog.io/@jxlhe46/Kotlin-5-2

람다식 (Lambda Expression)

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 // 람다식에서 리턴문을 사용하려면 라벨을 붙여야 함.  
}

고차 함수 (High-Order Function)

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

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


널 포획해야겠어! Closure!

  • 람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념
  • 람다식 안에 있는 외부 변수는 값을 유지하기 위해 람다가 포획(capture)한 변수라고 부른다.

코틀린은 실행 시점에서 람다식의 모든 참조가 포함된 닫힌(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.letitblock 결과
    T.alsoitT caller(it)
    T.applythisT caller(this)
    T.run 또는 runthisblock 결과
    withthisUnit
  • T는 형식 매개변수이며, 어떤 타입으로도 사용될 수 있다는 걸 의미한다. 위의 표에 있는 표준 함수들에 대해 차근차근 알아보자!


너가 한 일은 결과와 함께 반환해! let()

let()은 함수를 호출하는 객체 T를 이어지는 block의 인자로 넘기고, block의 결과값 R을 반환한다.

public inline fun <T, R> T.let(block: (T) -> R): R { ... return block(this) }

  • T는 우리가 사용할 요소, R은 반환할 요소
  • 매개변수 block은 T를 매개변수로 받아서 R을 반환한다.
  • let() 함수 역시 R을 반환한다.
  • this는 객체 T를 가리키는데, 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

let 함수의 체이닝 (chaining)

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

let의 중첩 사용

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() 활용하기

null 가능성이 있는 객체에서 let() 활용하기

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()

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 

let과 also 비교하기

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()

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 }

  • T.()와 같은 표현에서 람다식이 확장 함수로서 처리가 된다는 걸 알 수 있다.
  • 특정 객체를 생성하면서 함께 호출해야 하는 초기화 코드가 있는 경우 사용할 수 있다.
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로 받아서 블록 내부로 전달한다.


그냥 실행하고 결과를 반환하는 run()

public inline fun run(block: () -> R): R = return block()
public inline fun <T, R> T.run(block: T.() -> R): R = return block()

  • run() 함수는 인자가 없는 익명 함수 형태로 단독 사용하거나, 확장 함수로 호출하는 형태 두 가지로 사용할 수 있다.
  • 객체 T 자체가 아니라 람다식 block의 결과 R을 반환한다.
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


단독으로 실행되어 결과를 반환하는 with()

public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

  • with() 함수는 인자로 받은 객체를 이어지는 block의 receiver로 전달하며, 그 결과를 반환한다.
  • run() 함수와 기능이 거의 동일한데, run()의 경우 receiver가 없지만 with()는 소괄호 사이에 리시버로 객체를 전달 받는다.
  • with()는 리시버를 일반 인자로 받아서 처리하기 때문에 확장 함수 형태가 아니라, 단독으로 처리된다.
  • with()는 세이프 콜(?.)을 지원하지 않기 때문에, 다음과 같이 let을 같이 사용한다.
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.letitblock 결과
T.alsoitT caller(it)
T.applythisT caller(this)
T.run 또는 runthisblock 결과
withthisUnit

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()

  • run() 함수는 인자가 없는 익명 함수 형태로 단독 사용하거나, 확장 함수로 호출하는 형태 두 가지로 사용할 수 있다.
  • 객체 T 자체가 아니라 람다식 block의 결과 R을 반환한다.

public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

  • with() 함수는 인자로 받은 객체를 이어지는 block의 receiver로 전달하며, 그 결과를 반환한다.
  • run() 함수와 기능이 거의 동일한데, run()의 경우 receiver가 없지만 with()는 소괄호 사이에 리시버로 객체를 전달 받는다.

이렇게만 보면 복잡해보이지만, 위에서 설명했던 예시들처럼 실제로 사용을 많이 많이 해보면서 코틀린의 표준함수에 익숙해지도록 하자! 이 함수들은 우리를 괴롭히는 존재가 아니라, 우리가 개발을 더 편하게 할 수 있도록 만들어진 고마운 존재들이다! 🤗


사용했으면 닫아! use()

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

  • T의 제한된 자료형이 Closable? 이므로 block은 닫힐 수 있는 객체를 지정해야 한다.
  • Java 7 이후는 AutoClosable?로 사용된다.

파일 관련 처리 예시

기존의 자바 코드

 	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")
    }
}

자주 사용되는 기타 표준함수

takeIf()와 takeUnless()의 활용

  • takeIf() 함수는 람다식이 true면 객체 T를 반환하고, 그렇지 않으면 null을 반환
  • takeUnless() 함수는 람다식이 false면 객체 T를 반환하고, 그렇지 않으면 null을 반환

// 표준 함수의 정의
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))
}                             
profile
꾸준히!

0개의 댓글