반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[코틀린 완벽 가이드] 3장 : 함수 정의하기 본문

Kotlin/코틀린 완벽 가이드

[코틀린 완벽 가이드] 3장 : 함수 정의하기

어리둥절범고래 2022. 9. 21. 16:08
반응형

1. 함수


1. 코틀린 함수의 구조

 

코틀린 함수는 어떤 입력(파라미터(parameter))를 받아 자신을 호출한 코드 쪽에 출력값(결괏값)을 반환(return)할 수 있는 재사용 가능한 코드 블록이다.

 

import kotlin.math.PI

fun circleArea(radius: Double): Double {
	return PI*radius*radius
}

fun main() {
    print("Enter radius: ")
    val radius = readLine()!!.toDouble()
    println("Circle area: ${circleArea(radius)}")
}

 

circleArea 함수를 구성하는 요소를 살펴보면,

 

  • fun 키워드 뒤의 함수 이름을 변수 이름과 마찬가지로 아무 식별자나 쓸 수 있다.
  • 다음에는 괄호로 둘러싸여 있는 콤마(,)로 분리한 파라미터 목록이다. 파라미터는 컴파일러가 함수 정의에서 파라미터 타입을 추론하지 못하기 때문에 파라미터 타입은 변수와 달리 항상 타입을 지정해야 한다. 함수가 파라미터를 받지 않는다고 해도 파라미터를 감싸는 괄호는 꼭 있어야 한다.
  • 반환 타입은 함수를 호출한 쪽에 돌려줄 반환값의 타입이다. 반환값이 있다면 함수의 결괏값을 return문으로 지정한다. return문은 함수 실행을 끝내고 호출한 쪽에 돌려준다. (retrun문 다음에 위치한 코드는 실행되지 않는다.) 하지만 경우에 따라 반환 타입을 생략할 수 있는데, 유닛(unit)타입을 반환하는 경우다. 함수가 의미 있는 반환값을 돌려주지 않는다는 뜻이다. 코틀린은 함수 정의에서 반환값 타입을 지정하지 않으면 Unit 타입을 반환한다고 가정한다.
  • 함수 본문은 {}로 감싼 블록(문)이며, 함수의 구현을 기술한다.

 

블록({}) 안에는 지역 변수나 지역 함수 정의가 들어갈 수 있다. 이런 선언의 영역(scope)은 해당 함수/변수가 선언된 블록 내부로 제한된다.

 

파라미터 정의는 암시적으로 함수가 호출될 때 자동으로 인자 값으로 초기화되는 지역 변수로 취급된다.

코틀린의 함수 파라미터는 무조건 불변이며 함수 본문에서 파라미터 값을 변경하면 컴파일 오류가 발생한다.

이렇게 불변 값으로 고정해 놓기 때문에 파라미터 앞에 var이나 val을 표시할 수 없다.

 

val increment(n: Int): Int {
	return n++ // Error: can't change immutable variable
}

 

코틀린은 값에 의한 호출(call-by-value) 의미론을 사용한다. 

이 말은 파라미터 값에 호출하는 쪽의 인자를 복사한다는 뜻으로, 호출 인자로 전달된 변수를 변경해도 호출된 함수 내부의 파라미터 값에는 영향이 없다. 하지만 파라미터가 참조형 타입이라면 호출한 쪽의 데이터는 그대로 남아있고 이 데이터에 대한 참조만 복사된다.

따라서 파라미터 자체는 함수 내부에서 바뀔 수 없지만, 일반적으로 파라미터가 가리키는 데이터는 바뀔 수 있다.

 

fun increment(a: IntArray): Int {
    return a[0]++
}

fun main() {
    val a = intArrayOf(1, 2, 3)
    println(increment(a)) // 2
    println(a.contentToString()) // [2, 2, 3]
}

 


2. 위치 기반 인자와 이름 붙은 인자

 

기본적으로 함수 호출 인자가 순서대로 파라미터에 전달된다. 이런 방식을 위치 기반 인자(positional argument)라고 한다.

코틀린은 이름 붙은 인자(named argument)라고 불리는 방식도 제공한다.

이름 붙은 인자는 위치가 아니라 파라미터의 이름을 명시함으로써 인자를 전달하는 방식이다.

