자바에서는 클래스를 기본으로 코드를 구성한 반면, 코틀린에서는 함수형 프로그래밍을 지원하기 때문에 클래스를 사용하지 않아도 된다. 때문에 함수를 잘 사용할 줄 아는 것이 코틀린 개발의 기본이다.
함수 선언과 사용
클래스 없이도 함수를 정의할 수 있다. 구문은 다음과 같다.
fun 함수이름(매개변수): 반환값의_형식 {
// 함수 내용
}
fun double(i: Int): Int {
return 2 * i
}
함수를 호출하는 구문은 다음과 같다.
val 변수이름 = 함수이름(인수)
// ex
val number = double(5)
함수는 이름을 지정해 호출하는 코드의 조각이다. 메소드는 클래스의 인스턴스(객체)와 연결된 함수이며, 멤버 함수라고도 불린다. 즉, 메소드는 클래스 안에 있는 함수이다.
매개변수 (parameter)와 인수 (argument)
자바처럼 한 함수에서 여러 매개변수를 가질 수 있다. 또한, 만약 인수의 형식이 여러개라면 매개변수의 형식은 그들의 상위 형식인 Any
를 사용해야 한다. null을 허용하려면 Any?
, 허용하지 않으려면 Any
를 사용하면 된다.
fun printAnything(v: Any) {
println("Hello $v")
}
printAnything("Kate") // output: Hello Kate
printAnything("13") // output: Hello 13
Unit 인스턴스
코틀린에서는 모든 함수가 값을 반환한다. 만약 반환값의 형식을 지정하지 않으면 기본 반환값은 Unit 인스턴스다. 자바의 void에 해당하지만, 변수에 저장할 수 있다는 점이 자바와의 차이점이다.
함수가 Unit을 반환하는 경우 return을 호출하지 않아도 되지만, 그 외의 형식을 반환하는 경우 명시적으로 값을 반환해야 한다. Unit을 반환하는 것은 불필요한 경우가 많으니 사용하지 않는 것이 좋다.
fun sumPositive(a: Int, b: Int) {
if (a >= 0 && b >= 0) {
return // 리턴값 없어도 return 사용할 수 있음
}
println(a + b)
// if 조건에 해당되지 않는 경우에 대한 리턴값이 없어도 컴파일 됨
}
fun sumPositive(a: Int, b: Int): Int {
if (a >= 0 && b >= 0) {
return a + b
}
return 0 // 이 리턴 코드가 없다면 컴파일 X
}
vararg 매개변수
매개변수의 수를 사전에 알 수 없는 경우에 사용된다. 매개변수 이름 앞에 vararg
를 추가하면 필요한 만큼 인수를 받을 수 있다. 한 함수에 하나의 vararg 매개변수만 허용된다는 점을 유의하자.
fun printSum(vararg numbers: Int) {
val sum = numbers.sum()
print(sum)
}
printSum(1, 2, 3, 4, 5) // output: 15
printSum() // output: 0
vararg의 형식을 Any로 두면 다양한 형식의 값을 인수로 받을 수 있다.
fun printAll(vararg texts: Any) {
val allTexts = texts.joinToString(", ")
println(allTexts)
}
printAll("A", 1, 'b') // output: A,1,b
val texts = arrayOf("A", "B", "C")
printAll(*texts) // output: A,B,C
printAll("K", *texts, "D") // output: K,A,B,C,D
단일 식 함수 (single-expression function)
안드로이드 프로젝트에서 액티비티에 자주 사용되는 패턴 중 뷰에서 텍스트를 얻거나 뷰에서 얻은 데이터를 제공하는 메소드를 정의해 프레젠터에서 값을 얻는 것이 있다.
fun getEmail(): String {
return emailView.text.toString()
}
이렇게 단일 식의 결과를 반환하는 함수는 코틀린에서 더 간단하게 작성될 수 있다. 바로 중괄호와 함수 본문을 생략하고, =
기호로 식을 직접 지정하는 것이다. 위 코드를 단일 식 함수로 정의해보면 아래와 같다.
fun getEmail(): String = emailView.text.toString()
레이아웃 ID를 제공하고 ViewHolder를 생성하는 RecyclerView 어댑터의 코드 또한 단일 식 함수로 간단하게 작성할 수 있다.
class AddressAdapter : ItemAdapter<AddressAdapter.ViewHolder()> {
override fun getLayoutId() = R.layout.choose_address_view
override fun onCreateViewHolder(itemView: View) = ViewHolder(itemView)
}
단일 식 함수는 when 식과 잘 쓰인다. 아래 코드는 키를 기준으로 객체에서 특정 데이터를 얻는 방법을 보여준다.
fun valueFromBooking(key: String, booking: Booking?) = when(key) {
"patient.phone" -> booking?.patient?.phone
"patient.email" -> booking?.patient?.email
"comment" -> booking?.comment
else -> null
}
액티비티 메소드와 when 식을 결함해 최상위 메뉴 클릭을 처리하는 코드이다 (프로젝트에서 많이 사용된다).
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
item.itemId == android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
아래 코드는 단일 객체에 대한 여러 작업을 연결하는 코드이다.
fun textFormatted(text: String, name: String) = text
.trim()
.capitalize()
.replace("{name}", name)
val formatted = textFormatted("hello, {name}", "Kate")
println(formatted) // output: hello, Kate
꼬리 재귀 함수 (tail-recursive function)
함수가 수행한 마지막 작업으로 자신을 호출하는 함수이다. 기존 재귀함수를 사용했을 때 발생할 수 있는 StackOverflowError가 발생하지 않아 (재귀함수가 호출될 때마다 이전 함수의 반환 주소가 스택에 저장되기 때문) 더 효율적으로 코드를 작성할 수 있다.
함수를 꼬리 재귀 함수로 지정하고 싶으면 tailrec
키워드를 사용하면 된다.
tailrec fun getState(state: State, n: Int): State =
if (n <= 0) state
else getState(state.nextState(), n - 1)
tailrec
한정자가 작동하려면 아래의 조건이 충족되어야 한다.
기본 인수, 명명된 인수 구문
인수의 일부만 전달하면서 함수를 호출해야 할 경우, 자바에서는 인수로 null을 전달하는 경우가 많았다. 동일한 형식의 매개변수를 가지는 메소드끼리는 오버로딩이 되지 않기 때문이다. 코틀린에서는 기본 인수와 명명된 인수 구문으로 이러한 문제를 해결할 수 있다.
인자를 정의하면서 기본값도 설정할 수 있다. 만약 기본값을 설정하지 않았다면 함수를 부를 때 무조건 인자의 값을 제공해야 한다.
fun printMessage(product: String, amount: Int = 0,
name: String = "Annonymous") {
println($name has $amount $product)
}
printMessage("apple")
printMessage("apple", 5)
printMessage("apple", 5, "Wendy")
// output
Annonymous has 0 apple
Annonymous has 5 apple
Wendy has 5 apple
인수값을 함수에 넣을 때 인수 이름에 바로 값을 지정할 수 있다. 마지막 인수의 값만 지정할 때 유용하게 사용된다.
printMessage("apple", name = "John") // output: John has 0 apple
주의할 점은 명명된 인수 구문의 다음 인수부터는 기존 구문을 사용할 수 없기 때문에 계속 명명된 인수 구문을 사용해야 한다는 것이다.
printMessage(product = "peach", 2) // 오류
printMessage(product = "peach", amount = 2) // output: Annonymous has 2 peach
최상위 함수 (top-level function)
말 그대로 최상위에 정의된 함수이다. 예시를 보면 단번에 이해할 수 있다.
// Test.kt
package com.example
fun printTwo() {
print(2)
}
최상위 함수는 private으로 정의되지 않았다면 코드 어디에서나 사용할 수 있다. 최상위 함수에 접근하려면 import문으로 함수를 현재 파일로 불러와야 한다. (함수를 사용하면 자동으로 import가 추가된다.)
위 코드가 자바 바이트코드로 컴파일될 때, 다음과 같은 코드로 변환된다.
public final class TestKt {
public static void printTwo() {
System.out.println(2)
}
}
자바 코드에서 코틀린 최상위 함수를 호출하고 싶다면 클래스 이름을 지정해서 호출하면 된다.
TestKt.printTwo()
코틀린으로 제작한 라이브러리를 자바 클래스에서 사용할 때 유용하게 쓰인다.
자바에서 코틀린 최상위 함수를 사용하는 또 하나의 방법으로 애노테이션을 사용하는 것이 있다. 애노테이션을 파일 위의 패키지 이름 앞에 추가하면 된다. 구문은 다음과 같다.
@file:JvmName("코틀린파일_이름")
이 애노테이션이 적용되면 클래스 이름이 코틀린 파일의 이름으로 바뀐다. 이후 자바에서 코틀린 파일 이름을 클래스 이름으로 지정하고 최상위 함수를 호출하면 된다.
Test.printTwo()
파일 맨 위에 다음과 같은 애노테이션을 지정해 여러 파일에 정의된 최상위 함수를 동일한 클래스에 포함시킬 수 있다.
@file:JvmMultifileClass
아래의 코드는 최솟값과 최댓값을 자바에서 구하기 위한 코틀린 라이브러리이다.
// Max.kt
@file:JvmName("Math")
@file:JvmMultifileClass
package com.example.math
fun max(a: Int, b: Int): Int = if (a > b) a else b
// Min.kt
@file.JvmName("Math")
@file:JvmMultifileClass
package com.example.math
fun min(a: Int, b: Int): Int = if (a < b) a else b
위의 라이브러리를 자바에서 사용한 예시이다.
Math.min(3, 8)
Math.max(9, 5)
로컬 함수
다른 함수 안에 위치한 함수이다. 함수가 정의된 함수 외부에서 접근할 수 없다는 것이 특징이다. 로컬함수는 바깥 함수의 매개변수와 로컬 변수에 접근할 수 있다.
로컬함수는 한 함수에만 사용되는 기능을 추출하고, 이 기능이 해당 함수의 요소를 사용하는 경우에 사용된다.
fun makeStudentList(): List<Student> {
var students: List<Student> = emptyList()
fun addStudent(name: String, state: Student.state = Student.State.New) {
students += Student(name, state, courses = emptyList())
}
addStudent("Karina")
addStudent("Jane")
return students
}
만약 addStudent 함수가 makeStudentList 바깥에 있었다면 어땠을까? List<Student>
에 대한 인수가 추가로 필요했을 것이고, 코드 또한 복잡해졌을 것이다. 이처럼 로컬함수는 코드 추출 및 재사용에 유용하게 쓰일 수 있다.
Nothing 반환 형식
항상 예외를 생성하는 함수를 정의할 때 Nothing 클래스가 사용된다. 즉, 오류 생성을 간소화하는 함수 (발생한 오류에 대한 데이터를 제공해야 하는 라이브러리)나 단위테스트에서 오류를 생성하는데 사용되는 함수 (코드에서 오류 처리를 테스트)에서 사용된다.
Nothing 클래스는 비어있는 형식이기 때문에 인스턴스가 없다. 반환형식이 Nothing인 함수는 return문을 실행하지 않고 무조건 예외를 생성한다.
fun processElement(element: Element) {
fun throwError(message: String): Nothing = throw ProcessingError("Error in element $element: $message")
if (element.kind != ElementKind.METHOD)
throwError("Not a method")
}
Nothing 클래스는 null 허용과 불허를 포함한 모든 형식의 하위 형식처럼 작동된다. 그래서 함수를 throw문과 같이 사용할 수 있다.
fun getFirstCharOfFail(str: String): Char = if (str.isNotEmpty()) str[0] else fail()
val name: String = getName() ?: fail()
val enclosingElement = element.enclosingElement ?: throwError ("Lack of enclosing element")
참조문헌: 코틀린을 이용한 안드로이드 개발 (2018)