서른 번째, 함수입니다.
Kotlin 함수는 fun 키워드를 사용하여 선언합니다.
fun double(x: Int): Int {
return 2 * x
}
함수 사용법
함수는 표준적인 접근 방법으로 호출됩니다.
val result = double(2)
멤버 함수는 마침표 표기법(dot notation)을 사용합니다.
Stream().read() // Stream 클래스의 인스턴스를 생성하고 read() 호출
※ (익히 알고 있는 것처럼) 클래스나 객체 멤버에 속한 함수는 멤버 함수나 메소드로 불립니다. 이 연재 전체에 걸쳐서 이 용어들은 혼용되고 있습니다.
매개변수
함수의 매개변수는 파스칼 표기법 - 이름: 타입으로 정의됩니다. 매개변수는 콤마로 구분되고, 각각은 명시적으로 타입이 지정돼야 합니다.
fun powerOf(number: Int, exponent: Int): Int { /*...*/ }
함수 매개변수를 선언 할 때 후행 콤마(trailing comma)를 사용할 수 있습니다.
fun powerOf(
number: Int,
exponent: Int, // trailing comma
) { /*...*/ }
매개변수(parameter), 인수(argument)
이 두 단어는 혼용되기도 하지만, 명확히 구분할 필요할 있습니다. 이 연재에서도 그렇고, 공식 문서에서도 구분하여 사용하고 있습니다.
(공식 문서의 경우 간혹 혼용하는 경우도 있습니다)
매개변수는 함수의 시그니처 부분에 정의되는 것들로 함수 호출시 넘어오는 값들에 대해(받아) 함수 내부에서 사용되는 변수입니다.
인수(또는 인자)는 함수를 호출할 때, 함수의 각 매개변수에 대응하게 넘기는 것입니다. 에를 들어 다음과 같은 코드가 있다고 할 때
fun foo(a: String, b: String = "default") {
println(a)
println(b)
}
val bar = "argument"
foo(bar)
foo 시그니처의 a, b는 매개변수이고, foo() 호출시 넘기 bar는 인수입니다.
b: String = "default" 에서 "default" b에 대응하는 인수가 넘어오지 않았을 때 사용되는 값이므로 기본 인수입니다.
기본 인수 (Default arguments)
함수의 매개변수는 기본 값을 가질 수 있습니다. 이 기본값은 매개변수에 해당하는 인수가 생략됐을 때 사용됩니다. 기본 인수는 함수의 오버로드 수를 줄여 줍니다.
※ 기본 인수(또는 기본 값)가 지정된 매개변수를 기본 매개변수(default parameter) 또는 기본값 매개변수라고 부릅니다.
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /*...*/ }
/* 위의 함수는 다음과 같은 형태들로 호출할 수 있기 있기 때문에, 오버로드를 줄여 줍니다.
read(byteArrayOf())
read(byteArrayOf(), 10)
read(byteArrayOf(), 10, 0)
*/
기본값은 타입에 = 를 붙여 지정합니다.
오버라이딩 메소드는 항상 기반 메소드의 매개변수 기본 값을 사용할 수 있습니다. 기본 매개변수 값을 가지고 있는 메소드를 오버라이딩 할 때, 기본 매개변수 값은 시그니처에서 생략해야 합니다.
open class A {
open fun foo(i: Int = 10) { /*...*/ }
}
class B : A() {
override fun foo(i: Int) {
println(i)
}
}
fun main() {
B().foo()
}
/* 결과
10
*/
기본 매개변수가 기본값이 없는 매개변수 앞에 오는 경우에는 함수를 이름 지정 인수(named arguments)와 함께 호출할 때만 기본값을 사용할 수 있습니다(이름 지정된 인수는 아래쪽에서 설명합니다).
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // 기본 값 bar = 0 이 사용됩니다.
기본 매개변수 들에 오는 마지막 기본 인수가 람다인 경우에는 람다를 이름 지정 인수로 전달할 수도 있고, 괄호 바깥쪽에다가 넘길 수도 있습니다.
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /*...*/ }
foo(1) { println("hello") } // 기본값 baz = 1 사용
foo(qux = { println("hello") }) // 기본값 bar = 0, baz = 1 사용
foo { println("hello") } // 기본값 bar = 0, baz = 1 사용
이름 지정 인수 (Named arguments)
※ 네임드 아규먼트, 명명된 인수, 이름 있는 인수 등등 우리 말로는 다양하게 불릴거 같습니다. 여기서는 이름 지정 인수로 부르겠습니다.
함수를 호출할 때 인수의 이름을 지정할 수 있습니다(인수에 매개변수 이름을 지정하는 것입니다). 인수에 이름을 지정하는 것은 많은 매개변수를 가진 함수를 호출할 때나 특별히 boolen이나 null 같은 경우로 인수와 값을 연관시키기 어려울 때 도움이 될 수 있습니다.
함수 호출에서 이름 지정 인수를 사용할 때는 매개 변수의 순서와 다르게 마음대로 변경할 수 있습니다. 기본값을 사용하고 싶은 경우에는 단지 해당 인수를 제외하면 됩니다.
4개의 기본 인수 값을 갖는 다음과 같은 reformat() 함수를 살펴보겠습니다.
fun reformat(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
이 함수를 호출할 때 모든 인수에 이름을 지정할 필요는 없습니다.
reformat(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)
다음과 같이 기본값을 가진 것들은 모두 건너뛸 수 있습니다.
reformat("This is a long String!")
또한, 모두 생략하지 않고, 기본값이 있는 특정 인수들만 건너뛸 수 있습니다. 하지만, 처음 생략하는 인수 뒤로는 모두 이름을 지정해야 합니다.
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
인수의 수가 정해지지 않은 가변 인수(vararg)를 확산 연산자와 함께 이름을 지정하여 전달할 수 있습니다(관련된 내용은 밑에서 좀 더 자세히 설명합니다).
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
JVM에서 Java 함수를 호출할 때는 이름 지정 인수 구문을 사용할 수 없습니다. 왜냐하면, Java의 바이트 코드는 함수 매개변수의 이름을 항상 보관하는 것은 아니기 때문입니다.
Unit 반환 함수
함수가 (쓸모 있는) 값을 반환하지 않으면, 이 함수의 반환 타입은 Unit입니다. Unit는 오직 하나의 값 Unit만을 가지는 타입입니다. 이 값은 명시적으로 반환될 필요가 없습니다.
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` 을 명시적으로 하거나 생략할 수 있습니다.
}
Unit 반환 타입 선언도 선택 사항입니다. 위의 코드는 다음과 동일합니다.
fun printHello(name: String?) { ... }
단일 표현식 함수
함수의 몸체가 단일 표현식으로 구성될 때, 중괄호는 생략할 수 있으며 몸체는 = 기호 다음에 지정할 수 있습니다.
fun double(x: Int): Int = x * 2
컴파일러가 추론할 수 있는 경우에는 명시적인 반환 타입 지정은 선택 사항입니다.
fun double(x: Int) = x * 2
명시적 반환 타입
블록 몸체를 가지고 있는 함수는 Unit을 반환하는 것이 아니라면, 반드시 반환 타입을 명시적으로 표시해야 합니다(Unit을 반환할 때는 위에서 설명한 것처럼 반환 타입 지정은 선택 사항입니다).
블록 몸체를 가진 함수는 복잡한 흐름 제어를 가질 수 있고, 반환 타입이 코드를 읽는 사람(때때로 컴파일러에게도)에게 명확하지 않을 수 있기 때문에, Kotlin은 블록 몸체를 가진 함수의 반환 타입을 추론하지 않습니다.
가변 인수(variable numbers of arguments, vararg)
함수의 (보통 마지막) 매개변수에 vararg 수정자를 지정할 수 있습니다.
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts는 배열입니다.
result.add(t)
return result
}
이런 경우에는, 함수에 가변 인수를 넘길 수 있습니다.
// asList에 넘길 수 있는 인수의 수가 정해지 있지 않습니다.
val list = asList(1, 2, 3)
함수 내부에서 타입이 T인 vararg 매개변수는 T의 배열로 보입니다. 그래서, 위의 예에서 ts의 타입은 Array<out T>입니다.
오직 하나의 매개변수만 vararg로 지정할 수 있습니다. vararg 매개변수가 매개 변수 목록이 마지막이 아니라면, 뒤에 이어지는 매개변수들에 대해서는 이름 지정 인수 구문으로 인수를 전달하거나 매개변수가 함수 타입인 경우에는 괄호 바깥으로 람다를 전달해야 합니다.
가변 인수를 가진 함수를 호출할 때는 asList(1, 2, 3)처럼 각 인수를 개별적으로 전달할 수 있습니다. 이미 배열이 있고, 배열의 내용을 함수의 인자로 전달하고 싶을 때는 (배열 앞에 *를 붙이는) 확산 연산자를 사용합니다.
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
원시 타입 배열을 가변 인수로 전달하고 싶으면, toTypedArray()를 사용하여 정규 배열로 변환해야 합니다.
val a = intArrayOf(1, 2, 3) // IntArray는 원시 타입 배열입니다.
val list = asList(-1, 0, *a.toTypedArray(), 4)
중위 표기법
infix 키워드로 표기된 함수는 호출을 위한 마침표와 괄호를 생략한 중위 표기법을 사용할 수 있습니다. 중위 함수는 다음과 같은 요구 조건을 충족시켜야 합니다.
- 반드시 멤버 함수나 확장 함수여야 합니다.
- 매개변수가 하나여야 합니다.
- 매개변수는 가변 인수여서는 안 되고, 기본 값도 갖지 않아야 합니다.
infix fun Int.shl(x: Int): Int { ... }
// 중위 표기법을 사용하여 호출
1 shl 2
// 위 코드는 다음과 동일합니다.
1.shl(2)
중위 함수는 산술 연산자, 타입 캐스트, rangeTo 연산자보다 우선 순위가 낮습니다. 예를 들어,
- 1 shl 2 + 3 은 1 shl (2 + 3)과 같습니다.
- 0 until n * 2 는 0 until (n * 2)와 같습니다.
- xs union ys as Set<*> 은 xs union (ys as Set<*>)과 같습니다.
한 편, 중위 함수 호출은 불리언 연산자 &&와 ||, is 나 in 검사 그리고 다른 연산자보다는 우선 순위가 높습니다. 예를 들어,
- a && b xor c 는 a && (b xor c)와 같습니다.
- a xor b in c 는 (a xor b) in c와 같습니다.
중위 함수는 항상 수신자(receiver)와 인수 지정이 필요하다는 것을 주의해야 합니다. 현재 수신자를 대상으로 중위 표기법을 사용하여 메소드를 호출할 때는 명시적으로 this를 사용합니다. 이는 모호하지 않은 파싱을 보장해 줍니다.
class MyStringCollection {
infix fun add(s: String) { /*...*/ }
fun build() {
this add "abc" // 맞음
add("abc") // 맞음
//add "abc" // 틀림: 수신자를 명시적으로 지정해야 함
}
}
함수 유효 범위 (Function scope)
Kotlin은 파일의 최상위 수준에 함수를 정의할 수 있는데, 이 말은 Java, C#, Scala 같이 함수를 담기 위한 클래스를 만들지 않아도 된다는 뜻입니다(Scala의 경우는 3부터 최상위 수준 함수를 사용할 수 있습니다). 최상위 수준 함수에 더하여, Kotlin에서는 멤버 함수나 확장 함수도 지역적으로 선언할 수 있습니다.
지역 함수
Kotlin은 지역 함수를 지원합니다. 그래서, 함수 안에 함수가 있을 수 있습니다.
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}
지역 함수는 바깥 함수(클로저)의 지역 변수에 접근할 수 있습니다. 위의 예에서, visited는 다음과 같이 지역 변수가 될 수 있습니다. 그리고, 내부 함수 dfs 내에서 접근할 수 있습니다.
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}
멤버 함수
멤버 함수는 클래스나 객체 안에 정의된 함수입니다.
class Sample {
fun foo() { print("Foo") }
}
멤버 함수는 마침표 표기법으로 호출합니다.
Sample().foo() // Sample의 인스턴스를 만들고 foo()를 호출합니다.
클래스와 멤버 오버라이딩에 대한 자세한 정보는 클래스와 상속을 참고하시기 바랍니다.
제네릭 함수
함수는 제네릭 매개변수를 가질 수 있습니다. 이는 함수 이름 앞에 꺽쇠 괄호를 사용하여 표시합니다.
fun <T> singletonList(item: T): List<T> { /*...*/ }
제네릭 함수의 보다 자세한 내용은 제네릭을 살펴보시기 바랍니다.
꼬리 재귀 함수 (Tail recursive functions)
Kotlin은 함수형 프로그래밍에서 꼬리 재귀로 알려진 스타일을 지원합니다. 보통 루프를 사용하는 알고리즘에 대해서 스택 오버플로우 위험 없이 재귀 함수를 사용할 수 있습니다. 함수가 tailrec 수정자로 지정되고 형식적인 요구사항에 부합되는 경우, 컴파일러는 재귀 호출을 빠르고 효율적인 루프 버전으로 최적화합니다.
val eps = 1E-10 // "good enough", could be 10^-15
tailrec fun findFixPoint(x: Double = 1.0): Double =
if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
이 코드는 코사인의 부동점을 계산하는데, 이는 수학적 상수입니다. 1.0부터 시작해서 결과가 더이상 변하지 않을때까지 Math.cos를 반복해서 호출하고 eps에 지정한 정밀도에 따라 0.7390851332151611 이라는 결과를 산출합니다. 이 코드는 다음과 같은 보다 전통적인 스타일의 코드와 동일합니다.
val eps = 1E-10 // "good enough", could be 10^-15
private fun findFixPoint(): Double {
var x = 1.0
while (true) {
val y = Math.cos(x)
if (Math.abs(x - y) < eps) return x
x = Math.cos(x)
}
}
tailrec 수정자 붙이기에 적합하려면, 함수는 실행하는 마지막에서 자신을 호출해야 합니다. 재귀 호출 부분 이후로 코드가 있거나, 재귀 호출이 try/catch/finally 블록 내에 있거나, open 함수에 있는 경우에는 꼬리 재귀를 적용할 수 없습니다. 현재, 꼬리 재귀는 Kotlin/JVM과 Kotlin/Native에서 지원됩니다.
※ 꼬리 재귀 최적화를 지원하는 언어들을 사용하는 상황에서 재귀 호출을 해야 하는 경우에는 가능하면 요구 조건에 맞춰서 루프로 최적화 될 수 있게 하는게 성능면(함수 호출로 인한 호출 스택 불필요 등)이나 안전성(스택 오버플로우 방지)면에서 좋습니다.
'Kotlin' 카테고리의 다른 글
공식 문서로 배우는 코틀린 - 32. Inline functions (0) | 2024.03.14 |
---|---|
공식 문서로 배우는 코틀린 - 31. Higher-order functions and lambdas (0) | 2024.03.13 |
공식 문서로 배우는 코틀린 - 29. Type aliases (0) | 2024.03.13 |
공식 문서로 배우는 코틀린 - 28. Delegated properties (0) | 2024.03.12 |
공식 문서로 배우는 코틀린 - 27. Delegation (0) | 2024.03.12 |