자바의 메서드처럼 코틀린 함수도 어떤 입력(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 변경자(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 키워드를 사용할 수 있다.
fun main() {
fun readInt() = readLine()!!.toInt()
println(readInt())
}
// readInt 함수는 main 함수 내에 정의된 지역 함수이기에 외부에서 사용할 수 없다.
fun readIntPair() = intArrayOf(readInt(), readInt()) // Error: unresolved reference: readInt
지역 함수와 변수에는 가시성 변경자를 붙일 수 없다.
자바의 if문과 달리 코틀린은 if를 식으로 사용할 수 있다.
fun max(a: Int, b: Int) = if (a > b) a else b
코틀린은 자바와 달리 3항 연산자(조건 ? 참 : 거짓)가 없다.
하지만 if를 식으로 쓸 수 있다는 점이 이 부분을 상쇄해준다.
자바의 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
}
반만 닫힌 범위 연산자이다. 즉, 시작 값은 포함, 끝 값은 포함하지 않는다.
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
}
fun main() {
println(5 in 10 downTo 1) // true
println(11 in 10 downTo 1) // false
}
fun main() {
println(4 in 1..10 step 3) // true
println(5 in 1..10 step 3) // false
}
자바와 동일하다.
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")
}
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
}
예외를 던질 때는 다음과 같은 일이 벌어진다.
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")
}