3장. 함수 정의하기

Bimmer·2023년 8월 27일
0
post-thumbnail

함수

자바의 메서드처럼 코틀린 함수도 어떤 입력(parameter)을 받아서 자신을 호출한 코드 쪽에 출력값을 반환(return)할 수 있는 재사용 가능한 코드 블록이다.

// 파라미터가 없고 Int 타입을 반환하는 함수
fun readInt(): Int {
	return readLine()!!.toInt()
}

// 파라미터와 반환 타입이 없는 함수 -> 반환 타입이 없는 Unit 타입일 때만 반환 타입 생략이 가능하다.
fun main() {
	println(readInt())
}

/*
fun main(): Unit {
	println(readInt())
}
*/

// 파라미터로 Int형 a, b를 받고 Int 타입을 반환하는 함수
fun sum(a: Int, b: Int): Int {
	return a + b
}

// 반환 타입을 명시한 식이 본문인 함수
fun sum(a: Int, b: Int): Int = a + b

// 반환 타입을 생략한 식이 본문인 함수
fun sum(a: Int, b: Int) = a + b

// 반환 타입을 생략한 식이 본문인 함수에서 {} 중괄호 사용 시 람다로 해석되기에 주의
fun sum(a: Int, b: Int) = { a + b }

자바 메서드 파라미터는 디폴트가 가변이므로 함수 내부에서 변경하지 못하게 하려면 final 을 지정해 불변 값으로 바꿔야 하는데, 코틀린 함수 파라미터는 무조건 불변이다.
즉, 함수 본문에서 파라미터 값을 변경하면 컴파일 오류가 발생한다.

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

그리고 파라미터 앞에는 val이나 var 키워드를 표시할 수 없다.
또한, 변수와 달리 파라미터에는 항상 타입을 지정해야 하낟. 컴파일러는 함수 정의에서 파라미터 타입을 추론하지 못한다.


 

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

자바나 다른 언어와 마찬가지로 기본적으로 함수 호출 인자는 순서대로 파라미터에 전달된다.
첫 번째 인자는 첫 번째 파라미터, 두 번째 인자는 두 번째 파라미터라는 식이다.

코틀린에서는 이런 방식을 위치 기반 인자(positional argument)라고 한다.

하지만, 코틀린은 이름 붙은 인자(named argument)라는 방식도 제공한다.

fun sum(a: Int, b:Int) = a + b

sum(b = 10, a = 5)

 

오버로딩

자바 메서드와 마찬가지로 코틀린 함수도 오버로딩할 수 있다.
다만, 컴파일러가 어떤 함수를 호출해야 할지 구분할 수 있도록 오버로딩한 함수의 파라미터 타입이 모두 달라야 한다.

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

 
하지만 아래 함수는 파라미터 타입은 같고, 반환값만 다르기에 컴파일 오류가 발생한다.

fun plus(a: String, b: String) = a + b
fun plus(a: String, b: String) = a.toInt() + b.toInt()	// Error: conflicting overloads

 

디폴트 값

코틀린은 경우에 따라 함수 오버로딩을 쓰지 않고, 디폴트 파라미터를 쓸 수 있다.

// 파라미터 radix에 10이라는 디폴트 값을 명시
fun readInt(radix: Int = 10) = readLine()!!.toInt()

// 일반 파라미터와 디폴트 값이 있는 파라미터를 동시에 받는 함수인 경우, 디폴트 값이 있는 파라미터를 뒤쪽에 몰아두는 것이 좋다.
fun sum(a: Int, b: Int, c: Int = 10) = a + b + c

이렇게 선언하면 인자 없이 함수를 호출해도 되고 인자를 지정해 호출해도 된다.


 

vararg

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

fun printSorted(vararg items: Int) {
	items.sort()
    println(items.contentToString())
}

fun main() {
	printSorted(6, 2, 10, 1)	// [1, 2, 6, 10]
}

 
스프레드(spread) 연산자인 *를 사용하면 배열을 가변 인자 대신 넘길 수 있다.
스프레드 연산자는 배열을 복사한다. 따라서 원본에 영향을 미치지 않는다.

fun main() {
	printSorted(6, 2, 10 , 1)			// [1, 2, 6, 10]
    
    val arr = intArrayOf(6, 2, 10, 1)
    // 스프레드(spread) 연산자 *를 사용하면 배열을 가변 인자 대신 넘길 수 있다.
    printSorted(*arr)					// [1, 2, 6, 10]
    printSorted(arr)	// Error: passing IntArray instead of Int
    
    // 스프레드(spread) 연산자 *는 배열을 복사하기에 원본에는 영향을 미치지 않는다.
    println(arr.contentToString())		// [6, 2, 10, 1]
}

fun printSorted(vararg items: Int) {
    items.sort()
    println(items.contentToString())
}

 
하지만 이때 얕은(shallow) 복사가 이뤄진다. 즉, 배열 내부에 참조가 들어있는 경우에는 참조가 복사되기 때문에 참조가 가리키는 데이터가 호출하는 쪽과 함수 내부 배열에서 공유된다.

fun main() {
	val a = intArrayOf(1, 2, 3)
    val b = intArrayOf(4, 5, 6)
    
    change(a ,b)

	// change 함수 내부에서 100으로 변경된 값이 공유
	println(a.contentToString())	// [100, 2, 3]
	println(b.contentToString())	// [4, 5, 6]
}

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

 

함수의 영역과 가시성

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

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

 

최상위 함수

