1. 함수의 기본 구조
1.1 구조
우선 함수의 기본 구조를 보자 함수는 헤더(header)와 몸체(body)가 존재한다.
private fun pointStatus(name: String, score: Int) : String {
var result = when(score) {
100 -> "100점"
in 90..99 -> "90점"
in 80..89 -> "80점"
in 70..79 -> "70점"
else -> "70점 이하"
}
return "$name 님의 현재 점수는 $result 입니다."
}
1.2 헤더
함수의 첫번째 부분은 헤더다.
함수의 헤더는 다섯개 부분으로 구성된다.
가시성제한자, 함수키워드, 함수이름, 함수 매개변수, 함수 반환타입 이다.
- 가시성 제한자
- private
- 함수 키워드
- fun
- 함수 이름
- pointStatus
- 함수 매개변수
- (score: Int)
- 함수 반환타입
- : String
- 함수의 실행이 끝났을 때 반환되는 결과의 데이터 타입을 나타낸다.
- 아무것도 반환하지 않을때는 생략하거나 Unit, Void 를 이용할 수 있다.
1.3 몸체
함수 헤더 다음에는 중괄호 "{}" 를 사용해서 함수 몸체를 정의한다.
함수에서 실행되는 코드가 있는 곳이 몸체다.
몸체에는 반환될 데이터를 나타내는 return 문이 포함 될 수 있다.
{
var result = when(score) {
100 -> "100점"
in 90..99 -> "90점"
in 80..89 -> "80점"
in 70..79 -> "70점"
else -> "70점 이하"
}
return "$name 님의 현재 점수는 $result 입니다."
}
1.4 함수오버로딩
코틀린도 Java 와 같이 함수 오버로딩을 지원한다.
2. 함수 호출
2.1 기본
Java 의 함수 호출 방법과 동일하다
pointStatus("kyu", 10)
2.2 기본 인자
때로는 함수의 매개변수가 기본 인자 값을 가질 수 있다.
함수를 호출할때 null 을 전달하거나, 인자를 지정하지 않으면 기본인자 값이 사용된다.
private fun pointStatus(name: String = "kyu", score: Int = 69) : String {
var result = when(score) {
100 -> "100점"
in 90..99 -> "90점"
in 80..89 -> "80점"
in 70..79 -> "70점"
else -> "70점 이하"
}
return "$name 님의 현재 점수는 $result 입니다."
}
2.3 지명함수인자
지명함수인자(named function argument) 를 사용하여 원하는 순서대로 인자를 전달 할 수 있다.
private fun pointStatus(name: String, score: Int) : String {
var result = when(score) {
100 -> "100점"
in 90..99 -> "90점"
in 80..89 -> "80점"
in 70..79 -> "70점"
else -> "70점 이하"
}
return "$name 님의 현재 점수는 $result 입니다."
}
//지명 함수 인자를 사용하여 호출
pointStatus(score = 10, name = "kyu")
3. 단일 표현식 함수
단일 표현식을 갖는 함수에서는 헤더에 정의하는 반환타입, 몸체를 나타내는 중괄호,
몸체 내부의 return 문을 모두 생략 할 수 있다.
- 함수 몸체를 사용하는 대신에 대입 연산자인 = 다음에 표현식을 정의한다는 것을 알아두자.
- 두개 이상의 표현식을 실행하는 함수는 기본 방식으로 함수를 정의해야 한다.
private fun pointStatus(score: Int) =
when (score) {
100 -> "100점"
in 90..99 -> "90점"
in 80..89 -> "80점"
in 70..79 -> "70점"
else -> "70점 이하"
}
4. Unit 함수
4.1 기본 구조
모든 함수가 값을 반환하는 것은 아니다.
예를 들어 아래 함수를 보자.
private fun pointStatus(name: String, score: Int) =
println("$name 님의 현재 점수는 ${score} 입니다.")
코틀린 에서는 이런 함수를 Unit 함수라고 하며, 이 함수는 반환 타입이 Unit 이라는 뜻이다.
함수를 클릭하고 Ctrl + Shift + P 를 누르면 반환 타입 정보를 보여준다.
즉, 함수에서 return 키워드를 사용하지 않으면 그 함수의 반환 타입은 Unit 으로 간주된다.
4.2 Unit 이란?
Unit 은 무슨 타입일까?
Unit 은 함수이다.
코틀린 이전에 다른 많은 언어에서도 값을 반환하지 않는 함수를 나타내는 문제가 있었다.
그리고 일부 언어에서는 void 키워드를 채택하였다. 표면상으로는 이것이 그럴듯하게 보인다.
그러나 이 방법은 현대 언어의 중요한 기능인 제네릭(generic)을 처리하기 어렵다.
제네릭은 현대의 컴파일 언어에 커다란 유연성을 제공하는 기능이다.
제네릭 함수에서는 반드시 반환타입을 나타내야 한다.
그러나 void 키워드를 사용하는 일부 언어는 아무것도 반환하지 않는 제네릭 함수를 구현할 수 있는
마땅한 방법이 없다. void 는 타입이 아니며, '타입 정보가 없으므로 생략하라' 는 것이기 때문이다.
코틀린은 반환 타입 대신 Unit 을 지정하여 이 문제를 해결한다.
이것이 바로 코틀린에서 Unit 을 사용하는 이유다.
5. 익명 함수
5.1 익명 함수란?
이름이 없는 함수를 익명(anonymous) 함수라고 하며, 이 함수는 이름이 있는 함수와 비슷하지만 크게 2가지가 다르다.
아래와 같은 형태의 사용은 함수타입(function type) 과 함수인자(function argument) 를 사용하기 때문에 가능하다.
- 함수 정의 부분에 이름이 없다
- 주로 다른함수의 인자로 전달되거나 반환되는 형태로 사용된다는 것
코틀린 표준 라이브러리의 많은 함수 중 하나로 count 가 있다.
이 함수를 문자열에 대해 호출하면 문자개수를 출력 한다.
var numLetters = "southKorea".count()
println(numLetters)
//결과
10
그러나 특정 문자 개수 'o' 를 알고 싶다면 어떻게 해야 할까?
이때 다음과 같이 익명함수 {} 를 count 함수의 인자로 전달할 수 있다.
var numLetters = "southKorea".count({ letter ->
letter == 'o'
})
println(numLetters)
//결과
2
- {}
- 함수의 몸체를 정의하였다.
- letter ->
- 함수의 인자를 정의하였다.
다음과 같이 () 를 생략할 수 있다.
var numLetters = "southKorea".count { letter ->
letter == 'o'
}
println(numLetters)
//결과
2
it 키워드를 활용하여 인자 를 생략할 수 있다.
var numLetters = "southKorea".count{
it == 'o'
}
println(numLetters)
//결과
2
- it 키워드는 인자가 오직 1개 일때만 사용 할 수 있다.
5.2 익명 함수 정의하기
{} 를 사용하여 익명함수를 정의하였으며, () 를 사용하여 즉각 익명함수를 호출하였다.
println(
{
"southKorea".count {
it == 'o'
}
}()
)
//결과
2
5.3 함수 타입
익명함수도 타입을 가지며, 이것을 함수 타입 이라고 한다.
함수 타입의 변수들은 값으로 익명 함수를 저장한다.
그리고 다른 변수 처럼 익명 함수가 코드 어디든 전달 될 수 있다.
val resultFunction: () -> Int = {
"southKorea".count {
it == 'o'
}
}
println(resultFunction())
//결과
2
- 일반적인 변수 라면 val result: Int 로 정의 될 수 있지만 함수 타입은 val result: () -> Int 를 사용하여 변수가 저장하는 함수타입을 컴파일러에게 알려 준다.
- 일반적인 함수를 호출 할때 처럼 () 를 함수이름 뒤에 붙여 호출한다.
5.4 암시적 반환
위에서 정의 했던 함수를 보면 return 키워드가 없다는 것을 알 수 있다.
그러나 함수 타입에는 Int 를 반환하도록 지정되어있는데 컴파일 에러가 생기지 않았다
어떻게 된 것 일까?
이름이 있는 함수와 다르게 익명 함수는 데이터를 반환하기 위한 return 키워드가 필요 없다.
익명함수는 암시적으로 또는 자동으로 함수 정의의 마지막 코드 결과를 반환하기 때문이다.
또한, 익명 함수에서는 return 키워드가 금지되어 있다.
어떤 곳으로 복귀하여야 하는지 컴파일러가 알 수 없기 때문이다.
아래의 코드를 보자
val resultFunction: () -> Int = {
2
}
println(resultFunction())
//결과
2
5.5 함수 인자
이름이 있는 함수 처럼 익명함수는 어떤 타입의 인자도 받을 수 있다.
또한, 인자의 개수는 0 또는 1개 이상을 받을 수 있다.
익명함수의 매개변수 타입은 함수 타입 정의에 지정하며, 매개변수 이름은 함수 내부에 지정한다.
val resultFunction: (String, String) -> String = { name, message ->
"$name 님 $message"
}
println(resultFunction("kyu", "방문을 환영합니다."))
//결과
kyu 님 방문을 환영합니다.
- : (String, String)
- 익명함수가 String, String 2개의 인자를 받는다.
- 인자가 1개 일 경우 it 키워드를 사용 할 수 있다.
- -> String
- 익명 함수의 반환 타입은 String 이다.
- name, message ->
- 전달 받는 String, String 인자의 매개변수 이름을 name, keyword 로 지정한다.
5.6 타입 추론 지원
코틀린의 타입 추론(type interface) 규칙은 일반 함수와 마찬가지로 함수 타입에도 똑같이 적용된다.
즉, 변수가 선언될 익명 함수가 값으로 지정되면 해당 변수의 타입을 지정하지 않아도 된다.
예를 들어 인자가 없는 함수를 보자.
val resultFunction: () -> String = {
"kyu 님 방문을 환영합니다."
}
println(resultFunction())
//결과
kyu 님 방문을 환영합니다.
- 인자가 없이 String 을 반환하는 함수 타입 변수라는 걸 알 수 있다.
위 코드를 함수 타입 변수를 생략하여 선언하면 아래와 같은 코드가 된다.
val resultFunction = {
"kyu 님 방문을 환영합니다."
}
println(resultFunction())
//결과
kyu 님 방문을 환영합니다.
- : () -> String =
- 이 함수 타입은 반환 타입을 추론 할 수 있기 때문에 생략되어질 수 있다.
인자가 여러개인 함수도 마찬가지이다.
val resultFunction: (String, String) -> String = { name, message ->
"$name 님 $message"
}
println(resultFunction("kyu", "방문을 환영합니다."))
//결과
kyu 님 방문을 환영합니다.
아래 처럼 간략하게 정의 할 수 있다.
val resultFunction = { name: String, message: String ->
"$name 님 $message"
}
println(resultFunction("kyu", "방문을 환영합니다."))
//결과
kyu 님 방문을 환영합니다.
5.7 함수를 인자로 받는 함수
이제 부터는 익명 함수를 람다(lambda) 라고 하고, 익명 함수 정의를 람다 표현식(lambda expression)
또는 줄여서 람다식 이라고 할 것이다.
또한, 람다식의 반환 결과를 람다 결과(lambda result) 라고 할 것이다.
함수 매개변수는 함수를 포함해서 어떤 타입의 인자도 받을 수 있다.
함수 타입의 매개변수도 다른 타입의 매개변수처럼 정의된다.
즉, 함수 이름 다음에 나오는 괄호안에 매개변수를 나열하고 타입을 포함시킨다.
fun main(args: Array<String>) {
val resultFunction = { name: String, message: String ->
"$name 님 $message"
}
welcomeMessage("[시스템]: ", resultFunction)
}
fun welcomeMessage(label: String, resultFunction: (String, String) -> String){
println(label + resultFunction("kyu", "방문을 환영합니다."))
}
//결과
[시스템]: kyu 님 방문을 환영합니다.
또한 함수 인자도 단축문법으로 사용이 가능하다.
이를 람다를 전달한다 라고 정의 할 수 있다.
welcomeMessage 함수 내부의 코드는 변경 할 것이 없으며, 호출 코드만 수정하면 된다.
그리고 이제는 람다를 변수에 지정하지 않고, welcomeMessage 함수를 호출할때 직접 전달하므로
람다 매개변수들의 타입을 지정할 필요가 없다.
이런 단축 문법을 사용하면 더 깔끔하게 코드를 작성 할 수 있다.
fun main(args: Array<String>) {
welcomeMessage("[시스템]: "){ name: String, message: String ->
"$name 님 $message"
}
}
fun welcomeMessage(label: String, resultFunction: (String, String) -> String){
println(label + resultFunction("kyu", "방문을 환영합니다."))
}
//결과
[시스템]: kyu 님 방문을 환영합니다.
5.8 인라인 함수
유연성이 매우 좋은 프로그램을 작성할 수 있으므로 람다는 유용하게 사용될 수 있다.
그러나 그런 유연성에는 대가가 따른다.
람다를 정의하면 JVM 에서 객체로 생성된다.
또한 JVM 은 람다를 사용하는 모든 변수의 메모리 할당을 수행하므로 메모리가 많이 사용된다.
결국 람다는 성능에 영향을 줄 수 있는 메모리 부담을 초래할 수 있다.
따라서 코틀린에서는 다른 함수의 인자로 람다를 사용할 때 부담을 없앨 수 있는 인라인(inline) 이라는 최적화 방법을 제공한다.
인라인을 사용하면 람다의 객체 사용과 변수의 메모리 할당을 JVM 이 하지 않아도 된다.
람다를 인라인 처리하려면 람다를 인자로 받는 함수에 inline 키워드를 지정하면 된다.
inline 키워드를 추가하면 함수가 호출될 떄 람다가 객체로 전달 되지 않는다.
왜냐하면 코틀린 컴파일러가 바이트코드를 생성할 때 람다 코드가 포함된 welcomeMessage 함수 몸체의 전체코드를
복사한 후 이 함수를 호출하는 코드에 붙여 넣기 하여 교체하기 때문이다.
fun main(args: Array<String>) {
welcomeMessage("[시스템]: ") {name: String, message: String ->
"$name 님 $message"
}
}
inline fun welcomeMessage(label: String, resultFunction: (String, String) -> String){
println(label + resultFunction("kyu", "방문을 환영합니다."))
}
//결과
[시스템]: kyu 님 방문을 환영합니다.
- inline 이 허용되지 않는 경우가 있다. 그중하나가 람다를 인자로 받는 재귀 함수(recursive function)의 경우이다.
- 재귀 함수는 자신의 몸체코드를 여러번 반복하여 호출하여 실행하므로, 같은 코드가 무수히 많이 복사 및 붙여넣기가 된다.
- inline 이 허용되지 않는 함수는 코틀린 컴파일러가 효율성이 좋은 roof 형태로 변경하여 바이트 코드로 컴파일 한다.
5.9 함수 참조
지금까지는 람다를 사용해서 다른 함수의 인자로 함수를 전달하였다.
그러나 람다외에도 다른 방법이 있다. 함수 참조(function reference) 를 인자로 전달하는 것이다.
함수 참조는 이름이 있는 함수가 인자로 전달될 수 있게 해준다.
그리고 람다 표현식을 사용할 수 있는 곳이면 어디든 함수 참조를 사용 할 수 있다.
함수 참조를 얻고자 할때 :: 연산자를 사용한다.
fun main(args: Array<String>) {
welcomeMessage("[시스템]: ", ::logMessage) { name: String, message: String ->
"$name 님 $message"
}
}
inline fun welcomeMessage(
label: String,
logFunction: (String, Int) -> Unit,
resultFunction: (String, String) -> String) {
println(label + resultFunction("kyu", "방문을 환영합니다."))
logFunction(label, 2)
}
fun logMessage(label: String, loginCount: Int) {
println(label + "$loginCount 번째 접속입니다.")
}
//결과
[시스템]: kyu 님 방문을 환영합니다.
[시스템]: 2 번째 접속입니다.
5.10 반환 타입으로 함수 타입 사용
다른 어떤 타입과 마찬가지로 함수 타입도 반환 타입에 사용될 수 있다.
즉, 함수를 반환하는 함수를 정의 할 수 있다는 뜻이다.
fun main(args: Array<String>) {
welcomeMessage()
}
fun welcomeMessage() {
val welcomeFunction = configureWelcomeFunction()
println(welcomeFunction("[시스템]:", "kyu"))
println(welcomeFunction("[시스템]:", "kyu"))
}
fun configureWelcomeFunction(): (String, String) -> String {
var loginCount = 0
return { label: String, name: String ->
loginCount += 1
println("$label $loginCount 번째 접속입니다.")
"$label $name 님 방문을 환영합니다."
}
}
//결과
[시스템]: 1 번째 접속입니다.
[시스템]: kyu 님 방문을 환영합니다.
[시스템]: 2 번째 접속입니다.
[시스템]: kyu 님 방문을 환영합니다.
configureWelcomeFunction 은 다른 함수를 설정하는 '함수 팩토리' 라고 생각할 수 있다.
즉, 필요한 변수를 선언하고 람다에 포함시킨 후 이 람다를 호출 함수인 welcomeMessage 에 반환한다.
또한, loginCount 가 증가되어 출력된다. configureWelcomeFunction 에 선언된 지역 변수인 loginCount
는 configureWelcomeFunction 에서 반환하는 람다에서 사용되지만, 정상적으로 실행된다.
그러나 이 람다가 반환되면 configureWelcomeFunction 외부의 다른 곳에서 실행되는 어떻게 이것이 가능한 것일까?
왜냐하면 코틀린의 람다는 클로저(closure)이기 때문이다. 클로저는 "close over" 가 합쳐진 용어이며,
다른 함수에 포함된 함수에서 자신을 포함하는 함수의 매개변수와 변수를 사용할 수 있는 것을 말한다.
이 때 외부 함수에 val 로 선언된 변수는 그것을 사용하는 람다식 코드에 그 값이 바로 저장된다.
그리고 var 로 선언된 변수는 그 값이 별도의 객체로 저장되며, 그 객체의 참조값이 람다식 코드에
저장되어 값을 변경할 때 사용된다.
다른 함수를 인자로 받거나 반환하는 함수를 고차 함수(higher-order function)라고도 한다.
이 용어는 람다와 마찬가지로 수학에서 유래된 것이다.
'Develop > Kotlin' 카테고리의 다른 글
[Kotlin] File - mkdir(), mkdirs() 의 차이점 (0) | 2022.09.10 |
---|---|
[Kotlin] 2 - 조건문과 조건식 (0) | 2020.04.08 |
[Kotlin] 1 - 타입, 변수, 상수 (0) | 2020.04.08 |