Kotlin 기초 - 기초 함수

안녕하세요. 이번 시간에는 코틀린 함수의 구조와 제어 구조 즉, 조건문, 루프, 오류처리 등과 코틀린 패키지 구조와 import에 대하여 개꿀떡 해보겠습니다.


1. 함수

- 자바처럼 코틀린 함수도 파라미터 즉, 어떠한 입력값을 받아서 자신을 호출한 쪽에 출력값을 반환할 수 있는 재사용가능한 코드블록을 말합니다.

 

1.1 코틀린 함수의 구조

- 'fun' 키워드는 컴파일러에게 함수 정의가 뒤따라온다는 사실을 알려준다.

- 변수처럼 아무 식별자나 함수 이름으로 사용 가능하다.

- 다음 괄호로 둘러싸여 있는 콤마(,)로 구분되어 있는 부분은 파라미터 목록이오고 함수를 호출할 때 넘길 수 있는 데이터를 컴파일러에게 알려준다.

- 반환 타입은 ' : ' 이후에 있으며 함수를 호출한 쪽에 돌려줄 반환값의 타입이다.

- 함수의 본문은 ' {} '로 감싼 블록이며, 함수 동작을 나타낸다.

- 파라미터를 감싸는 괄호는 필수가 아니다.

- return문 다음 코드들은 죽은 코드이다.

- 타입 지정을 생략해도 되는 변수와 달리 파라미터에는 항상 타입을 지정해야한다.(파라미터 타입추론 불가)

- 반환 타입은 타입추론이 가능하지만 파라미터와 마찬가지로 항상 타입 지정해야한다.

- Unit 타입일 경우 반환타입 생략가능하다.(결과가 항상 Unit으로 동일하기 때문에 return문 불필요)

- 식이 본문인 함수도 반환타입 생략이 가능하다.(중괄호 생략 가능, 반환 타입 생략 가능)

- 블록이 본문인 함수를 정의할 때 {} 앞에 =를 넣으면 람다로 해석이되서 원하는 값을 얻을 수 없다.(return 추가시 컴파일오류)

 

① vs Java

- Java와 달리 코틀린에서 도달할 수 없는 코드(죽은 코드)는 오류가 아니다.

- Java의 파라미터는 디폴트가 가변이므로 함수 내부에서 변경하지 못하게 하려면 final을 지정해야하지만 코틀린 함수는 무조건 불변이다.(파리미터 앞에 val, var 표시불가)

 

② 예시

< 코드_1 >

// 'fun' 키워드, 함수명, 파라미터 및 파라미터 타입, 리턴 타입, 중괄호(함수 동작)
fun squareArea(nWidth: Double, nHeight: Double): Double {
    return nWidth * nHeight
    // 이후 죽은코드
}

fun main() {
    println("Input Width : ")
    val nWidth = readLine()!!.toDouble()

    println("Input Height : ")
    val nHeight = readLine()!!.toDouble()

    println("Square Area : ${squareArea(nWidth, nHeight)}")
}

 

< 결과_1 >

 

<코드_2>

// 아래 함수와 동등한 함수이다.
//fun unitExample(sName: String): Unit {
//    println("Hello, My name is $sName!!")
//}

/* 변환 타입 생략 가능 - Unit */
fun unitExample(sName: String) {
    println("Hello, My name is $sName!!")
}

fun main() {
    unitExample("개꿀떡")
}

 

< 결과_2 >

 

< 코드_3 >

/* 변환 타입 생략 가능 - 식이 본문인 경우 */
fun squareArea2(nWidth: Double, nHeight: Double) =  nWidth * nHeight

/* 람다 함수 변환 시 원하는 값X */
fun squareArea3(nWidth: Double, nHeight: Double) =  { 
    nWidth * nHeight 
    // return nWidth * nHeight  // 리턴값 사용 시 에러
}

