[Kotlin] 3-1. 함수 정의와 호출

hansol_kim·2021년 9월 3일
0

kotlin 적응기

목록 보기
2/5
post-thumbnail

코틀린 공부 2일차

* 컬렉션, 문자열, 정규식을 다루기 위함 함수
* 이름 붙인 인자, 디폴트 파라미터 값, 중위 호출 문법
* 확장 함수와 확장 프로퍼티를 사용하여 자바 라이브러리 사용
* 최상위 및 로컬 함수와 프로퍼티를 사용해 코드 구조화

이번 챕터에서는 함수 정의와 호출 기능을 코틀린이 어떻게 개선했는지 알아보도록 한다.
추가로 확장함수와 프로퍼티를 사용하여 자바 라이브러리를 코틀린 스타일로 적용하는 방법을 살펴본다.

코틀린에서 컬렉션 만들기

val set = hashSetOf(1, 28, 37)
val list = arrayListOf(1, 28, 37)
val map = hashMapOf(1 to "one", 28 to "twenty-eight", 37 to "thirty-seven")

여기서 to는 키워드가 아닌 함수이다. 조금 이따가 알아보도록 하자.

코틀린은 자신만의 컬렉션 기능을 제공하지 않는다. 기존 자바 컬렉션을 활용한다. 이는 자바코드와 상호작용하는데에 쉽도록 하기위함이다. 하지만 코틀린에서는 자바보다 더 많은 기능을 제공한다.

list.last() // 마지막 원소를 가져옴
numbers.max() // 최댓값을 리턴함

참고로 현재 max() 메서드는 Deprecated 되었다. 대신 maxByOrNull()을 제공하니 해당 메서드를 사용하면 될 듯하다.

근데, 자바에 있는 컬렉션을 사용해놓고 어떻게 새로운 메서드를 사용할 수 있지?

이러한 메서드가 어떻게 동작하고 정의하는지 이번 챕터에서 알아보도록 하자

함수를 호출하기 쉽게 만들기

먼저 여러 원소를 포함하고 있는 컬렉션의 원소를 출력하기 위한 문자열을 리턴하는 함수를 만들어보자. 물론 toString 메서드가 존재하지만 ex) [1, 2, 3] 이러한 형태의 출력을 원하지 않을 수 있으니 커스텀한 함수를 만들어보자.

