[코틀린] 함수 사용하기
들어가기에 앞서
코틀린과 자바는 함수를 사용하는 방법이 조금 차이가 있다. 비슷하지만 분명한 차이점들이 존재하기 때문에 코틀린스러운 코드를 위해서 코틀린에서 함수를 다루는 다양한 방법을 익혀보자.
함수 선언과 반환
기본적으로 가지고있는 몇 가지 규칙이 있다.
- public을 선언하지 않아도 기본적으로 public으로 정의하고 있다. 필요시에 private등으로 선언 할 수 있다.
- 아무것도 반환하지 않는(자바에서의 void)타입을 Unit이라고 부른다. 반환타입을 지정하지 않으면 자동으로 Unit으로 지정된다.
- 함수 선언은 함수 이름앞에 fun을 붙이면서 시작한다.
간단한 함수 선언 코드를 보면 위의 설명을 한 번에 이해할 수 있을 것이다.
fun study(a: Int, b: Int): Int {
if(a > b) {
return a
} else {
return b
}
}
study라는 함수를 선언하였다. public을 선언 하지 않아도 public으로 선언되어지고, 함수의 반환값으로 Int를 지정하였다.
여기까지는 자바의 함수선언과 크게 다르지 않을 것이다. 코틀린의 함수 사용법은 아래와 같은 특성들을 가지고 있다.
- if가 expression형식이기 때문에 if자체를 반환값으로 사용할 수 있다. 즉, if를 return으로 받을 수 있다.
fun study(a: Int, b: Int): Int {
return if(a > b) {
a
} else {
b
}
}
이 처럼 if문의 결과마다 return을 선언하지 않고 if문 자체를 return할 수 있다.
또 다른 특징도 있다.
- 간단한 식의 경우 block( {} )이 아니라 ( = ) 으로 표현 할 수 있다.
- ( = ) 으로 표현했을 경우 반환값을 코틀린이 유추해주기 때문에 생략할 수 있다.(이 경우 Unit이 아니라 실제 반환값 타입을 잡아준다.)
- 자바와 동일하게 간단한 식에서의 block( {} )은 생략할 수 있다.
fun study(a: Int, b: Int) = if(a > b) a else b
이렇게 아주 간단하게 코드를 작성할 수 있다.
함수의 기본 값 설정 및 호출
자바의 오버로딩을 예로 들면 같은 기능을 가졌지만 다른 파라미터를 받는 함수를 오버로딩 하고자하는 개수만큼 만들어서 사용해야 할 것이다. 하지만 코틀린은 하나의 함수에서 파라미터에 기본값을 설정함으로서 다른 함수를 만들지 않고 하나의 함수에서 오버로딩의 효과를 볼 수 있다.
fun main() {
writeOfName(name = "test", num = 3)
writeOfName(name = "test", enter = false)
}
fun writeOfName(
name: String,
num: Int = 3,
enter: Boolean = true
) {
for (i in 1..num) {
if(enter) {
println(name)
} else {
print(name)
}
}
}
변수를 받을 때 num과 enter에 값을 넣어줬다. 이렇게 하면 함수를 호출할 때 파라미터를 넣지 않으면 넣지 않은 파라미터는 미리 생성해놓은 값을 기본값으로 사용하기 때문에 파라미터 개수에 따라 함수를 여러개 만들필요가 없다.
그리고 위의 main에서 실행하는 것 처럼 key = value 형식으로 파라미터를 입력해주면 중간의 파라미터를 생략하고 입력하는 것도 가능하다. 무엇보다 파라미터를 입력할 때 변수명을 함께 입력하기 때문에 builder를 따로 쓰지 않더라도 같은 효과를 볼 수 있다.
가변인자를 호출하는 방법은 자바와 비슷하다.
fun main() {
manyParameter("iphone", "galaxy")
val array = arrayOf("iphone", "galaxy")
manyParameter(*array)
}
fun manyParameter(vararg names: String) {
for (name in names) {
println(name)
}
}
파라미터앞에 vararg를 선언해주면 가변인자로 파라미터를 받을 수 있다. 함수를 호출할 때도 해당하는 타입의 값을 나열해서 입력하거나 list에 담아서 보낼수도 있다. 대신 list담아서 보낼때는 함수를 호출하는 변수앞에 *를 필수로 입력해주어야 한다.
확장 함수
확장함수는 클래스 밖에 선언된 함수이지만 해당 클래스에서 선언된 함수처럼 사용할 수 있게 만든 함수이다. 클래스 내부에 선언된 함수를 멤버함수라고 하는데 일반적으로 멤버함수를 사용하는 방법은 클래스를 선언하고 생성된 인스턴스를 통해서 함수를 실행하는 방식이다. 이 확장함수를 사용하면 클래스 외부에 함수를 선언해도 해당 클래스의 인스턴스를 통해서 함수를 사용할 수 있다.
class ExtendedFunction(val num1: Int, val num2: Int) {
// 멤버 변수
fun addNum(): Int {
return num1 + num2
}
}
// 확장 함수
fun ExtendedFunction.multipleNum(): Int {
return num1 * num2
}
fun main() {
val extendedFunction = ExtendedFunction(2, 5)
println( extendedFunction.addNum() ) // 7
println( extendedFunction.multipleNum() ) // 10
}
현재 클래스는 ExtendedFunction이다. 여기에 생성자로 num1과 num2라는 2개의 숫자를 받게 하였다. 그리고 멤버 변수 addNum을 통해 두개의 숫자를 더한 값을 반환하는 함수를 만들었다. 이 함수는 클래스를 선언하면 당연하게 사용할 수 있는 함수이다.
코드에서 확장함수를 보면 클래스 외부에 선언된 것을 볼 수 있다. 확장함수를 선언할 때는 다음과 같은 규칙이 있다.
- fun [클래스명.함수이름](): [반환타입]
선언하고자 하는 함수이름 앞에 클래스 이름을 붙여주어야 한다.
이렇게 선언된 확장 함수는 선언된 클래스를 통해서 실행될 수 있게된다.
확장 함수를 사용하면서 의문점이 생길 수 있는 몇 가지를 알아보자.
(1) 확장함수와 멤버변수의 시그니처가 같다면 멤버변수가 우선순위를 가지게 된다.
class ExtendedFunction(val num1: Int, val num2: Int) {
// 멤버 변수
fun randomNum(): Int {
return num1 - num2
}
}
// 확장 함수
fun ExtendedFunction.randomNum(): Int {
println("확장 함수")
return num1 + num2
}
fun main() {
val extendedFunction = ExtendedFunction(2, 5)
println( extendedFunction.randomNum() ) // -3
}
멤버 변수에 randomNum()이라는 함수가 있는데 확장 함수에도 동일한 이름의 함수가 존재한다. 이런 경우에는 멤버 변수가 우선순위를 가지게 된다. 결과 main함수에서 randomNum()을 실행시켰을 때 2 - 5의 결과값인 -3이 출력되었다.
(2) 부모 - 자식의 클래스가 존재할 때 두 클래스에 모두 확장함수를 만들어서 실행하면 타입에 맞는 확장함수를 호출한다.
fun main() {
val dog: Dog = Dog() // (1)
val dog2: Dog = Maltese() // (2)
val maltese: Maltese = Maltese() // (3)
println( dog.getOwner() ) // Dog 확장 함수
println( dog2.getOwner() ) // Dog 확장 함수
println( maltese.getOwner() ) // Maltese 확장 함수
}
Dog클래스가 있고 Dog클래스를 상속받은 Maltese클래스가 있다고 해보자. 이때 두 클래스에 대해 모두 확장 함수를 만들었다. 이때 3개의 경우로 나눠서 테스트 해보았다. 1번과 3번은 정확활 수 있지만 2번은 조금 애매한 상황인 것 같다. 타입은 부모클래스이지만 인스턴스는 자식클래스이다.
결과는 선언된 타입에 맞는 확장 함수가 실행되는 것을 볼 수 있다. 인스턴스가 어떤 것이 생성되었든지 변수의 타입에 해당하는 확장 함수가 실행되는 것이다.
(3) 확장 함수도 custom getter처럼 사용 할 수 있다.
class ExtendedFunction(val num1: Int, val num2: Int) {
}
// 확장 함수
val ExtendedFunction.multipleNum: Int
get() = num1 * num2
fun main() {
val extendedFunction = ExtendedFunction(2, 5)
println( extendedFunction.multipleNum ) // 10
확장 함수도 프로퍼티로 확장해서 사용할 수 있다. 확장 함수와 custom getter를 섞어 놓은 것 처럼 사용할 수 있다.
확장 함수를 사용하면 해당 클래스의 private으로 선언된 변수들도 함수로 가져와서 접근할 수 있다고 생각할 수 있다. 이렇게 되면 외부로 노출시키지 않기 위한 변수가 노출되는 것이므로 캡슐화가 깨질 수가 있다. 이런 경우를 예방하기 위해서 확장 함수에서는 private이나 protected에는 접근이 불가능하게 되있다. 만약 접근하고자 한다면 컴파일에러가 발생하기 때문에 안전하다고 생각해도 된다.
infix 함수
infix는 중위 함수라고 불리는 새로운 함수호출 방법이다. 이 함수는 멤버 함수, 확장 함수 모두 사용할 수 있다.
class InfixFunction(val num1: Int, val num2: Int) {
}
// 확장 함수
infix fun InfixFunction.addNum(num3: Int): Int {
return num1 + num2 + num3
}
fun main() {
val infixFunction = InfixFunction(2, 5)
println( infixFunction.addNum(1) )
println( infixFunction addNum 1 )
}
원래 일반적인 함수를 사용할 때는 [변수.함수이름(argument)]의 형식으로 사용해야하지만 infix가 붙은 함수는 [변수 함수이름 argument]의 형식으로 사용할 수 있다. 마치 DSL과 같은 형식으로 사용할 수 있는 것이다.
infix를 사용할 때는 반드시 함수에 파라미터가 하나만 있어야 한다. 파라미터가 없거나 2개이상일 경우 컴파일에러가 발생하면서 infix를 사용할 수 없기 때문에 이 점을 인지해야 한다.
inline 함수
inline함수는 코드가 바이트코드로 변환되었을 때 함수의 위치 자체가 클래스 내부로 들어가게 된다.
class InlineFunction(val num1: Int, val num2: Int) {
}
// inline 함수
inline fun InlineFunction.addNum(num3: Int): Int {
return num1 + num2 + num3
}
fun main() {
val inlineFunction = InlineFunction(2, 5)
println( inlineFunction.addNum(1) )
}
클래스 외부에 선언된 함수를 클래스를 통해 실행하고 있다. 이것만 보면 확장 함수와 다른 것을 알 수 없지만 해당 코드를 바이트 코드로 변환하면 addNum이라는 함수가 클래스 내부로 들어가게 된다. 마치 멤버 함수처럼 사용할 수 있는 것이다.
이 함수를 사용하는 이유는 파라미터 전달에 대한 오버헤드를 줄이기 위해서이다. 하지만 성능 측정과 함께 신중하게 사용되어야 할 함수이다.
지역 함수
지역 함수는 함수내부에 다시 함수를 선언하는 것이다.
class LocalFunction {
// fun validationCheck(startDate: String, endDate: String): String {
// if(startDate.isEmpty()) throw IllegalArgumentException("시작날짜가 잘못됐습니다.")
// if(endDate.isEmpty()) throw IllegalArgumentException("종료날짜가 잘못됐습니다.")
//
// return "success"
// }
fun validationCheck(startDate: String, endDate: String): String {
fun validateDate(date: String, name: String) {
if (date.isEmpty()) throw IllegalArgumentException("${name}날짜가 잘못댔습니다.")
}
validateDate(startDate, "시작")
validateDate(endDate, "종료")
return "success"
}
}
fun main() {
val localFunction = LocalFunction()
localFunction.validationCheck("", "")
}
주석된 코드처럼 다시 함수로 만들어서 사용할 수 있을것같은 코드를 함수 내부에서 함수로 만들어서 사용하는 것이다. 이렇게 사용하는 방식은 어떤면에서는 유용할 수 있으나 depth도 깊어지게 되고 코드의 가독성도 떨어질 수 있으므로 잘 판단해서 사용해야 할 것이다.
'Kotlin' 카테고리의 다른 글
LocalDate 사용해보기 (0) | 2022.08.26 |
---|