fun main() {
    println("Input Width : ")
    val nWidth = readLine()!!.toDouble()

    println("Input Height : ")
    val nHeight = readLine()!!.toDouble()

    println("Square Area2 : ${squareArea2(nWidth, nHeight)}")

    println("Square Area3 : ${squareArea3(nWidth, nHeight)}")
}

 

< 결과_3 >

 

 

1.2 위치 기반 파라미터와 이름 붙은 파라미터

- 위치 기반 파라미터란 첫 번째 인자는 첫 번째 파라미터에 두 번째 인자는 두 번째 파라미터에 전달하는 방식이다.

- 코틀린은 이름 붙은 파라미터도 제공한다.

- 한 호출에서 위치 기반 파라미터와 이름 붙은 파라미터 동시 사용이 가능하다.

 

① 예시

< 코드 >

fun square3DVolume(nWidth: Double, nLength: Double, nHeight: Double): Double {
    return nWidth * nLength * nHeight
}

fun main() {
    println("Input Width : ")
    val nW = readLine()!!.toDouble()

    println("Input Length : ")
    val nL = readLine()!!.toDouble()

    println("Input Height : ")
    val nH = readLine()!!.toDouble()

    // 위치 기반 파라미터 호출
    println("Square Area : ${square3DVolume(nW, nL, nH)}")

    // 이름 붙은 파라미터 호출
    println("Square Area : ${square3DVolume(nHeight = nH, nWidth = nW, nLength = nL)}")

    // 위치 기반 파라미터 + 이름 붙은 파라미터 호출
    println("Square Area : ${square3DVolume(nW, nHeight = nH, nLength = nL)}")             
}

 

< 결과 >

 

1.3 오버로딩과 디폴트 값

- 파라미터의 타입이 다르고 이름이 같은 함수를 여러개 작성이 가능하다. 즉, 오버로딩이 가능하다.

- 코틀린에서는 함수 오버로딩 보다는 디폴트 파라미터를 사용하는것이 좋다.

- 디폴트 파라미터 뒤에 디폴트가 아닌 파라미터가 있을 경우 이름 붙은 파라미터 호출방식을 사용해야한다.

- 디폴트 파라미터는 최대한 함수 파라미터 목록 젤 뒤에 사용하는것이 좋다.

 

① 오버로딩 해소 규칙

- 파라미터의 개수와 타입을 기준으로 호출할 수 있는 모든 함수를 찾는다.

- 위에서 선택된 함수중에서 파라미터 타입의 상위 타입인 경우 제외한다.

- 최종적으로 남은 함수가 하나인 함수가 호출되며 2개 이상일 경우 컴파일 오류가 발생한다.

 

② 예시

< 코드_1 >

/* 오버로딩 예제 */
fun mul(nA: Int, nB: Int)          = nA * nB            // 함수1
fun mul(nA: Int, nB: Int, nC: Int) = nA * nB * nC       // 함수2
fun mul(sA: String, nB: Int)       = sA.repeat(nB)      // 함수3
fun mul(nA: Any, nB: Int)          = Array(nB) { nA }   // 함수4

fun main() {
    println("결과1 : ${mul(1, 2)}") // Int가 Any의 하위 타입이므로 함수1 호출
//    println("결과2 : ${mul(1, 2L)}") // (Int, Long) 타입을 받을 수 있는 함수가 없으므로 오류
    println("결과3 : ${mul("0", 3)}") // String이 Any의 하위 타입 이므로 함수3 호출
    println("결과4 : ${mul("0" as Any, 3)}") // as타입을 사용해 "0" 값의 타입을 캐스팅 해서 험수4 호출
}

 

< 결과_1 >

 

< 코드_2 >

/* 디폴트 파라미터 예시 - 디폴트값은 뒤로 넣는 것이 좋다 */
fun squareArea4(nWidth: Double, nHeight: Double = 10.0): Double {
    return nWidth * nHeight
}

fun main() {
    println("Square Area1 : ${squareArea4(10.0)}")
    println("Square Area2 : ${squareArea4(15.0, 10.0)}")
}

 

