본문 바로가기
대외활동/Naver Boostcourse

[부스트코스] 코틀린 프로그래밍 기본 1/2(함수편) - 코틀린과 표준함수

by 드인 2021. 2. 12.

코틀린 프로그래밍 기본 1/2 (함수편)

 


 

0. 오리엔테이션

1. 코틀린의 기본의 기본을 읽혀요!

- 코틀린이란 무엇일까?

 - 변수와 자료형, 연산자

2. 함수형 프로그래밍 이란?

 - 마법의 요술상자, 함수의 기본

 - 요술상자, 함수 가지고 놀기

3. 프로그램 흐름의 제어

 - 프로그램의 흐름을 제어해보자!

4. 코틀린의 표준함수 활용하기

 - 코틀린과 표준함수

5. 강좌 마무리 프로젝트

▶ 깃허브 코드 : github.com/0525hhgus/Kotlin-study

 

0525hhgus/Kotlin-study

Kotlin-study. Contribute to 0525hhgus/Kotlin-study development by creating an account on GitHub.

github.com


[ 표준함수]


1. 람다식과 고차함수 요약

1) 람다식

▶ 구성

- {  매개변수[ , ... ] -> 람다식 본문 }

 

▶ 사용 예

val sum: (Int, Int) -> Int = { x, y -> x + y } 
val mul = { x: Int, y: Int -> x * y }
val add: (Int) -> Int = { it + 1 }

 

2) 식이 여러개인 경우

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

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

 

3) 고차 함수

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

fun high(name: String, body: (Int)->Int): Int {
    println("name: $name")
    val x = 0
    return body(x)
}

 

4) 고차 함수와 다양한 람다식 인자

// 함수를 이용한 람다식
val result = high("Sean", {  x -> inc(x + 3) })

// 소괄호 바깥으로 빼내고 생략
val result2 = high("Sean") { inc(it + 3) }

// 매개변수 없이 함수의 이름만 사용할 때
val result3 = high("Kim", ::inc) // 일반 함수 -> 람다식

// 람다식 자체를 넘겨 준 형태
val result4 = high("Sean") { x -> x + 3 }

// 매개변수가 한 개인 경우 생략
val result5 = high("Sean") { it + 3 }

// 만일 일반 매개변수가 없고 람다식이 유일한 인자라면 다음과 같이 표현 가능
val result6 = highNoArg { it + 3 }

 

2. 클로저(closure)

1) 클로저

람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념

▶ 람다식 안에 있는 외부 변수는 값을 유지하기 위해 람다가 포획(capture)한 변수

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에는 반환값이 없음
        add(a, b)
    }
}

 

2) 함수의 매개변수 접근

▶ 함수 자체를 같이 포획해 해당 매개변수에 접근함

// 길이가 일치하는 이름만 반환
fun filteredNames(length: Int) {
    val names = arrayListOf("Kim", "Hong", "Go", "Hwang", "Jeon")
    val filterResult = names.filter {
        it.length == length // 바깥의 length에 접근 
    }
    println(filterResult)
}
...
filteredNames(4)

 

 

🤔 생각해보세요.

 클로저 사용된 사례들을 검색하고 그 예를 남겨보세요!

Q. 클로저 사용된 사례로 무엇이 있을까요?

A. 클로저는 접근 권한 제어, 부분 적용 함수, 커링 함수에 사용될 수 있습니다.

 

3. let()

1) 코틀린의 표준 라이브러리

▶ 람다식을 사용하는 코틀린의 표준 라이브러리에서 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 : 형식 매개변수

 

2) let()의 동작정의

▶ 함수를 호출하는 객체 T를 이어지는 block의 인자로 넘기고 block의 결과값 R을 반환

// 표준 함수의 정의
public inline fun <T, R> T.let(block: (T) -> R): R { ... return block(this) }

- 매개변수 block은 T를 매개 변수로 받아 R을 반환