fun <T> joinToString(
    collection: Collection<T>,
    separator: String,
    prefix: String,
    postfix: String
): String {
    val result = StringBuilder(prefix)

    for((index, element) in collection.withIndex()) {
        if(index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

//// fun call ////
println(joinToString(list, "; ", "(", ")"))

//// result ////
(apple; banana; orange)

함수를 호출한 부분을 보면 각 파라미터가 무엇을 의미하는 지 바로 알기 어렵다. (해당 함수의 파라미터를 외우지 않는 한 알 수가 없다. 누가 외우나 이걸...)

그래서 함수에 전달하는 인자 중 일부 또는 전부의 이름을 명시할 수 있다

joinToString(collection = list, separator = " ", prefix = "", postfix = "")

안타깝게도 자바로 작성한 함수를 호출할 때는 코틀린에서 위와 같이 파라미터 명을 명시할 수 없다.

디폴트 파라미터 값

파라미터를 사용하지 않아도 기본 값을 세팅할 수 있고 이는 오버로딩 지옥에서 벗어날 수 있게 해준다.

fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ", " ,
    prefix: String = "",
    postfix: String = "",
): String

Tips) 자바에는 디폴트 파라미터 값이라는 개념이 존재하지 않는다. 그래서 코틀린으로 작성한 함수를 자바에서 호출하는 경우 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다. 자주 호출해야한다면 불편할 수 있기 때문에 @JvmOverloads를 함수에 추가해 사용하면 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 오버로딩된 자바 메서드를 추가해준다.

최상위 함수와 프로퍼티

자바와 달리 함수를 클래스 내부에 작성하지 않아도 된다. JVM에서 컴파일러는 해당 파일이름에 맞는 클래스를 정의해준다. 자바에서는 해당 함수를 사용할 때 해당 파일명을 클래스로 생각하여 사용한다.

/// For example ///
import strings.JoinKt
...
JoinKt.joinToString(list)

파일에 대응하는 클래스의 이름을 변경할 수도 있다. "StringFunctions" 와 같은 클래스명을 사용하고 싶다면 아래와 같이 @JvmName 어노테이션을 사용하면 된다.

@file:JvmName("StringFunctions")

프로퍼티의 경우도 최상위 프로퍼티를 활용하여 상수를 추가할 수 있다.

val UNIX_LINE_SEPARATOR = "\n" -> (1)
const val UNIX_LINE_SEPARATOR = "\n" -> (2)

//// java ////
public static final String UNIX_LINE_SEPARATOR = "\n" -> (3)

(1)의 경우 상수처럼 보이지만 getter가 생긴다.(var의 경우 getter, setter) 상수인데 getter는 자연스럽지 못하다. 그래서 (2)와 같이 const 변경자를 추가하면 (3)과 동등한 바이트코드를 만들어내어 사용할 수 있다.

메서드를 다른 클래스에 추가 - 확장 함수와 확장 프로퍼티

기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 핵심 목표 중 하나다. 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 사용할 수 있다면 상당히 편리할 것이다.

바로 확장함수가 그런 역할을 한다.

확장함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다.

fun String.lastChar(): Char = this.get(this.length - 1)
println("kotlin".lastChar())

//// result ////
> n

예시를 보면 lastChar 메서드는 String 클래스 내부에 존재하여 제공하는 것처럼 보인다. 하지만 외부에서 선언한 확장함수임을 알 수 있다

확장함수를 만들려면 함수 이름 앞에 그 함수가 확장하려는 클래스의 이름을 붙이면 된다. 클래스 이름을 수신 객체 타입이라 부르며, 호출되는 대상이 되는 값을 수신 객체라고 부른다. 방금 전 작성했던 확장함수를 들여다보자.

fun String.lastChar(): Char = this.get(this.length - 1)

/**
 * 여기서 String은 수신객체 타입이 되고
 * this가 수신객체가 된다.
 **/

그리고 수신객체는 this를 생략할 수 있다

fun String.lastChar(): Char = get(length - 1)

확장함수가 캡슐화를 깨지는 않는다. 클래스 안에서 정의한 메소드는 private 또는 protected 멤버를 사용할 수 있지만 확장 함수 내에서는 사용할 수 없다.

joinToString 함수를 확장함수로 변경

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    for((index, element) in withIndex()) {
        if(index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

//// use ////
list.joinToString()

확장함수를 사용하여 마치 Collection에서 제공하는 함수인듯한 joinToString 메서드를 만들었다.

확장함수는 오버라이드 할 수 없다.

코틀린의 메소드 오버라이드도 일반적인 객체지향의 메소드 오버라이드와 마찬가지다.

open class View {
    open fun click() = println("View Clicked")
}

open class Button: View() {
    override fun click() = println("Button Clicked")
}

//// use ////
val view: View = Button()
view.click()

//// result ////
> Button Clicked

하지만 확장함수는 오버라이드 할 수 없다.

fun View.showoff() = println("*** view ***")
fun Button.showoff() = println("*** button ***")

//// use ////
view.showoff()

//// result ////
> *** view ***

결과 출력이 *** button ***을 기대했지만 *** view *** 로 출력되었다. 즉, 확장함수는 오버라이드할 수 없다.

왜 그럴까?

오버라이드가 이루어지는 과정을 생각해보자. 오버라이드는 실행 시점에서 호출할 대상 메소드를 결정한다. 하지만, 확장함수는 컴파일 시점에서 결정되기 때문에 오버라이드가 이루어지지 않는다.

추가적으로 확장함수와 시그니처가 같은 멤버 함수가 있다면 우선순위는 멤버 함수에 있기 때문에 멤버 함수가 호출된다.

profile
1주 1글 실천하자

0개의 댓글