< 결과_2 >

 

1.4  vararg

- 파라미터의 개수가 정해지지 않을 경우에 사용한다.

- 배열을 파라미터로 넘길경우에는 스프레드 연산자인 ' * '을 사용해야한다. (스프레드는 배열을 얕은 복사함으로 파라미터 배열의 내용이 바꿔도 원본은 바뀌지 않는다.)

- 둘 이상을  vararg 파라미터로 선언하는 것은 금지한다. 하지만 콤마로 분리하면 여러 인자와 스프레드를 섞을 수 있다.

- vararg 파라미터가 맨 마지막에 파라미터가 아니라면 vararg이후의 파라미터는 이름 붙은 파라미터로만 전달 가능하다.

- 디폴트 값이 있는 파라미터와 vararg 파라미터를 섞어 쓰려면 vararg 파라미터를 이름 붙은 파라미터 호출과 스프레드를 사용해야한다.

- vararg 파라미터 뒤에 디폴트 파라미터 일 경우  디폴트 파라미터를 이름 붙은 파라미터 호출방식을 사용해야 한다.

- vararg 파라미터와 다른 함수가 오버로딩으로 겹친다면 덜 구체적인 함수로 판단한다.

 

① 예시

< 코드 >

/* 파라미터 개수 정해지지 않을 경우 */
fun printSort(vararg nNumbers: Int) {
    nNumbers.sort()
    println("정렬 결과 : ${nNumbers.contentToString()}")
}

/* 디폴트값 + vararg 조합 일 경우 */
fun printSort2(sString: String = "", vararg nNumbers: Int) { println("printSort2()함수 정상 호출") }

/* vararg + 디폴트값 조합 일 경우 */
fun printSort3(vararg nNumbers: Int, sString: String = "") { println("printSort3()함수 정상 호출") }

/* vararg의 오버로딩 해소 */
fun printSort4(vararg nNumbers: Int) { println("printSort4()함수 정상 vararg 호출") }          // 함수1
fun printSort4(nA: Int, nB: Int, nC: Int) { println("printSort4()함수 정상 nA, nB, nC 호출") } // 함수2

fun main() {
    printSort(5, 3, 1, 2, 4)
    
    // 배열 넘기기
    val arrNumbers = intArrayOf(5, 3, 1, 2, 4)
//    printSort(arrNumbers) // 그냥 넘길 시 오류 발생
    printSort(*arrNumbers) // 반드시 스프레드 연산자 사용
    println(arrNumbers.contentToString()) // 얕은복사가 이뤄져 원본에 영향 안미친다.
    printSort(8, *arrNumbers, 7) // 섞어 사용가능
    
    // 디폴트값 + vararg 조합 일 경우 vararg을 이름 붙은 파라미터 방식 사용 하지 않을 때 오류 발생
//    printSort2(5, 3, 1, 2, 4)
    printSort2(nNumbers = *intArrayOf(5, 3, 1, 2, 4))

    // vararg + 디폴트값 조합 일 경우 디폴트값을 이름 붙은 파라미터 방식 사용 하지 않을 때 오류 발생
//    printSort3(5, 3, 1, 2, 4, "!")
    printSort3(5, 3, 1, 2, 4, sString = "!")

    // vararg 오버로딩 시 덜구체적인 함수 취급
    printSort4(1, 2, 3) // 함수2 호출
    printSort4(1, 2)     // 함수1 호출
}

 

< 결과 >

 

1.5 함수의 영역과 가시성

- 최상위 함수 : 파일에 직접 선언된 함수

- 멤버 함수 : 어떤 타입 내부에 선언된 함수

- 지역 함수 : 다른 함수 안에 선언된 함수

- 최상위 함수의 디폴트는 ' public ' 으로 공개 함수로 프로젝트 어디에서나 사용 가능하다.

