[Kotlin] 코틀린 함수
1. 코틀린 함수의 구조
코틀린에서 함수가 파라미터를 받지 않는다고 해도 파라미터를 감싸는 괄호는 꼭 있어야 한다. 예를 들어, fun readInt(): Int 와 같은 함수에서는 괄호가 필수적이다. 이는 코틀린의 문법 규칙에 따른 것이다.
fun readInt(): Int {
return readLine()!!.toInt()
}
fun main(){
println(readInt())
}
자바 vs 코틀린
자바와 달리 코틀린에서 도달할 수 없는 코드(unreachable code)는 오류가 아니다. 하지만 컴파일러는 경고를 표시하고 IDE는 해당 부분을 강조해주기 때문에 코드 중 일부가 죽은 코드임을 쉽게 알 수 있다.
파라미터 정의는 암시적으로 함수가 호출될 때 자동으로 인자값으로 초기화 되는 지역변수로 취급된다.
fun increment(n:Int): Int {
return n++ // Error: can't change immutable variable
}
자바 vs 코틀린
코틀린의 함수 파라미터는 무조건 불변이며 함수 본문에서 파라미터 값을 변경하면 위와 같이 컴파일 오류가 발생한다.
이렇게 불변 값으로 고정해 놓기 때문에 파라미터 앞에 var이나 val을 표시할 수 없다.
이렇게 강제하는 이유는 파라미터에 대입하는 중에 실수할 가능성이 높을 뿐 아니라 파라미터를 불변 값으로 강제하는 편이 더 깔끔하고 이해하기 편한 코드를 만들어내기 때문이다.
값에 의한 호출
코틀린에서 기본적으로 모든 파라미터는 값에 의한 호출(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]
}
타입 지정을 생략해도 되는 변수와 달리 파라미터에는 항상 타입을 지정해야 한다. 컴파일러는 함수 정의에서 파라미터 타입을 추론하지 못한다.
반면 반환 타입은 함수 파라미터에서 추론이 가능한데도 여전히 명시해야 한다. 그러나 아래와 같은 경우는 반환 타입을 생략할 수 있습니다.
1. 유닛(Unit) 타입을 반환할 때
- 유닛은 자바 void에 해당하는 코틀린 타입으로, 반환값을 돌려주지 않는다는 뜻이다.
- Unit은 코틀린의 내장 타입에 속하는 상수이므로, 다음 두 함수의 정의는 같다.
fun test(name: String) {}
fun test(name: String): Unit {}
2. 식이 본문(expression-body)인 함수일 때
- 어떤 함수가 단일식으로 구현이 될 수 있다면 return 키워드와 중괄호를 생략하고 아래와 같은 형태로 함수를 작성해도 된다.
- 단, 반환 타입과 함수 본문식 사이에 =가 들어 있다는 점에 유의하자.
fun circleArea1(radius: Double) = PI*radius*radius // 반환값이 Double로 추론됨
fun circleArea2(radius: Double) = { PI*radius*radius }
fun circleArea3(radius: Double) = {
return PI*radius*radius
}
반면 circleArea3과 같이 블록이 본문인 함수를 정의할 때 { } 앞에 =를 넣으면 이 블록이 익명 함수를 기술하는 람다로 해석되기 때문에 원하는 결과를 얻을 수 없다.
※식이 본문인 함수 안에서 return 문은 금지
2. 위치 기반 인자와 이름 붙은 인자
위치 기반 인자
기본적으로 함수 호출 인자는 순서대로 파라미터에 전달된다.
함수 호출의 인자를 순서대로 파라미터에 전달하는 방식으로 첫 번째 인자는 첫 번째 파라미터, 두 번째 인자는 두 번째 파라미터라는 식이다.
이름붙은 인자
위치가 아니라 파라미터의 이름을 명시함으로써 인자를 전달하는 방식이다.
fun rectangleArea(w: Int, h: Int) {
return w * h
}
fun main() {
rectangleArea(w=10, h=3)
rectangleArea(h=4, w=3)
}
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 exampleFunction(a: Int, b: Int = 10, c: Int) { ... }
여기서 b는 디폴트 파라미터다. 이 함수를 b의 디폴트 값을 사용하면서 호출하고자 한다면, c에 대한 값을 명시적으로 지정해야 한다.
예를 들어
exampleFunction(a = 5, c = 15)
이렇게 이름 붙은 인자를 사용하면, 디폴트 파라미터 b는 그 디폴트 값을 사용하게 되고, a와 c에 대해서는 명시적인 값을 제공한다.
디폴트 파라미터 뒤에 디폴트가 지정되지 않은 파라미터가 있는 경우, 디폴트 파라미터를 지정하지 않고 이 함수를 호출하는 유일한 방법은 이름 붙은 인자를 사용하는 것뿐이다.
이러한 이유로 디폴트 파라미터를 사용하려면 함수 인자 목록 뒤쪽에 몰아두는 쪽이 좋다. 이렇게 하면 함수를 호출할 때 훨씬 더 유연하게 디폴트 파라미터를 활용할 수 있게 된다. 예를 들어, 모든 파라미터가 디폴트 값을 가지고 있고, 마지막 파라미터만 명시적으로 지정하려는 경우에 특히 유용하다.
4. varag
인자의 개수가 정해지지 않은 함수는 파라미터 정의 앞에 vararg 변경자를 붙이면 된다.
fun printSorted(vararg items: Int) { // items는 IntArray이다.
items.sort()
println(items.contentToString())
}
fun main() {
printSorted(6, 2, 10 ,1)
}
함수 내부에서는 파라미터를 적절한 배열 타입으로 사용할 수 있다.
예를 들어 printSorted( ) 내부에서 items는 IntArray이다.
또한, 스프레드 연산자인 *를 사용하면 배열을 가변 인자 대신 넘길 수 있다.
둘 이상을 vararg 파라미터로 선언하는 것은 금지된다. 하지만 vararg 파라미터에 콤마로 분리한 여러 인자와 스프레드를 섞어서 전달하는 것은 괜찮다. vararg 또한 디폴트와 마찬가지로 파라미터 목록의 맨 뒤에 위치시키는 것이 좋다.
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 파라미터는 이름 붙은 인자로 전달할 수 없다.
}
스프레드는 배열을 복사한다는 점에 유의해야 한다. 따라서 파라미터 배열의 내용을 바꿔도 원본 원소에는 영향을 미치지 않는다.
5. 함수의 영역과 가시성
코틀린 함수는 정의된 위치에 따라 세 가지로 구분됩니다.
함수의 영역
1. 최상위 함수
- 파일의 최상위 레벨, 즉 어떠한 클래스나 객체 내부가 아닌 파일에 직접 선언된다.
- 같은 파일 내부 또는 다른 파일에서도 호출될 수 있다 (가시성에 따라 달라짐).
2. 멤버함수
- 클래스나 객체 내부에 선언된 함수
- 해당 클래스의 인스턴스나 객체를 통해서만 호출될 수 있으며, 클래스의 다른 멤버에 접근할 수 있다.
3. 지역함수
- 다른 함수의 내부에 선언된 함수
- 선언된 함수의 영역 내에서만 사용할 수 있으며, 외부에서는 접근할 수 없다.
- 지역 함수는 자신을 둘러싼 함수의 지역 변수에 접근할 수 있다.
가시성 변경자 (Visibility Modifiers)
private, internal과 같은 가시성 변경자(visibility modifier) 키워드를 붙이면 함수가 쓰일 수 있는 위치를 제한할 수 있다.
-public: 함수가 정의된 파일 내부뿐 아니라 프로젝트 어디에서나 쓰일 수 있다. (디폴트)
-internal: 함수가 적용된 모듈 내부에서만 함수를 사용할 수 있게 제한한다.
-private: 함수가 정의된 파일 안에서만 해당 함수를 볼 수 있다.
지역 변수처럼 함수 내부에 지역 함수를 정의할 수 있으며, 이 함수의 영역은 함수를 감싸는 블록으로 한정된다
fun main() {
fun myReadInt() = readLine()!!.toInt()
println(myReadInt())
}
fun readIntPair() = intArrayOf(myReadInt(), myReadInt())
// error: unresolved referce: myReadInt
지역 함수는 자신을 둘러싼 함수, 블록에 선언된 변수나 함수에 접근할 수 있다. 이런 변수에는 지역 함수를 둘러싸고 있는 함수의 파라미터도 포함된다.
※ 지역 함수와 변수에는 가시성 변경자를 붙일 수 없다.
6. 패키지와 임포트
임포트 디렉티브 사용하기
코틀린에서는 서로 다른 패키지에 있는 일부 선언의 이름이 똑같다면 구분 할 수 있게 임포트 별명(alias)으로 사용할 수 있다.
import app.util.foo.readInt as fooReadInt
import app.util.bar.readInt as barReadInt
fun main() {
val n = fooReadInt()
val m = barReadInt()
}
7. 조건문
if문
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
자바 vs 코틀린
코틀린은 자바의 삼항연산자가 없지만 if를 식으로 쓸 수 있다.
범위, 진행, 연산
코틀린은 순서가 정해진 값 사이의 수열을 표현하는 몇 가지 타입을 제공한다.
범위
for 루프로 어떤 수 범위를 반복해야 할 때는 이런 타입이 유용하다. 이런 타입을 범위(range)라고 부른다.
1. .. 연산자
val chars = 'a'..'h' // 'a'부터 'h'까지의 모든 문자
val twoDigits = 10..99 // 10부터 99까지의 모든 수
val zero20ne = 0.0..1.0 // 0부터 1까지의 모든 부동소수점 수
.. 연산에 의해 만들어지는 범위는 닫혀 있습니다. 즉, 시작과 끝값이 범위에 포함된다.
2. until 연산자
val twoDigits = 10 until 100 // 10..99
반만 닫힌 연산자, 정수 타입에 대해서만 쓸 수 있고 끝 값보다 1 작은 값까지 들어있는 범위를 나타낸다.
3. in 연산자
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: 끝 값이 시작 값보다 더 작으면 빈 범위가 된다.
진행
진행은 정해진 간격(step)만큼 떨어져 있는 정수나 Char 값들로 이뤄진 시퀀스를 말란다.
println(5 in 10 downTo 1) //true
println(5 in 1 downTo 10) // false: 빈 진행임
위와 같이 downTo 연산자로 아래로 내려가는 진행을 만들 수 있다.
1..10 step 3 // 1,4,7,10
15 downTo 9 step 2 //15,13,11,9
그리고 step 연산자로 진행의 간격을 지정할 수 있으며, 진행의 간격은 양수여야 한다.
until, downTo, step은 다른 이름 붙은 중위 연산자(and, or 등)와 우선순위가 같다.
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 문은 when 키워드 다음에 블록이 온다.
- 블록 안에는 조건 -> 문 형태로 된 여러 개의 가지와 else-> 문 형태로 된 한 가지가 있을 수 있다.
- when문도 if처럼 식을 쓸 수 있고, 이. 경우 else 가지를 반드시 포함시켜야 한다.
자바 vs 코틀린
코틀린의 when에서는 임의의 조건을 검사할 수 있지만, 자바의 switch는 주어진 식의 여러 가지 값 중 하나만 선택할 수 있다.
추가로 자바의 switch는 풀스루를 지원하지만 코틀린의 when은 조건을 만족하는 가지만 실행하고 절대 풀스루 하지 않는다.
루프
fun main(){
val a = IntArray(10) {it * it}
val sum = 0
for(x in a) {
sum += x
}
println("Sum: $sum")
}
일반 변수와는 달리 루프 변수에는 val 이나 var 을 붙이지 않으며 루프 변수는 자동으로 불변 값이 된다.
for 루프와 이터러블
코틀린의 for 루프는 컬렉션과 비슷하게 여러 값이 들어있을 수 있는 값에 대한 루프를 수행할 수 있다.
val a = IntArray(10) { it*it } // 0, 1, 4, 9, 16, ..
var sum = 0
for (x in a) { // 배열 루프
sum += x
}
println("Sum: $sum") // Sum : 285
루프는 다음 세 부분으로 이뤄진다.
- 이터레이션 대상을 담을 변수 정의(x)
- 이터레이션에 사용할 값들이 담겨 있는 컨테이너를 계산하기 위한 식(a)
- 루프 몸통에 해당하는 문({ sum += x }) 이터레이션 시 이 몸통이 실행된다.
코틀린의 for 루프는 Iterable 인스턴스에 대한 루프를 간편하게 작성하도록 해주는 자바 for-each 루프와 꽤 비슷하다. 배열, 리스트, 집합은 Iterable에 속하며, 사용자가 정의한 타입도 Iterable을 상속할 수 있다. 하지만 코틀린에는 선언, 초기화, 검사 및 갱신으로 구성되는 일반 자바 for 루프에 해당하는 언어 구조가 없다.
// 수 범위에 대한 일반적인 루프 수행
val a = IntArray(10) { it*it } // 0, 1, 4, 9, 16, ...
for (i in 0..a.lastIndex) { // 0, 1, 2, 3, ...
if (i % 2 == 0) { // 0, 2, 4, 6, ...
a[i] *= 2
}
}
이 루프 간격을 사용한 진행으로 더 간편하게 작성할 수 있다.
for (i in 0..a.lastIndex step 2) {
a[i] *= 2
}
문자열과 배열에는 원소나 문자의 인덱스 범위를 제공하는 indices라는 프로퍼티가 들어있다.
val a = IntArray(10) { it*it }
for (i in a.indices step 2) {
a[i] *= 2
}
자바 vs 코틀린
자바의 전통적인 for 루프는 초기화, 조건 검사, 값 갱신을 한 줄에 표현한다.
예를 들어
for (int i = 0; i < 10; i++) {
// 반복할 코드
}
반면에 코틀린에서는 이러한 전통적인 for 루프 구조를 직접적으로 제공하지 않습니다. 대신, 코틀린은 범위를 사용한 for 루프를 제공하여 유사한 작업을 수행합니다
for (i in 0 until 10) {
// 반복할 코드
}
또한 자바에서 문자열의 각 문자에 대해 루프를 실행하려면 문자열의 길이를 기반으로 for 루프를 사용해야 합니다.
예를 들어
String str = "Hello";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
System.out.println(c);
}
코틀린에서는 문자열에 대한 반복이 더 간단하고 직관적입니다. for 루프를 사용하여 문자열을 직접 순회할 수 있기 때문입니다.
val str = "Hello"
for (c in str) {
println(c)
}
내포된 루프와 레이블
라벨명@ 사용
코틀린의 모든 표현식은 @을 이용하여 label(라벨, 레이블)될 수 있다. 이는 해당 표현식에 이름을 붙여준다고 생각하면 된다. 따라서 break와 continue 시 label을 사용하여 가장 가까운 루프 말고 원하는 루프의 흐름을 제어할 수 있다.
fun indexOf(subarray: IntArray, array: IntArray): Int {
outerLoop@ for (i in array.indices) { // 외부 루프
innerLoop@ for (j in subarray.indices) { // 내부 루프
// 만약 'subarray'의 현재 요소가 'array'의 현재 탐색 위치에 해당하는 요소와 다르면
if (subarray[j] != array[i + j]) {
continue@outerLoop // 외부 루프의 다음 반복으로 넘어간다.
}
}
// 내부 루프를 벗어난 경우
return i
}
// 외부 루프를 벗어난 경우
return -1
}
꼬리 재귀 함수
코틀린은 꼬리 재귀 함수에 대한 최적화 컴파일을 지원한다.
코틀린에서는 함수에 tailrec을 붙이면 컴파일러가 재귀 함수를 비재귀적인 코드로 자동 변환해준다.
재귀 함수는 일반적으로 비재귀 루프 보다 성능 차원에서 약간의 부가 비용이 발생하고 스택 오버플로우가 발생할 가능성이 있기 때문에 tailrec 키워드를 사용하여 재귀 함수의 간결함과 비재귀 루프의 성능만을 취할 수 있다.
// 꼬리 재귀로 구현한 팩토리얼 함수
tailrec fun factorial(n: Int, acc: Int = 1): Int {
return if (n <= 0) {
acc
} else {
factorial(n - 1, n * acc) // 꼬리 재귀 호출
}
}
fun main() {
println("factorial(10) : ${factorial(10)}" ) // factorial(10) 호출
}
이런 재귀 함수를 꼬리 재귀로 변환을 적용하려면 함수가 재귀 호출 다음에 아무 동작도 수행하지 말아야 한다.
함수에 tailrec을 붙였는데 꼬리 재귀가 아니라는 사실을 컴파일러가 발견하면, 컴파일러는 경고를 표시하고 함수를 일반적인 재귀 함수로 컴파일한다.
8. 예외처리
코틀린의 예외처리는 자바와 아주 비슷하다.
예외던지기
오류조건을 신호로 보내려면 자바와 마찬가지로 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
}
자바 vs 코틀린
자바와 달리 코틀린에서는 클래스 인스턴스를 생성, 예외처리 할 때 new 같은 특별한 구문을 사용하지 않아도 된다. 코틀린에서는 생성자호출도 일반 함수 호출과 똑같이 생겼다.
try 문으로 예외처리 하기
코틀린에서 예외를 처리할 때는 기본적으로 자바와 똑같은 문법의 try문을 사용한다.
import java.lang.NumberFormatException
fun readInt(default: Int): Int {
try {
return readLine()!!.toInt()
} catch (e: Exception) {
return 0
} catch (e: NumberFormatException) {
return default // 죽은 코드
}
}
자바 vs 코틀린
자바와 코틀린의 try문에서 가장 크게 다른 점은 코틀린 try가 식이라는 것이다. 이 식의 값은 try 블록의 값이거나 예외를 처리한 catch 블록의 값이 된다.
또한 자바와 달리 코틀린에서는 검사 예외와 비검사 예외를 구분하지 않습니다.
import java.lang.NumberFormatException
fun readInt(default: Int) = try {
readLine()!!.toInt()
} catch (e: NumberFormatException) {
default
}
try문의 다른 형태는 finally 블록을 사용하는 것이다.
finally
import java.lang.NumberFormatException
fun readInt(default: Int) = try {
readLine()!!.toInt()
} finally {
println("Error")
}
finally 블록은 try 블록을 떠나기 전에 프로그램이 어떤 일을 수행하도록 만들어준다. 이는 try 블록 앞이나 내부에서 할당한 자원을 해제할 때 유용하다. try 블록을 식으로 사용할 경우, finally 블록의 값은 전체 try 블록의 값에 영향을 미치지 못한다.