return 키워드와 블록을 만드는 중괄호({})를 생략하는 단일 식 형태의 함수로, 컴파일러가 표현식의 타입을 추론할 수 있기 때문에 명시적인 반환 타입을 생략해도 된다.
fun circleArea(radius: Double) = { PI * radius * radius }
로직이 단순하고 결과를 바로 반환하는 경우 식이 본문인 함수를 쓰는게 더 좋다.
블록이 본문인 함수는 복잡한 연산이나 여러 단계의 처리가 필요한 경우에 더 적절하다.
미리 정해진 디폴트 값을 사용할 수 있게 하고자 메서드를 오버로딩해야 하는 경우 디폴트 파라미터를 사용하고, 파라미터의 개수나 타입에 따라 다른 로직을 수행해야 하는 경우 오버로딩을 사용하는 것이 좋다.
fun plus(a: Int, b: Int) = a + b
fun c(a: Int, b: Int = plus(a, 4), c: Int = plus(a + b)) = a + b + c
장점 : 인자의 순서를 바꿀 수 있다.
fun plus(a: Int, b: Int) = a + b
fun main() {
val a = 3
val b = 7
print(plus(b, a)) // 10
}
단점 : 위치 기반 인자와 이름 붙은 인자를 함께 사용한 경우 원래 인자가 들어가야 할 위치에 이름 붙은 인자를 지정해야 정상 처리되며, 그렇지 않은 경우 위치 기반 인자의 타입이 어긋나거나 이미 할당된 인자를 재할당하기 때문에 컴파일 오류가 발생한다.
파라미터 정의 앞에 vararg
변경자(modifier)를 붙여 배열 타입으로 넘기거나 스프레드(spread) 연산자인 *
를 사용하여 배열을 가변 인자 대신 넘길 수 있다.
자바에서는 vararg 파라미터가 항상 마지막 파라미터이지만, 코틀린에서는 vararg 파라미터가 함수 파라미터 중 어디에나 올 수 있다. 다만, vararg 파라미터 이후의 파라미터는 이름 붙은 인자로만 전달할 수 있다.
fun printSorted(vararg items: Int, prefix: String = "") { }
fun main() {
printSorted(6, 2, 10, 1, "!") // Error
printSorted(6, 2, 10, 1, prefix = "!") // Success
val numbers = intArrayOf(6, 2, 10, 1)
printSorted(*numbers) // Success
}
fun printSorted(vararg items: Int, block: () -> Unit = {}) { }
fun main() {
printSorted(1, 2, 3 , 5) { /*....*/ } // Success
}
Unit은 함수가 의미 없는 반환값을 돌려줄 때 사용한다.
Nothing은 프로그램의 순차적 제어 흐름이 그 부분에서 끝나되 어떤 잘 정의된 값에 도달하지 못할 때 사용한다.
Unit은 자바 void에 해당하는 코틀린 타입으로, Nothing과 달리 한 가지 인스턴스가 존재하는데, 이 인스턴스는 보통 유용한 값이 없다는 사실을 표현한다. 반면 Nothing은 아예 값이 없다는 사실을 표현한다.
Nothing 타입은 return의 경우 이 문장을 둘러싼 함수가 끝난다. 모든 코틀린 타입의 하위 타입으로 간주되기 때문에 식이 필요한 위치에 return을 사용해도 타입 오류가 발생하지 않는다.
return e
의 e 값은 return 식의 값이 아니라 함수의 반환값이라는 점에 유의하자. return 식 자체는 아무 값이 없고 Nothing 타입에 속한다.Unit 타입은 함수에서 결과가 항상 Unit으로 동일하기 때문에 결과를 지정하는 return 문을 쓸 필요가 없다. 하지만 함수 본문의 끝에 도달하기 전에 함수 실행을 마치려면 return 문을 사용해 함수를 끝내야 한다.
해당 함수가 정수 0을 반환하는 것으로, 해당 함수의 반환 타입이 Int일 때 사용된다. 보통 함수의 반환 유형을 명시적으로 선언하고, 반환값이 필요 없는 경우에는 Unit을 사용한다. 따라서 return 0은 주로 함수가 특정 결과를 반환하는 상황에서 사용된다.
fun exampleFunction(): Int {
// 어떤 계산을 수행한 후, 결과가 0이면 성공적으로 완료되었음을 나타냄
return 0
}
fun main() {
val result = exampleFunction()
if (result == 0) {
println("함수가 성공적으로 실행되었습니다.")
} else {
println("함수 실행 중 오류가 발생했습니다.")
}
}
함수의 성공 또는 실패를 나타내는 값이 0 하나로 특정되어 있기 때문이다. 즉, 함수의 성공 여부를 판단하는 데 사용되는 값이 항상 0이라면, 매번 이 값을 확인하는 코드가 중복된 느낌을 줄 수 있다.
코틀린에서는 일반적으로 함수가 성공적으로 수행되면 Unit을 반환하거나, 실패한 경우 예외를 던지는 등의 방식을 사용한다. 이로써 성공 또는 실패를 판단하는 코드에서 중복을 최소화하고 가독성을 높일 수 있다.
fun exampleFunction(): Unit {
// 어떤 계산을 수행
// 성공적으로 완료되면 아무것도 반환하지 않음 (Unit)
}
fun main() {
try {
exampleFunction()
println("함수가 성공적으로 실행되었습니다.")
} catch (e: Exception) {
println("함수 실행 중 오류가 발생했습니다: $e")
}
}
반환값이 필요 없는 함수(반환 타입이 Unit인 경우)를 정의할 때 return 문을 생략할 수 있다.
특정 함수 내에 정의된 함수로, 자신을 둘러싼 함수의 변수(args)나 함수에 접근할 수 있다. 지역 함수와 변수는 가시성 변경자를 붙일 수 없다.
fun main(args: Array<String>) {
fun swap(i: Int, j: Int): String {
val chars = args[0].toCharArray()
val tmp = chars[i]
chars[i] = chars[j]
chars[j] = tmp
return chars.concatToString()
}
println(swap(0, args[0].lastIndex))
}
지역 함수를 둘러싼 영역의 변수나 파라미터 목록 등의 문맥을 포획해주는 특별한 클래스(익명 클래스, 람다 표현식)를 선언한다. 그래서 지역 함수를 호출할 때마다 이런 특별한 객체를 생성하는 부가 비용이 든다.
최상위 함수는 디폴트로 공개 가시성을 가지며, 함수가 정의된 파일 내부뿐 아니라 프로젝트 어디에서나 쓰일 수 있다. 반면에 최상위 함수를 비공개로 정의하면 함수가 정의된 파일 안에서만 해당 함수를 볼 수 있다.
맨 앞에 패키지 이름을 지정하면 파일에 있는 모든 최상위 선언(타입, 함수, 프로퍼티)을 지정한 패키지 내부에 넣을 수 있다. 패키지를 사용하지 않으면 컴파일러는 파일이 디폴트 최상위 패키지에 속한다고 가정한다. 디폴트 최상위 패키지는 이름이 없다.
package foo.bar.util
fun readInt(radix: Int = 10) = readLine()!!.toInt(radix)
// 패키지 계층에서 루트 패키지 바로 아래에 있는 경우
package numberUtil
fun readDouble() = readLine()!!.toDouble()
자바에서는 패키지 구조와 컴파일 대상 루트에 있는 소스 트리 디렉터리 구조가 같아야 한다. 둘의 경로가 다르면 컴파일 오류가 발생한다.
코틀린의 패키지 계층 구조는 소스 파일에 있는 패키지 디렉티브로부터 구성된 별도의 구조다. 즉 소스 파일 트리와 패키지 계층 구조가 다를 수도 있다.
임포트한 선언에 새 이름을 부여하는 것으로, 새 이름은 임포트 이렉티브가 있는 파일 전체 영역에서 유효하다. 또 다른 형태의 임포트로 어떤 영역에 속한 모든 선언을 한꺼번에 임포트할 수 있다. 전체 이름 뒤에 *
를 붙이면 된다.
자바에서는 임포트 별명을 줄 수 없기 때문에 이름이 같으면 풀패키지 밖에 명시할 수 없다.
import foo.readInt as fooReadInt
import bar.readInt as barReadInt
fun main() {
val n = fooReadInt()
val m = barReadInt()
}
코틀린에서는 정적 임포트 구문 없이 일반적인 임포트 디렉티브 구문을 사용해 클래스의 메소드나 속성을 임포트할 수 있다.
if 문/식은 불(boolean) 식의 결과에 따라 두 가지 대안 중 하나를 선택할 수 있다. 코틀린 if는 자바 if 문과 비슷한 문법을 제공하지만, 3항 연산자(조건 ? 참일 때식 : 거짓일 때식)가 없다. 대신에 if 문을 식으로 쓸 수 있다는 점이 이 단점을 대부분 상쇄해준다. if 문을 식으로 사용할 때는 양 가지가 모두 있어야 한다.
when 문은 코드에 쓰여져 있는 순서대로 조건을 검사해서 맨 처음으로 참으로 평가되는 조건을 찾고, 해당 조건에 대응하는 문을 실행한다. 만약 모든 조건이 거짓이라면 else 부분을 실행한다.
for (i in 0..99) or for (i in 0 until 100)
while 문은 어떤 조건이 참인 동안 루프를 실행하지만 루프 몸통을 실행하기 전에 조건을 먼저 검사한다. 이 경우 처음부터 조건이 거짓이면 루프 몸통이 한 번도 실행되지 않는다.
do-while 문은 루프 몸통을 실행한 다음에 조건을 검사하므로 루프 몸통이 최소 한 번은 실행된다.
break는 즉시 루프를 종료시키고, 실행 흐름이 루프 바로 다음 문으로 이동하게 만든다.
continue는 현재 루프 이터레이션(iteration)을 마치고 조건 검사로 바로 진행하게 만든다.
프로그램은 예외를 잡아내는 핸들러를 찾는다. 예외와 일치하는 핸들러가 있다면 예외 핸들러가 처리한다.
현재 함수 내부에서 핸들러를 찾을 수 없으면 함수 실행이 종료되고 함수가 스택에서 제거 된다. 그리고 호출한 쪽에서 핸들러 검색을 수행한다. 이런 경우를 호출자에게 전파했다고 말한다.
프로그램 진입접에 이를 때까지 예외를 잡아내지 못하면 현재 스레드가 종료된다.
자바와 달리 코틀린에서는 클래스 인스턴스를 생성(여기서는 예외)할 때 new 같은 특별한 구문을 사용하지 않는다. 자바에서는 도착할 수 없는 코드를 금지하지만, 코틀린은 허용한다. 자바와 달리 코틀린에서는 검사 예외(checked exception)와 비검사 예외(unchecked exception)을 구분하지 않는다.
코틀린에서는 try가 식으로 쓰일 수 있어 try 블록의 값이거나 예외를 처리한 catch 블록의 값이 된다.
명시적인 반환 타입은 먼저 반환 타입을 확정짓고, 그 타입에 맞춰 함수 블록을 개발할 수 있다.
암시적인 반환 타입은 함수 블록이 내가 원하는 타입으로 잘 반환하는지를 컴파일러의 입장을 들어볼 수 있다. 하지만 복잡성이 높아질수록 컴파일 속도가 엄청 느려지는 단점이 있다.
할당 식도 타입 추론이 똑같이 작동한다. 변수마다 타입을 명시해주는 것이 컴파일 속도가 빨라진다.
fun a(): Number = if(true) {
// 가정: 타입 추론을 적용했을 때
val a: {Comparable<*> & Number} = if (true) 3 else 7.0
4
} else {
6
}
디폴트 파라미터로 함수 오버로딩을 절대로 흉내낼 수 없는 부분은 인자가 다른 형을 갖는 것을 허용하지 않는다. 어떤 함수가 다양한 형의 인자를 받고 싶다면 함수 오버로딩 외에는 방법이 없다.
fun plus(a: Int, b: Long) = a + b // Error
fun c(a: Int, b: Int = plus(a, 4), c: Int = plus(a + b)) = a + b + c
장점 : 인자의 순서를 바꿀 수 있다.
"인자의 순서를 바꿀 수 있다"의 조건
(1) 인자가 너무 많을 때 (2) 기본값 인자가 많을 때 (3) vararg 뒤에 인자가 더 있을 때
fun plus(c: Int, a: Int = 3, b: Int = 4) = a + b
fun main() {
print(plus(5, b = 2, a = 1))
}
// Compose-style : 인자가 수백가지다.
style(
border = 10.px,
padding = 10.px,
...
)
fun main(args: List<String>) {
fun localFun(a: Int) {
a * 2
}
println("${localFun(11)}")
}
public final class MainKt {
public static void main(@NotNull String[] args) {
...
println(String.valueOf($fun$localFun$1.invoke(11)))
}
}
함수는 람다와 달리 리턴문을 사용할 수 있기 때문에, 예외 처리를 다룰 때(쉴드 패턴) 함수 내부를 구현하기 쉽다.
// 임의의 정수를 그에 대응하는 16진 숫자로 바꾸는 함수
fun hexDigit(n: Int): Char {
when {
n in 0..9 -> return '0' + n
n in 10..15 -> return 'A' + n - 10
else -> return '?'
}
}
fun readHexDigit() = when(val n = readLine()!!.toInt()) {
in 0..9 -> '0' + n
in 10...15 -> 'A' + n - 10
else -> '?'
}
fun main() {
val a = readLine()!!.toInt()
val b = readLine()!!.toInt()
println(5 in a..b)
}
반복문 안에서 for 문은 while 문보다 얼마나 반복할지를 명확히 알 수 있다.
바디가 루프 계획에 영향을 미치는 경우(동적 계획)는 for 문보다 while 문을 사용하는 것이 좋다.