- 가시성 변경자인 ' private '' internal '을 이용하면 프로젝트의 나머지 부분으로 구현 상세 내요을 숨겨서 보호 할 수 있다.

- ' private '함수는 정의된 파일 내에서만 사용 가능하다.

- ' internal '함수는 정의된 모듈 내에서만 사용 가능하다.

 

① 예시

< 코드_같은 모듈 util.kt >

package com.example.kotlinbase.BasicFunction

/* 디폴트가 public 최상위 함수 */
fun printHello() = println("Hello World!!")

/* private 최상위 함수 */
private  fun printName(sName: String) = println("My Name is $sName")

/* internal 최상위 함수 */
internal fun printAge(nAge: Int) = println("My Age is $nAge")

<코드_다른 모듈 util.kt>

package com.example.testmodule.internal

/* internal 최상위 함수 */
internal fun printGender(sGender: String) = println("My gender is $sGender")

<코드_main.kt>

package com.example.kotlinbase.BasicFunction

fun main() {
    /* 최상위 함수 호출 */
    printHello() // public 촤상위 함수 호출 가능
//    printName("개꿀떡") // private 최상위 함수 호출 불가능
    printAge(32) // 같은 모듈내 정의 되어 있으므로 internal 최상위 함수 호출 가능
//    printGender("남자") // 같은 모듈외 정의 되어 있으므로 internal 최상위 함수 호출 불가능
}

 

< 결과 >

 

2. 패키지와 임포트

- 코틀린의 패키지는 관련있는 선언들을 묶는 방법이다.

- 패키지는 이름이 있고, 다른 패키지를 포함 할 수도 있다.

- Java의 패키지와 개념은 비슷하지만 코틀린만의 특징도 있다.

 

2.1 패키지와 디렉터리 구조

- 자바와 마찬가지로 맨 앞에 패키지 이름을 지정하면 파일에 있는 모든 최상위 선언을 지정한 패키지 내부에 넣을 수 있다.

- 패키지를 지정하지 않으면 디폴트로 최상위 패기지에 속한다고 가정한다.

- ' package ' 키워드를 사용하며 점( . )으로  구별 되어 있다.

- 같은 패키지 안에서는 간단하게 함수를 호출 할 수 있다.,

- 다른 패키지 함수를 호출 할 때는 전체 이름을 사용하여 호출 할 수 있다.

- 전체 이름을이 너무 길어 지게 되면 import 디렉티브를 사용 하여 간단하게 호출 가능하다.

 

① vs Java

- Java에서는 패키지 구조와 컴파일 대상 루트에 있는 소스 트리 디렉터리 구조가 다르면 오류가 발생한다.

 

② 예시

< 코드_같은 패키지 Util.kt >

package com.example.kotlinbase.BasicFunction

fun printHelloWorld() = println("Hello World!!")

< 코드_다른 패키지 Util.kt >

package com.example.kotlinbase.BasicFunction2

fun printHelloName(sName: String) = println("Hello $sName!!")

< 코드_main.kt >

package com.example.kotlinbase.BasicFunction

fun main() {
    // 같은 패키지내 함수 호출
    printHelloWorld()

    // 다른 패키지의 전체이름 사용한 함수 호출
    com.example.kotlinbase.BasicFunction2.printHelloName("개꿀떡")
}

< 코드_main2.kt >

package com.example.kotlinbase.BasicFunction

import com.example.kotlinbase.BasicFunction2.printHelloName

fun main() {
    // 다른 패키지의 import 디렉티브 사용해 함수 호출
    printHelloName("개꿀떡")
}

 

< 결과_main.kt >

< 결과_main2.kt >

 

 

2.2 임포트 디렉티브 사용하기

- 전체이름을 사용하지 않아 코드를 간단하게 할 수 있다.

- 다른 패키지의 똑같은 이름의 선언을 임포트 디렉티브 할 경우 별명을 주어 구분 한다.

- 임포트 디렉티브 시 ' * '를 사용하면 모든 선언을 한꺼번에 임포트 가능하다.