- let() 함수 역시 R을 반환

- 본문의 this는 객체 T를 가리키는데 람다식 결과 부분을 그대로 반환한다는 뜻

- 다른 메서드를 실행하거나 연산을 수행해야 하는 경우 사용

 

▶ let 함수 사용 예시

fun main() {
    val score: Int? = 32
    // var score = null
    
    // 일반적인 null 검사
    fun checkScore() {
    	if (score != null) {
        	println("Score: $score")
        }
    }
    
    // let을 사용해 null 검사를 제거
    fun checkScoreLet() {
        score?.let { println("Score: $it") } // ①
        val str = score.let { it.toString() } // ②
        println(str)
    }
    
    checkScore()
    checkScoreLet()
}

- let()을 쓰는 이유 : 앞쪽의 형태가 복잡할 때, it으로 받아서 단축할 수 있음

 

3) let 함수의 체이닝

fun main() {
	var a = 1
	var b = 2

	a = a.let { it + 2 }.let {
    	println("a = $a")
    	val i = it + b
    	i  // 마지막 식 반환
	}
	println(a) //5
}

 

 

4) let의 중첩 사용

var x = "Kotlin!"
	x.let { outer ->
    	outer.let { inner ->
        	print("Inner is $inner and outer is $outer") // 이때는 it을 사용하지 않고 명시적 이름을 사용
        }
    }

 

▶ 반환값은 바깥쪽의 람다식에만 적용

var x = "Kotlin!"
	x.let { outer ->
    	outer.let { inner ->
        	print("Inner is $inner and outer is $outer")
           	"Inner String" // 이것은 반환되지 않음
        }
        "Outer String" // 이 문자열이 반환되어 x에 할당
    }

 

5) 커스텀 뷰에서 let의 활용

▶ 안드로이드의 커스텀 뷰에서 Padding 값으르 지정

val padding = TypeValue.applyDimension(
	TypeValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics).toInt()
 
setPadding(padding, 0, padding, 0) // 왼쪽, 오른쪽 padding 설정
TypeValue.applyDimension(
	TypeValue.COMPLEX_UNIT_DIP, 16f, 
	resources.displayMetrics).toInt().let { padding -> 
	setPadding(padding, 0, padding, 0) // 계산된 값을 padding 이라는 이름의 인자로 받음
}
TypeValue.applyDimension(
	TypeValue.COMPLEX_UNIT_DIP, 16f, 
	resources.displayMetrics).toInt().let {
	setPadding(it, 0, it, 0) // padding 대신 it 사용
}

 

 

6) null 가능성 있는 객체에서 let를 활용

▶ null 검사

- let을 세이프 콜(?.)과 함께 사용하면 if (null != obj)와 같은 null 검사 부분을 대체

var obj: String? // null일 수 있는 변수 obj
...
if (null != obj) { // obj가 null이 아닐 경우 작업 수행 (기존 방식)
    Toast.makeText(applicationContext, obj, Toast.LENGTH_LONG).show()
}
obj?.let { // obj가 null이 아닐 경우 작업 수행 (Safe calls + let 사용)
    Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show()
}

 

7) 엘비스 연산자와 함꼐 사용

▶ else문이 포함된 문장 대체

var firstName: String?
var lastName: String
...
// if 문을 사용한 경우
if (null != firstName) {
	print("$firstName $lastName")
} else {
	print("$lastName")
}
// let을 사용한 경우
firstName?.let { print("$it $lastName") } ?: print("$lastName")

 

 

🤔 생각해보세요.

 앞서 공부한 세이프콜(.?)이 다시 나왔습니다. let과 세이프콜을 사용하면 null 검사 부분을 단순화 할 수 있었죠. let() 함수를 어디에 응용할 수 있을지 생각해보세요!

Q. let() 함수를 어디에 응용할 수 있을까요?

A.