한 호출 안에서 위치 기반 인자와 이름 붙은 인자를 함께 사용할 수도 있다. 다만 이 경우에는 원래 인자가 들어가야할 위치에 이름 붙은 인자를 지정해야 정상 처리된다.

 

fun swap(s: String, from: Int, to: Int): String {
    val chars = s.toCharArray() // 배열로 변환
    // 배열 원소 교환하기
    val temp = chars[from]
    chars[from] = chars[to]
    chars[to] = temp
    return chars.concatToString() // 문자열로 다시 변환
}

fun main() {
    println(swap("Hello", 1, 2)) // Hlelo
    println(swap("Hello", from = 1, to = 2)) // Hlelo
    println(swap("Hello", to = 3, from = 0)) // lelHo
    println(swap("Hello", 1, to = 3)) // Hlleo
    println(swap(from = 1, s = "Hello", to = 2)) // Hlelo
    // 위치 기반 인자와 이름 붙은 인자를 혼용한 경우
    println(swap(s = "Hello", 1, 2)) // 1.4 이전에서는 컴파일 오류, 1.4부터는 정상
    println(swap(s = "Hello", 1, to = 2)) // error: an argument is already passed for ths parameter
                                          // error: no value passed for parameter 'to'
}

 


3. 오버로딩과 디폴트 값

 

오버로딩은 같은 함수를 여럿 작성할 수 있다는 뜻이다. 다만 함수의 파라미터 타입이 모두 달라야 한다.

 

fun mul(a: Int, b: Int) = a*b            // 1
fun mul(a: Int, b: Int, c: Int) = a*b*c  // 2
fun mul(s: String, n: Int) = s.repeat(n) // 3
fun mul(o: Any, n: Int) = Array(n) { o } // 4

 

컴파일러는 주어진 호출 식에 대해 실제 함수를 결정할 때 다음과 같은 규칙을 따른다.

 

  • 파라미터의 개수와 타입을 기준으로 찾는다.
  • 덜 구체적인 함수를 제외한다. (후보 목록에서 함수의 파라미터 타입이 다른 함수의 파라미터 타입의 상위 타입이면 덜 구체적인 함수다.)
  • 후보가 하나로 압축되면 이 함수가 호출할 함수다. 후보가 둘 이상이면 컴파일 오류가 발생한다.

 

코틀린에서는 파라미터 뒤에 변수 초기화 식을 추가하면 원하는 파라미터에 디폴트 값을 제공할 수 있다.

 

fun readInt(radix: Int = 10) = readLine()!!.toInt(radix)

fun restrictToRange(
    from: Int = Int.MAX_VALUE
    to: Int = Int.MIN_VALUE
    what: Int
): Int = Math.max(from, Math.min(to, what))