- 지정된 선언 임포트 디렉티브 보다 ' * ' 임포트 디렉티브의 우선순위가 낮다.

 

① vs Java

- Java와 달리 코틀린은 타입 멤버를 임포트한는 별도의 import static 디렉티브가 없다.

 

② 예시

< 코드_다른 패키지 util.kt >

package com.example.kotlinbase.BasicFunction2

fun printHelloHello() = println("Hello Hello2")

< 코드_또 다른 패키지 util.kt >

package com.example.kotlinbase.BasicFunction3

fun printHelloHello() = println("Hello Hello3")

< 코드_main.kt >

package com.example.kotlinbase.BasicFunction

import com.example.kotlinbase.BasicFunction2.printHelloHello as printHelloHello2
import com.example.kotlinbase.BasicFunction3.printHelloHello as printHelloHello3

fun main() {
//    printHelloHello() // 그냥 호출시 오류 나오므로 별명을 지어 구분 지어주기
    printHelloHello2()  // com.example.kotlinbase.BasicFunction2 패키지의 함수 호출
    printHelloHello3()  // com.example.kotlinbase.BasicFunction3 패키지의 함수 호출
}

< 코드_main2.kt >

package com.example.kotlinbase.BasicFunction

import com.example.kotlinbase.BasicFunction2.printHelloHello
import com.example.kotlinbase.BasicFunction3.*

fun main() {
    // * 가 우선순위가 낮아지므로 com.example.kotlinbase.BasicFunction2 패키지의 함수 호출
    printHelloHello()
}

 

< 결과_main.kt >

< 결과_main2.kt >

 

3. 조건문

- 조건문은 어떤 조건의 값에 따라 둘 이상의 동작 중 하나를 수행할 수 있다.

 

3.1 if 문

- Boolean식의 결과에 따라 두 가지 대안 중 하나를 선택 한다.

- 기본적으로 if문의 조건이 참일 때 첫 번째 문장을 실행시키고, 거짓일 경우 else 다음의 문장을 실행 시킨다.

 

① vs Java

- Java의 3항 연산자(조건 ? 참일 때_식 : 거짓일 때_식)가 없다.

 

② 예시

< 코드 >

fun max(nNumber1: Int, nNumber2: Int): Int {
    if(nNumber1 > nNumber2) return nNumber1
    else return  nNumber2
}

fun main() {
    println("누가 더 클까?  ${max(2, 3)}")
}

 

< 결과 >

 

3.2 범위, 진행, 연산

- 범위는 수열을 표현 하는 몇가지 타입이다.

- 범위의 대표적인 연산자는 ' .. '가 있다.

- ' in ' 연산자는 어떤 값이 범위 안에 들어 있는지 알 수 있다. (' !in '은 반대 개념)

- ' in ' 연산자와  ' .. '연산자를 사용하면 Char, Boolean, String 등 비교 가능하다.

- ' until '은 반만 닫힌 범위 연산자 이다.

- 진행 연산자는 간격을 나타내는 ' step ' 연산자와 내려가는 값을 나타내는 ' downTo ' 등이 있다.

- 범위 연산자를 활용하여 subString()함수로 문자열을 뽑아 낼 수 있다.

- ' in '' !in '연산자는 범위만 있는것은 아니고 문자열이나 배열에서 다른 원소를 담는지 유무를 알 수 있다.

 

① 연산자 우선순위

덧셈, 뺄셈 +, -
범위 ..
중위 이름 붙은 중위 연산자들
원소 검사 in, !in
비교  <, >, <=, >=

 

② 예시

< 코드 >