1. 지정된 값이 null 이 아닌 경우에 코드를 실행

2. 단일 지역 변수의 범위를 제한하여 가독성 향상

 

- Reference

kotlinlang.org/docs/scope-functions.html#let

 

Scope functions - Help | Kotlin

 

kotlinlang.org

 

4. also()

1) also() 동작

also()는 함수를 호출하는 객체 T를 이어지는 block에 전달하고 객체 T 자체를 반환

// 표준 함수의 정의 let과 비교
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 }

 

▶ also()는 블록 안의 코드 수행 결과와 상관없이 T인 바로 객체 this를 반환

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

 

2) let과 also 비교 예제

fun main() {
	data class Person(var name: String, var skills: String)
    var person = Person("Kildong", "Kotlin")
    
    val a = person.let {
    	it.skills = "Android"
        "success" // 마지막 문장을 결과로 반환
    }
    
    println(person)
    println("a: $a") // String
    
    var b = person.also {
    	it.skills = "Java"
        "success" // 마지막 문장은 사용되지 않음
    }
    println(person)
    println("b: $b") // Person의 객체 b
}

 

 

▶ 실습: let과 also 사용해보기

package chap06.section4

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("Kildong", "Kotlin")

    val a = person.let {
        it.skills = "Java"
        "success"
    }

    println("a: $a")
    println(person)

    var b = person.also {
        it.skills = "Python"
        "success"
    }

    println("b: $b")
    println(person)
}

 

3) 특정 단위의 동작 분리 - 디렉터리 생성

▶ 디렉터리 생성 활용

// 기존의 디렉터리 생성 함수
fun makeDir(path: String): File {
	val result = File(path)
    result.mkdirs()
    return result
}
// let과 also를 통한 개선된 함수
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

 

- let은 식의 결과를 반환하고 그 결과를 다시 also를 통해 넘겨짐

- 이때 중간 결과가 아니라 넘어온 결과만 반환

 

🤔 생각해보세요.

 also를 활용할 수 있는 사례들을 좀 더 찾아서 서로 공유해봅시다.

Q. also를 활용할 수 있는 사례로 무엇이 있을까요?

A. 컨텍스트 객체를 사용하는 작업에서 사용할 수 있습니다.

EX) 안드로이드의 startActivity

 

- Reference

kotlinlang.org/docs/scope-functions.html#also

 

Scope functions - Help | Kotlin

 

kotlinlang.org

 

5. apply()

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

- T.()와 같은 표현에서 람다식이 확장 함수로서 처리

 

▶ apply 사용 예제

fun main() {
    data class Person(var name: String, var skills : String)
    var person = Person("Kildong", "Kotlin")

    // 여기서 this는 person 객체를 가리킴
    person.apply { this.skills = "Swift" }
    println(person)

    val retrunObj = person.apply { 
        name = "Sean" // ① this는 생략할 수 있음
        skills = "Java" // this 없이 객체의 멤버에 여러 번 접근
    }
    println(person)
    println(retrunObj)
}

- also()는 it으로 받고 생략할 수 없음

- apply()는 this로 받고 생략할 수 있음

 

2) 안드로이드 레이아웃 초기화 사례

▶ 레이아웃 초기화 시 기존 코드

// 기존 코드
val param = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT)
param.gravity = Gravity.CENTER_HORIZONTAL
param.weight = 1f
param.topMargin = 100
param.bottomMargin = 100
val param = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
	gravity = Gravity.CENTER_HORIZONTAL
	weight = 1f // param을 사용하지 않고 직접 값을 지정할 수 있다.
	topMargin = 100
	bottomMargin = 100
}

 

3) 디렉터리 생성 시 apply 적용

// 기존 코드
fun makeDir(path: String): File {
    val result = File(path)
    result.mkdirs()
    return result
}

 

File(path).apply { mkdirs() }

 