fun main() {
    println(restrictToRange(10, what = 1)
}

 

디폴트 파라미터 뒤에 디폴트가 지정되지 않은 파라미터가 있는 경우, 디폴트 파라미터를 지정하지 않고 이 함수를 호출하는 유일한 방법은 이름 붙은 인자를 사용하는 것뿐이다. 이러한 이유로 디폴트 파라미터를 사용하려면 함수 인자 목록 뒤쪽에 몰아두는 쪽이 좋다.

 


4. vararg

 

파라미터 정의 앞에 vararg 변경자(modifier)를 붙여 인자의 개수가 정해지지 않은 함수를 만들 수 있다.

파라미터를 적절한 배열 타입으로 사용할 수 있다. 혹은 스프레드(spread) 연산자인 *를 사용하여 배열을 가변인자 대신 넘길 수도 있다. 스프레드는 배열을 복사하기 때문에 파라미터 배열의 내용을 바꿔도 원본 원소에는 영향을 끼치지 않는다. 둘 이상을 vararg 파라미터로 선언하는 것은 금지된다. 하지만 vararg 파라미터에 콤마로 분리한 여러 인자와 스프레드를 섞어서 전달하는 것은 괜찮다. vararg 또한 디폴트와 마찬가지로 파라미터 목록의 맨 뒤에 위치시키는 것이 좋다.

 

fun printSorted(vararg items: Int) {  // items는 IntArray이다.
    items.sort()
    println(items.contentToString())
}

fun main() {
    val numbers = intArrayOf(6,2,10,1)
    printSorted(*numbers)                 // [1, 2, 6, 10]
    println(numbers)                      // Error: pass IntArray of Int
    println(numbers.contentToString())    // [6, 2, 10, 1]
    printSorted(6, 2, 10 ,1)              // [1, 2, 6, 10]
    printSorted(6, 1, *intArray(3, 8), 2) // 6,1,3,8,2 순서로 원소가 들어있는 배열이 전달되고,
                                          // [1,2,3,6,8]이 반환된다.
    printSorted(items = 1, 2, 3).         // 에러: vararg 파라미터는 이름 붙은 인자로 전달할 수 없다.
}

 

배열 내부에 참조가 들어있는 경우에는 참조가 복사되기 때문에 참조가 가리키는 데이터가 호출하는 쪽과 함수 내부 배열에서 공유된다.

 

fun change(vararg items: IntArray) {
    items[0][0]
}

fun main() {
    val a = intArrayOf(1, 2, 3)
    val b = intArrayOf(4, 5, 6)
    change(a, b)
    println(a.contentToString()) // [100, 2, 3]
    println(b.contentToString()) // [4, 5, 6]
}

 


5. 함수의 영역과 가시성

 

코틀린 함수는 정의된 위치에 따라 세 가지로 구분된다.

 

  • 파일에 직접 선언된 최상위 함수
  • 어떤 타입 내부에 선언된 멤버 함수
  • 다른 함수 안에 선언된 지역 함수

 

private, internal과 같은 가시성 변경자(visibility modifier) 키워드를 붙이면 함수가 쓰일 수 있는 위치를 제한할 수 있다.

 

  • 디폴트(public)로 선언된 최상위 함수는 함수가 정의된 파일 내부뿐 아니라 프로젝트 어디에서나 쓰일 수 있다.
  • 함수를 비공개(private)로 정의하면 함수가 정의된 파일 안에서만 해당 함수를 볼 수 있다.
  • internal 변경자를 적용하면 함수가 적용된 모듈(컴파일되는 파일 전부) 내에서만 함수를 사용할 수 있게 제한한다.

 

지역 함수의 영역은 함수를 감싸는 블록으로 한정된다. 지역 함수는 자신을 둘러싼 함수, 블록에 선언된 변수나 함수에 접근할 수 있다.

 


 

2. 패키지와 임포트

 


1. 패키지와 디렉터리 구조

 

패키지 디렉티브는 package 키워드로 시작하고 점(.)으로 구별된 식별자들로 이뤄진 패키지 전체 이름(qualified)이 뒤에 오며, 프로젝트의 전체 패키지 계층에서 루트 패키지로부터 지정한 패키지에 도달하기 위한 경로이다.

다른 패키지에 있는 함수를 호출하려면 그 선언이 포함된 패키지의 전체 이름을 덧붙이면 된다.

 

util.kt

package foo.bar.util

fun readInt(radix: Int = 10) = readLine()!!.toInt(radix)

------------------------------------------------------------

main.kt

package foo.bar.main

fun main() {
    println(foo.bar.util.readInt(8))
}

 

이처럼 사용하면 이름이 너무 길어진다. 때문에 파일 맨 앞에서 선언의 전체 이름을 import 디렉티브를 사용해 임포트하면 간단한 이름을 사용해 접근할 수 있다.

 

util.kt

package foo.bar.util

fun readInt(radix: Int = 10) = readLine()!!.toInt(radix)

------------------------------------------------------------

main.kt

package foo.bar.main

import foo.bar.util.readInt

fun main() {
    println(readInt(8))
}

 


2. 임포트 디렉티브 사용하기

 

임포트 디렉티브가 클래스나 함수 등의 최상위 선언만 임포트할 수 있는 것은 아니다.

클래스 안에 내포된 클래스(nested class)나 상수(enum constant)등도 임포트할 수 있다.

 

import kotlin.Int.Companion.MIN_VALUE
fun fromMin(steps: Int) = MIN_VALUE + n // MIN_VALUE를 간단한 이름으로 가리킴

 

서로 다른 패키지에 있는 일부 선언의 이름이 똑같다면 구분 할 수 있게 임포트 별명(alias)을 사용하면 된다.

 

import app.util.foo.readInt as fooReadInt
import app.util.bar.readInt as barReadInt

fun main() {
    val n = fooReadInt()
    val m = barReadInt()
}

 

혹은 영역에 속한 모든 선언을 한꺼번에 임포트할 수도 있다. 전체 이름 뒤에 *를 붙이면 된다.

 

import app.util.bar.*

 


 

3. 조건문

 


1. if 문으로 선택하기

 

if문은 조건이 참일 때 첫 번째 문장을 실행하고, 조건이 거짓일 때 else 다음의 문장을 실행한다.

 

fun max(a: Int, b: Int): Int {
    if(a>b) return a // 조건은 항상 Boolean 타입의 식이어야 한다.
    else return b
}

 

코틀린에서는 if를 식으로 사용할 수 있다.

if를 식으로 사용할 때는 양 가지가 모두 있어야 한다. else 가지가 없으면 컴파일되지 않는다.

 

fun max(a: Int, b: Int) = if(a>b) a else b

 

코틀린은 자바의 삼항연산자가 없지만 if를 식으로 쓸 수 있다는 점에서 이런 단점을 상쇄해준다.

 


2. 범위, 진행, 연산

 

코틀린은 범위(range)라고 하는 순서가 정해진 값 사이의 수열(interval)을 표현하는 타입을 제공한다.

범위를 만드는 가장 간단한 방법은 수 값에 대해 .. 연산자를 사용하는 것이다.

 

val chars = 'a'..'h'    // 'a'부터 'h'까지의 모든 문자
val twoDigits = 10..99  // 10부터 99까지의 모든 수
val zero20ne = 0.0..1.0 // 0부터 1까지의 모든 부동소수점 수

 

in 연산을 사용하면 어떤 값이 범위 안에 들어있는지 알 수 있다.

수 타입, Char, Boolean, String 등 모든 비교 가능한 타입에 대해 ..연산을 쓸 수 있다.

 

val num = readLine()!!.toInt()
println(num in 10..99)    // num >= 10 && num <= 99
println(num in 10 until 99)    // num >= 10 && num < 99
println(num !in 10..99)   // !(num in 10..99)
println("def" in "abc".."xyz") // true
println("zzz" in "abc".."xyz") // false
println(5 in 10..1)       // false: 끝 값이 시작 값보다 더 작으면 빈 범위가 된다.

 

범위와 연관이 있는 개념으로 진행(progression)이 있다. 진행은 정해진 간격(step)만큼 떨어져 있는 정수나 Char 값들로 이뤄진 시퀀스를 말한다.

 

println(1..10 step 3)         // 1, 4, 7, 10
println(15 downTo 9 step 2)   // 15, 13, 11, 9
println(5 in 10 downTo 1)     // true
println(5 in 5 until 5)       // false
println(5 in 10..1)           // false
println(1..10 step -3)        // error: 진행의 간격은 양수여야 한다.
//문자열
"Hello, World".substring(1..4)               // ello
"Hello, World".substring(1 until 4)          // ell
"Hello, World".substring(1, 4)               // ell: substring(1 until 4)와 같으
//배열
IntArray(10) { it*it }.sliceArray(2..5)      // 4, 9, 16, 25
IntArray(10) { it*it }.sliceArray(2 until 5) // 4, 9, 16
//컨테이너 타입
val numbers = intArrayOf(3, 7, 2, 1)
val text = "Hello!"
println(2 in numbers)  // true
println(9 !in numbers) // true
println(4 in numbers)  // false
println('a' in text)   // false
println('H' in text)   // true
println('h' in text)   // true

 

..연산은 우선순위로 보면 덧셈과 중위 연산 사이에 속하며 until, downTo, step은 다른 이름 붙은 중위 연산자(and, or 등)와 우선순위가 같다.

 

덧셈 +, - a+b..c-d // (a+b)..(c-d)
범위 .. a..b step c // (a..b) step c
a in b..c // a in (b..c)
중위 이름 붙은 중위 연산자들 a<b or b<c // (a<(b or b))<c
a==b and b==c // (a==b) and (b==c)
a in b or a in c // (a in (b or a)) in c
비교 < > <= >= a<b == b<c // (a<b)==(b>c)
a<b && b<c // (a<b)&&(b>c)

3. when 문과 여럿 중에 하나 선택하기

 

코틀린은 여러 대안 중 하나를 선택할 수 있는 when을 제공한다.

when문도 if처럼 식으로 쓸 수도 있다.

 

fun hexDigit(n: Int): Char {
    when {
        n in 0..9 -> return '0' + n
        n in 10..15 -> return 'A' + n - 10
        else -> return '?'
    }
}

fun hexDigitWhen(n: Int) = when {
    n in 0..9 -> '0' + n
    n in 10..15 -> 'A' + n - 10
    else -> '?'
}

fun numberDescription(n: Int): String = when(n) {
    0 -> "Zero"
    1, 2, 3 -> "Small"
    in 4..9 -> "Medium"
    in 10..max -> "Large"
    !in Int.MAX_VALUE -> "Nagativ"
    else -> "Huge"
}

fun readHexDigit() = when(val n = readLine()!!.toInt()) { // 식의 대상을 변수에 연결할 수 있다.
    in 0..9 -> '0' + n
    in 10..15 -> 'A' + n - 10
    else -> '?'
}

 

코틀린의 when은 자바의 switch 문과 비슷하다. 하지만 중요한 차이는 when에서는 임의의 조건을 검사할 수 있지만 switch에서는 주어진 식의 여러 가지 값 중 하나만 선택할 수 있다는 점이다. 자바 12부터는 코틀린의 대상이 있는 when과 비슷한 switch 식이 등장했다. 하지만 범위 검사를 지원하지 않고 오직 정수, 이넘, 문자열 같은 몇 가지 타입에 대해서만 사용할 수 있다.

 


 

4. 루프

 


1. while과 do-while 루프

 

while문은 어떤 조건이 참인 동안 루프를 실행한다. 이때 조건이 거짓이면 루프 몸통은 한 번도 실행되지 않는다.

do-while문은 루프 몸통을 실행한 다음에 조건을 검사하므로 루프 몸통이 최소 한번은 실행된다.

 

fun main() {
    var sum = 0
    var n
    
    do {
        sum = readLine()!!.toInt()
        sum += sum
    } while (n != 0)
    
    println("Sum: $sum")
    
    val m = Random.nextInt(1, 101)
    var guess = 0
    
    while (guess != num) {
        guess = readLine()!!.toInt()
        if (guess < num) println("Too small")
        else if (guess > num) println("Too big")
    }
    
    println("Right: it's $num")
}

 


2. for 루프와 이터러블

 

코틀린의 for 루프는 컬렉션과 비슷하게 여러 값이 들어있을 수 있는 값에 대한 루프를 수행할 수 있다.

 

fun main() {
    val a = IntArray(10) { it*it } // 0, 1, 4, 9, 16, ..
    val sum = 0
    
    for (x in a) { // 배열 루프
        sum += x
    }
    println("Sum: $sum") // Sum : 285
    
    for (i in 0..a.lastIndex step 2) { // 범위 루프
        a[i] *= 2 // 0, 2, 4, 6 ..
    }
    
    val str = "Hello"
    var rs = ""
    
    for (c in str) {  // 문자열 루프
        if(c in 'a'..'z') rs += c
    }
}

 

문자열과 배열에는 원소나 문자의 인덱스 범위를 제공하는 indices라는 프로퍼티가 들어있다.

 

val a = IntArray(10) { it*it } // 0, 1, 4, 9, 16 ..
for (i in a.indices step 2) {  // 0, 2, 4, 6, 8 ..
    a[i] *= 2
}

3. 루프 제어 흐름 변경하기: break와 continue

 

  • break는 즉시 루프를 종료시키고, 실행 흐름이 루프 바로 다음 문으로 이동하게 만든다.
  • continue는 현재 루프 이터레이션(iteration)을 마치고 조건 검사로 바로 진행하게 만든다.

 


4. 내포된 루프와 레이블

 

break/continue 식은 가장 안쪽에 내포된 루프에만 적용된다.

경우에 따라 더 밖에 있는 루프의 제어 흐름을 변경하고 싶을 때는 레이블 기능을 사용할 수 있다.

레이블을 사용하면 break와 continue를 제어를 옮길 대상 루프를 지정할 수 있다.

 

import kotlin.random.*

fun main() {
    val num = Random.nextInt(1, 101)
    
    loop@ while (true) {
        val guess = readLine()!!.toInt()
        
        val message = when {
            guess < num -> "Too small"
            guess > num -> "Too big"
            else -> break@loop //정상
        }
        println(message)
    }
    println("Right: it's $num")
}

 


5. 꼬리 재귀 함수

 

코틀린에서는 함수에 tailrec을 붙이면 컴파일러가 재귀 함수를 비재귀적인 코드로 자동 변환해준다.

재귀 함수는 일반적으로 비재귀 루프 보다 성능 차원에서 약간의 부가 비용이 발생하고 스택 오버플로우가 발생할 가능성이 있기 때문에 tailrec 키워드를 사용하여 재귀 함수의 간결함과 비재귀 루프의 성능만을 취할 수 있다.

 

tailrec fun factorial(n: Int, acc: Int): Int { // 꼬리 재귀로 구현한 팩토리얼
    return if (n <= 0) {
        acc
    } else {
        factorial(n-1, n*acc)
    }
}

fun main(args: Array<String>) {
    println("factorial(10) : ${factorial(10, 1)}" )
}

 

이런 재귀 함수를 꼬리 재귀로 변환을 적용하려면 함수가 재귀 호출 다음에 아무 동작도 수행하지 말아야 한다.

함수에 tailrec을 붙였는데 꼬리 재귀가 아니라는 사실을 컴파일러가 발견하면, 컴파일러는 경고를 표시하고 함수를 일반적인 재귀 함수로 컴파일한다.

 


 

5. 예외 처리

 


1. 예외 던지기

 

오류 조건을 신호로 보내려면 throw식에 예외 객체를 사용해야 한다.

 

fun parseIntNumber(s: String): Int {
    var num = 0
    
    if (s,length !in 1..31) throw NumberFormatException("Not a number: $s")
    
    for (c in s) {
        if (c !in '0'..'1') throw NumberFormatException("Not a number: $s")
        num = num*2 + (c - '0')
    }
    return num
}

 

예외를 던지면 다음과 같은 일이 벌어진다.

  • 프로그램은 예외를 잡아내는 핸들러를 찾는다. 예외와 일치하는 예외 핸들러가 있다면 예외 핸들러가 예외를 처리한다.
  • 현재 함수 내부에서 핸들러를 찾을 수 없으면 함수 실행이 종료되고 함수가 스택에서 제거된다. 호출한 쪽의 문맥 안에서 예외 핸들러 검색을 수행한다. 이 경우 예외를 호출자에게 전파했다고 말한다.
  • 프로그램 진입점에 이를 때까지 예외를 잡아내지 못하면 현재 스레드가 종료된다.

 


2. try 문으로 예외 처리하기

 

예외가 발생할 수 있는 코드를 try 블록으로 감싼다.

try 문에는 최소한 하나 이상 적절한 타입의 예외를 잡아내는 catch  블록이 있어야 한다. try 블록 내부의 코드가 에외를 던지면, 코드 실행이 중단되고 예외를 처리할 수 있는 첫 번째 catch 블록으로 제어를 이동한다.

일치하는 catch 블록이 없으면 예외가 전파된다.

 

import java.lang.NumberFormatException

fun readInt(default: Int): Int {
    try {
        return readLine()!!.toInt()
    } catch (e: Exception) {
        return 0
    } catch (e: NumberFormatException) {
        return default // 죽은 코드
    }
}

 

코틀린의 try는 식이다. 이 식의 값은 try 블록의 값이거나 예외를 처리한 catch 블록의 값이 된다.

 

import java.lang.NumberFormatException

fun readInt(default: Int) = try {
    readLine()!!.toInt()
} catch (e: NumberFormatException) {
    default
}

 

finally 블록은 try 블록을 떠나기 전에 프로그램이 어떤 일을 수행하도록 만들어준다. 이는 try 블록 앞이나 내부에서 할당한 자원을 해제할 때 유용하다. try 블록을 식으로 사용할 경우, finally 블록의 값은 전체 try 블록의 값에 영향을 미치지 못한다.

 

import java.lang.NumberFormatException

fun readInt(default: Int) = try {
    readLine()!!.toInt()
} finally {
    println("Error")
}

 

반응형