fun main() {
    /* 범위 연산자 */
    val nTwoDigits = 10..99 // 범위 연산자
    println("아무 숫자 입력 : ") // 범위 안에 들어가는지 확인
    val nInputNum1  = readLine()!!.toInt()
    println("결과1 : ${nInputNum1 in nTwoDigits}") // 범위 안에 들어가는지 확인

    val sThreeChars = "abc" .. "xyz"
    println("결과2: ${"zzz" in sThreeChars}") // 스트링 비교 가능

    val nTwoDigitsWithOut99 = 10 until 99
    println("결과3 : ${99 in nTwoDigitsWithOut99}") // 범위 안에 들어가는지 확인

    val nDownTo = 10 downTo 1   // 10 부터 내려감
    println("결과4 : ${5 in nDownTo}") // 범위 안에 들어가는지 확인

    val nStep = 1..10 step 3    // 1, 4, 7, 10
    println("결과5 : ${5 in nStep}") // 범위 안에 들어가는지 확인

    val nDownToStep = 15 downTo 8 step 2    // 15, 13, 11, 9(downTo, step 동시 사용 가능)
    println("결과5 : ${9 in nStep}") // 범위 안에 들어가는지 확인

    println("결과6 : ${"Hello World".substring(1..4)}") // subString함수에 사용
}

 

< 결과 >

 

3.3 when문

- if문을 연쇄적으로 사용하면 순차적으로 검사하여 조건에 맞는 부분을 찾는 반면, when은 여러 대안 중 하나를 선택 하여 간결하게 찾는다.

- 한가지 값에 대해 동등성과 in 연산만 사용하는 경우 조건값 생략 가능하다.

 

① vs Java

- Java의 switch문과 비슷 하지만 when문은 임의의 조건을 검사 할 수 있지만 switch문은 주어진 식의 여러 가지 값 중 하나만 선택 가능하다.

- Java의 switch문은 오직 정수, 이넘, 문자열 같은 몇 가지 타입에 대해서만 사용 가능하다.

 

② 예시

< 코드 >

/* 일반적인 when문 */
fun numberPrint1(n: Int):String {
    return when {
        n == 0 -> "Zero"
        n == 1 || n == 2 || n == 3 -> "Small"
        n in 4..9 -> "Medium"
        n in 10..100 -> "Large"
        n !in Int.MIN_VALUE until 0 -> "Negative"
        else -> "Huge"
    }
}

/* when 조건이 동등 조건과 in 연산만 있을 경우 생략 가능 */
fun numberPrint2(n: Int):String {
    return when(n) {
        0 -> "Zero"
        1, 2, 3 -> "Small"
        in 4..9 -> "Medium"
        in 10..100 -> "Large"
        !in Int.MIN_VALUE until 0 -> "Negative"
        else -> "Huge"
    }
}

fun main() {
    println("numberPrint1() 함수 결과 : ${numberPrint1(32)}")
    println("numberPrint2() 함수 결과 : ${numberPrint1(32)}")
}

 

< 결과 >

 

4. 루프 

- 같은 명령 시퀀스를 주어진 데이터에 대해 수행하거나 주어진 조건이 만족될 때까지 수행하는 구조이다.

 

4.1 while과 do-while 루프

- ' do-while '은 do와 while 사이의 루프 몸통을 실행한다.

- ' do-while '에서 while 다음의 조건이 참이면 루프 몸통을 다시 실행하고 거짓이면 루프문 다음 문을 실행 한다.

- ' do-while '은 루프 몸통이 최소 한번은 실행 된다.

- ' while '은 어떤 조건이 참인 동안 루프를 실행하지만 루프 몸통을 실행 하기 전에 조건을 먼저 검사 한다.

 

 예시

< 코드 >

/* Do - While 함수 */
fun sumNumber() {
    var nSum = 0
    var nNum: Int

    do {
        nNum = readLine()!!.toInt()
        nSum += nNum
    } while (nNum != 0)

    println("Do-While 결과 : $nSum")
}

/* While 함수 */
fun sumNumbe2() {
    var nSum = 0
    var nNum = 1

    while (nNum != 0) {
        nNum = readLine()!!.toInt()
        nSum += nNum
    }

    println("While 결과 : $nSum")
}