🤔 생각해보세요.

 마찬가지로 apply( )를 사용하는 사례들을 검색해보고 서로 공유해봅시다.

Q. apply( )를 사용하는 사례로 무엇이 있을까요?

A. 객체 초기화 작업을 수행하는 경우에 효과적으로 사용할 수 있습니다.

 

6. run()

1) run() 동작

▶ run() 함수는 인자가 없는 익명 함수처럼 동작하는 형태와 객체에서 호출하는 형태 두 가지로 사용

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

 

▶ run 사용의 예

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

val a = 10
skills = run {
    val level = "Kotlin Level:" + a
    level // 마지막 표현식이 반환됨
}

println(skills) // Kotlin Level:10

 

2) apply와 run 비교

fun main() {
    // apply와 run 비교
    data class Person(var name: String, var skills : String)
    var person = Person("Kildong", "Kotlin")

    val retrunObj = person.apply { 
        name = "Sean"
        skills = "Java"
        "success" // 사용되지 않음
    }
    println(person)
    println("retrunObj: $retrunObj")
    
    val retrunObj2 = person.run { 
        name = "Dooly" // this로 받고 생략 가능
        skills = "C#"
        "success"
    }
    println(person)
    println("retrunObj2: $retrunObj2")
}

 

🤔 생각해보세요.

 run이 사용되는 사례를 검색하고 서로 공유해 봅시다.

Q. run이 사용되는 사례로 무엇이 있을까요?

A. 매개 변수로 받은 명시적 수신객체를 암시적 수신 객체로 변환하려는 경우에 사용할 수 있습니다.

 

7. with()

1) with() 동작

▶ 인자로 받는 객체를 이어지는 block의 receiver로 전달하며 결과값을 반환

- run()함수와 기능이 거의 동일한데, run의 경우 receiver가 없지만 with()에서는 receicer로 전달할 객체를 처리

// 표준 함수의 정의
public inline fun <T, R> with(receiver: T, block: T.() -> R): R  = receiver.block()

- with는 세이프 콜(?.)은 지원하지 않기 때문에 다음과 같이 let과 같이 사용

supportActionBar?.let {
    with(it) {
        setDisplayHomeAsUpEnabled(true)
        setHomeAsUpIndicator(R.drawable.ic_clear_white)    
    }
}

 

▶ with 사용의 예

try {
    예외 발생 가능성 있는 문장
} catch (e: 예외처리 클래스명) {
    예외를 처리하기 위한 문장
} finally {
   반드시 실행되어야 하는 문장
}

 

2) let과 with의 표현 병합

▶ run과 동일

supportActionBar?.run {
    setDisplayHomeAsUpEnabled(true)
    setHomeAsUpIndicator(R.drawable.ic_clear_white)
}

 

▶ 실습: with 함수 사용해보기

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"
        email = "kildong@example.com"
    }
    println(user)
    println("result: $result")
}

- 마지막 표현식 X → Unit 반환

 

🤔 생각해보세요.

 with( )에 대해서 살펴보았는데 null을 처리할 때는 run( )이 더 적합하다는 것을 알았습니다. with는 receiver를 일반 인자로 받아서 처리하기 때문에 확장 함수 형태가 아닌 단독으로 처리되죠. with()의 사용 사례를 수집해봅시다.

Q. with()의 사용 사례로 무엇이 있을까요?

A. 인자를 객체로 받아서 사용하는 것이 효율적일 경우에 사용합니다.

 

8. use()

1) use() 동작

▶ use()를 사용하면 객체를 사용한 후 close() 등을 자동적으로 호출해 닫아줌

// 표준 함수의 정의
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R 또는
public inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R

▶ T의 제한된 자료형을 보면 Closeable?로 block은 닫힐 수 있는 객체를 지정해야 함

▶ Java 7이후는 AutoCloseable?로 사용됨

 

 

2) 비 최적화된 파일 읽기 자바 코드의 예