main()과 같은 최상위 함수는 디폴트로 공개(public) 함수다.
즉, 디폴트로 선언된 최상위 함수는 정의된 파일 내부뿐 아니라 프로젝트 어디에서나 쓰일 수 있다.

자바와 달리 디폴트가 public이기에 변경자를 명시할 필요 없다.

// public은 불필요한 중복이다.
public fun main() {
}

위치를 제한하고 싶은 경우 가시성 변경자(visibility modifier)로 private이나 internal 키워드를 사용할 수 있다.

  • private: 함수가 정의된 파일 안에서만 해당 함수를 사용할 수 있다.
  • internal: 함수가 적용된 모듈 내에서만 함수를 사용할 수 있다.

 

지역 함수

fun main() {
	fun readInt() = readLine()!!.toInt()
    println(readInt())
}

// readInt 함수는 main 함수 내에 정의된 지역 함수이기에 외부에서 사용할 수 없다.
fun readIntPair() = intArrayOf(readInt(), readInt())	// Error: unresolved reference: readInt

지역 함수와 변수에는 가시성 변경자를 붙일 수 없다.


 

조건문

if

자바의 if문과 달리 코틀린은 if를 식으로 사용할 수 있다.

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

코틀린은 자바와 달리 3항 연산자(조건 ? 참 : 거짓)가 없다.
하지만 if를 식으로 쓸 수 있다는 점이 이 부분을 상쇄해준다.

 

when

자바의 switch와 유사하다. 또한,if처럼 식으로 사용할 수 있다.

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

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

자바의 switch 문은 풀스루(fall-through)로 명시적으로 break를 만날 때까지 모든 가지를 실행한다.
하지만 코틀린 when은 조건을 만족하는 가지만 실행하고 절대 폴스루 하지 않는다. -> break 필요 없다.

 
when의 조건이 한 가지 값을 대상으로 하는 경우 아래와 같이 쓸 수 있다.

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

 

범위, 진행, 연산

 

.. 연산자

.. 연산에 의해 만들어지는 범위는 닫혀 있다. 즉, 시작 값과 끝 값이 범위에 포함된다.

fun main() {
    val num1 = 10
    val num2 = 20
    val num3 = 21
    val range = 10..20
    
	println(num1 in range)	// true
	println(num2 in range)	// true
	println(num3 in range)	// false
    
    val char1 = 'c'
    val char2 = 'h'
    val char3 = 'i'
    println(char1 in 'a'..'h')	// true
    println(char2 in 'a'..'h')	// true
    println(char3 in 'a'..'h')	// false
}

 

until 연산자

반만 닫힌 범위 연산자이다. 즉, 시작 값은 포함, 끝 값은 포함하지 않는다.

fun main() {
    val num1 = 10
    val num2 = 20
    val num3 = 21
    val range = 10 until 20
    
	println(num1 in range)	// true
	println(num2 in range)	// true
	println(num3 in range)	// false
    
    val char1 = 'c'
    val char2 = 'h'
    val char3 = 'i'
    println(char1 in 'a' until 'h')	// true
    println(char2 in 'a' until 'h')	// true
    println(char3 in 'a' until 'h')	// false
}

 

downTo 연산자

fun main() {
	println(5 in 10 downTo 1)	// true
    println(11 in 10 downTo 1)	// false
}

 

step 연산자

fun main() {
	println(4 in 1..10 step 3)	// true
	println(5 in 1..10 step 3)	// false
}

 

루프

 

while, do-while

자바와 동일하다.

fun main() {
	var sum = 0
    var num
    
    do {
    	num = readLine()!!.toInt()
        sum += num
    } while (num != 0)
    
    println("Sum: $sum")
}
fun main() {
	val num = 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")
}

 

for 루프와 이터러블

val a = IntArray(10) { it*it }	// 0, 1, 4, 9, 16, ...

for (i in 0..a.lastIndex) {
	if (i % 2 == 0) {
    	a[i] *= 2
    }
}

// step을 사용하면 더 간편하게 작성 가능
for (i in 0..a.lastIndex step 2) {
	a[i] *= 2
}

 

예외 처리

예외 던지기

자바와 마찬가지로 throw 식에 예외 객체를 사용해야 한다.
단, 예외를 생성할 때 new 같은 키워드 없이 함수 호출과 동일하게 작성한다.

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
}

 
예외를 던질 때는 다음과 같은 일이 벌어진다.

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

 

try 문으로 예외 처리하기

catch 블록은 선언된 순서대로 예외 타입을 검사하기 때문에 어떤 타입을 처리할 수 있는 catch 블록을 그 타입의 상위 타입을 처리할 수 있는 catch 블록보다 앞에 작성해야 한다.

즉, NumberFormatException은 Exception의 하위 타입이기에 앞단에 위치해야 한다.

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

자바 7부터는 catch (FooExcetion || BarException e) {} 같은 구문을 사용해 한 캐치 블록 안에서 여러 예외를 처리할 수 있지만, 코틀린은 아직 지원하지 않는다.

또한, 자바와 달리 코틀린은 try가 식이다.

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

자바와 달리 코틀린에서는 검사 예외(checked exception)와 비검사 예외(unchecked exception)를 구분하지 않는다.
큰 프로젝트에서 발생할 수 있는 예외를 함수에 지정하도록 요구해도 실제로는 생산성이 저하되고 불필요하게 긴 준비 코드를 생성한다는 사실을(자바를 통한 경험) 알았기 때문이다.

자바와 동일하게 finally 블록도 지원한다.

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

profile
hello

0개의 댓글