fun main() {
    sumNumber() // Do-While문 호출 : 반드시 한번은 반복
    sumNumbe2() // While문 호출 : 조건 먼저 검사
}

 

< 결과 >

 

4.2 for 루프

- for 루프를 사용하면 컬렉션과 비슷하게 여러 값이 들어있을 수 있는 값에 대한 루프를 수행 가능하다.

- 반복 대상을 담을 변수 정의

- 반본에 사용할 값들이 담겨 있는 컨테이너를 계산하기 위한 식 정의

- 루프 몸통에 해당하는 문, 반복 시 몸통 실행

- 문자열의 각 문자에 대한 루프 수행 가능하다.

 

① vs Java

- 코틀린의 for 루프는 이터러블 인스턴스에 대한 루프를 간편하게 작성하도록 되어 Java의 for-each 루프랑 비슷하다.

- Java는 문자열의 각 문자에 대해 루프를 직접 수행 할 수 없어 인덱스로 루프를 돌거나 문자를 배열로 바꿔 루프를 실행 해야한다.

 

② 예시

< 코드 >

fun main() {
    val arrA = IntArray(10) {it * it}
    var nSum = 0

    for(i in arrA) {
        nSum += i
    }

    println("합계 : $nSum")

    val sString = "Hello, World"
    for(j in sString) {
        println(j)
    }
}

 

< 결과 >

 

4.3 루프 제어 흐름 변경하는 break와 continue

- 루프의 흐름을 바꾸고 싶을 때 사용한다.

- ' break '는 즉시 루프를 종료 시키고, 루프 다음 문으로 이동 하게 한다.

- ' continue '는 현재 루프 반복을 마치고 조건 검사로 바로 진행하게 만든다.

 

① vs Java

- return과 마찬가지로 코틀린에서는 break와 continue의 뒤에는 죽은코드이다.

- Java에서는 switch문의 남은 조건 실행을 막기위해 break를 사용하지만 코틀린의 when문에는 사용하지 않는다.

 

② 예시

< 코드 >

fun main() {
    var nNum: Int

    while (true) {
        nNum = readLine()!!.toInt()

        if(nNum in 10 .. 99) break
        else {
            println("두자리수 찾기 시도 중...")
            continue
        }
    }

    println("두자리수 찾기 완료")
}

 

< 결과 >

 

4.4 내포된 루프와 레이블

- 루프를 내포시켜 사용하는 경우  break/continue 식은 가장 안쪽 루프에만 적용되지만 레이블을 사용하여 밖에 있는 루프의 제어 흐름을 변경 할 수 있다.

 

① vs Java

- Java는 loop: while(true) break loop

- 코틀린은 loop@ while(true) break@loop

 

② 예시

< 코드 >

import kotlin.random.Random

fun main() {
    val nRan = Random.nextInt(1, 101)

    loop@ while (true) {
        val nGuess = readLine()!!.toInt()

        val sMessage = when {
            nGuess < nRan -> "Small"
            nGuess > nRan -> "Big"
            else -> break@loop
        }
        println(sMessage)
    }
    println("Right: it's $nRan")
}

 

< 결과 >

 

4.5 꼬리 재귀 함수

- 코틀린은 꼬리 재귀 함수에 대한 최적화 컴파일을 지원한다.

- 일반적으로 재귀함수를 사용하게되면 스택 오버플로가 발생하게 되는데 코틀린에서는 ' tailrec '키워드를 사용하여 재귀함수를 비재귀적인 코드로 자동으로 변환시켜 간결함과 비재귀 루프성능을 얻을 수 있다.

- 꼬리재귀는 재귀 호출이 끝나면 아무 일도 하지 않고 결과만 바로 반환 되도록 한다.

 

 예시

< 코드 >

/* 일반 재귀 함수 */
fun factorial1(nNum: Int): Long {
    println("Number : $nNum")
    return if(nNum == 1) nNum.toLong() else nNum * factorial1(nNum - 1)
}