private String readFirstLine() throw FileNotFoundException {
    BufferReader 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;
}

 

3) 코틀린으로 최적화된 코드

private fun readFirstLine(): String {
	BufferedReader(FileReader("test.file")).use { return it.readLine() }
}

 

4) use 구현부

...
public inline fun <T: Closeable?, R> T.use(block: (T) -> R): R {
	var exception: Throwalbe? = null
    try {
    	return block(this)
    } catch (e: Throwable) {
    	exception = e
        throw e
    } finally {
    	when {
        	apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
            	try {
                	close()
                 } catch (closeException: Throwable) {
                 	// cause.addSuppressed(closeException) // ignored here
                 }
...

 

5) 파일 쓰기의 실습

▶ 파일 닫기에 대한 처리

package chap10.section1

import java.io.*

fun main() {
	PrintWriter(FileOutputStream("d:\\test\\output.txt")).use {
    	it.println("hello")
    }
}

 

🤔 생각해보세요.

 역시 use의 사용 사례들을 검색해 보고 서로 공유해 봅시다.

Q. use의 사용 사례로 무엇이 있을까요?

A. 안드로이드 파일 입출력 과정에 사용되는 cursor에서 사용할 수 있습니다.

 

9. 기타 표준 함수

1) takeIf()와 takeUnless()

▶ takeIf() 함수는 람다식이 true이면 객체 T를 반환하고 그렇지 않은 경우 null을 반환

▶ takeUnless() 함수는 람다식이 false이면 객체 T를 반환하고 그렇지 않은 경우 null을 반환

// 표준 함수의 정의
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T?
  = if (predicate(this)) this else null

 

▶ takeIf() 사용 예제

// 기존 코드
if (someObject != null && someObject.status) {
	doThis()
}
// 개선한 코드
if (someObject?.status == true) {
	doThis()
}
// takeIf를 사용해 개선한 코드
someObject?.takeIf { it.status }?.apply { doThis() }

 

2) takeIf()와 엘비스 연산자

val input = "Kotlin"
val keyword = "in"

// 입력 문자열에 키워드가 있으면 인덱스를 반환하는 함수를 takeIf를 사용하여 구현
input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")

// takeUnless를 사용하여 구현
input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")

 

3) 시간의 측정

▶ kotlin.system 패키지에 있는 두 개의 측정 함수

- measureTimeMillis()와 measureNanoTime()

 

▶ 선언부

// 코틀린 system 패키지의 Timing.kt 파일
public inline fun measureTimeMillis(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}

public inline fun measureNanoTime(block: () -> Unit): Long {
    val start = System.nanoTime()
    block()
    return System.nanoTime() - start
}

 

▶ 시간 측정 사용 방법

val executionTime = measureTimeMillis {
    // 측정할 작업 코드
}
println("Execution Time = $executionTime ms")

 

4) 난수 생성

▶ 코틀린의 난수 생성

- 자바의 java.util.Random을 이용할 수도 있었지만 JVM에만 특화된 난수를 생성하기 때문에 코틀린에서는 멀티플랫폼에서도 사용 가능한 kotlin.random.Random를 제공

 

▶ 0부터 21사이의 난수를 제공하는 예제

import kotlin.random.Random
...
val number = Random.nextInt(21)  // 숫자는 난수 발생 범위
println(number)

 

🤔 생각해보세요.

 기타 표준 함수에 대해서 살펴보았습니다. 역시 각 함수의 사용 사례를 검색해보고 서로 공유해봅시다.

Q. 기타 표준 함수의 사용 사례로 무엇이 있을까요?

A. takeif()와 takeunless()는 이전에 소개된 let(), also(), run()와 결합하여 사용할 수 있습니다.

 

- reference

kotlinlang.org/docs/scope-functions.html#takeif-and-takeunless

 

Scope functions - Help | Kotlin

 

kotlinlang.org


서포터즈 네임택


감사합니다!