/* 꼬리 재귀 함수 */
tailrec fun factorial2(nNum: Int, nRun: Int = 1): Long {
    println("Number : $nNum")
    return if(nNum == 1) nNum.toLong() else nNum * factorial2(nNum - 1, nRun * nNum)
}

fun main() {
    var nNumber = readLine()!!.toInt()
    val nResult1: Long

    // 일반 재귀 함수 결과
    nResult1 = factorial1(nNumber)
    println("Factorial1: $nResult1")

    val nResult2: Long
    // 일반 재귀 함수 결과
    nResult2 = factorial2(nNumber)
    println("Factorial2: $nResult2")
}

 

< 결과 >

 

5. 예외처리

- 함수가 비정상종료 될 경우 예외를 던질 수 있다.

 

5.1 예외 던지기

- 오류 조건을 신호로 보내려면 자바와 마찬가지로 throw 식에 예외 객체를 사용 해야한다.

- 예외 던지기 시 프로그램은 예외를 잡아내는 핸들러를 찾고 일치한다면 예외 핸들러가 예외를 처리한다.

- 핸들러를 찾을 수 없다면 함수 실행이 종료되고 함수가 스택에서 제거 되고 호출한 쪽의 문맥 안에서 예외 핸들러 검색을 수행한다.

- 프로그램 진입점에 이를 때까지 예외를 잡아내지 못하면 현재 스레드가 종료된다.

 

① vs Java

- Java와 달리 new와 같은 클래스 인스턴스를 생성 할 필요 없다.

- 코틀린에서는 return, break, continue와 같이 throw 뒤에는 죽은코드이다.

 

② 예시

< 코드_1 >

fun sayHello(sName: String) {
    val sMsg =
        if (sName.isNotEmpty()) "Hello, $sName"
        else throw IllegalArgumentException("Empty name")

    println(sMsg)
}

fun main() {
    sayHello("") // 비워 있을경우 예외처리
//    sayHello("개꿀떡") // 정상 처리
}

 

< 결과_1 >

< 코드_2 >

fun sayHello(sName: String) {
    val sMsg =
        if (sName.isNotEmpty()) "Hello, $sName"
        else throw IllegalArgumentException("Empty name")

    println(sMsg)
}

fun main() {
//    sayHello("") // 비워 있을경우 예외처리
    sayHello("개꿀떡") // 정상 처리
}

 

< 결과_2 >

 

5.2 try 문으로 예외 처리하기

- Java와 마찬가지로 예외처리시 try 문 사용

- ' try - finally '문은 try 블록이 떠나기 전에 finally 블록에서 어떤 일을 수행하도록 만들어준다.(할당된 자원을 해제할 때 유용)

 

① vs Java

- Java와 같이 catch(FooException | BarException e) {} 와 같은 한 캐치 블록안에서 여러 예외처리가 불가능하다.

- 코틀린에서는 순차적으로 catch블록을 검사하기 때문에 상위예외를 나중에 작성해야한다. (상위예외를 먼저 작성하면 이후 코드들은 죽은 코드가 되버린다.)

 

② 예시

< 코드 >

fun readInt(nNum: Int): Int {
    try {
        return readLine()!!.toInt()
    } catch (e: Exception) {
        return 0
    } catch (e: NumberFormatException) {
        return  nNum    // Exception이 NumberFormatException보다 상위 타입 이기 때문에 죽은코드
    } finally {
        println("Error")
    }
}

fun main() {
    println("결과 : ${readInt(12)}")
}

 

< 결과 >


이번 시간에는 코틀린의 기초적인 함수에 대하여 개꿀떡해보았습니다. 다음시간에는 객체지향 프로그래밍으로써 클래스와 객체를 정의하는 방법과 클래스 초기화를 이해하며 프로퍼티를 선언하고 사용하는 방법과 null값을 다루는 방법에대해 개꿀떡 해보겠습니다. 감사합니다:)

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유