Kotlin in Action - Chapter3) Defining and calling functions

7hong13·2023년 6월 25일
0

Kotlin in Action

목록 보기
3/3
post-thumbnail
* Contents

Part1: Introducing Kotlin
	Chapter1: Kotlin: what and why
    Chapter2: Kotlin basics 
    Chapter3: Defining and calling functions ✔
    Chapter4: Classes, objects, and interfaces
    Chapter5: Programming with lambdas
    Chapter6: The Kotlin type system
    
Part2: Embracing Kotlin
	Chapter7: Operator overloading and other conventions
    Chapter8: Higher-order functions
    Chapter9: Generics
    Chapter10: Annotations and reflection
    Chapter11: DSL construction

코틀린의 collections

코틀린의 collection 클래스들은 자바의 표준 collection 클래스들을 그대로 사용한다.
(자바와의 상호호환에서 이점을 갖기 위해서이다.)
따라서 코틀린은 자체적 collection 클래스를 갖지 않는다.

하지만 코틀린의 collections는 자바의 collections 보다 부가적인 기능을 갖는다.
예를 들어 코틀린에선 collection의 마지막 요소나 최대 값을 갖는 요소를 바로 얻을 수 있다.

val strings = listOf("first", "second", "fourteenth")
println(strings.last()) // fourteenth

val numbers = setOf(1, 14, 2)
println(numbers.max()) // 14

이번 챕터에서는 이러한 부가 함수들이 어떻게 지원되는지 알아보고자 한다.

코틀린의 함수

특정 collection의 요소들을 문자열 형태로 반환하는 함수 joinToString()을 만든다고 가정해보자.
(*참고로 joinToString()은 코틀린에서 제공하는 내장 함수이다.)

초기 구현은 다음과 같다.

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()
}

위 함수의 호출은 다음과 같을 것이다.

val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")")) // (1; 2; 3)

이 자체로도 문제는 없지만, 코틀린의 여러 기능을 활용하면 훨씬 가독성있고 간편하게 함수를 호출할 수 있다.
각 기능에 대해 차례대로 살펴보자.

Named arguments

joinToString(collection, " ", " ", ".")

위 함수 호출의 문제는 무엇일까? 바로 가독성이 떨어진다는 것이다.
각 인자값이 어떤 매개변수와 매칭되는지 파악하기 어렵다.

코틀린에선 이를 다음과 같이 개선할 수 있다.

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

위와 같이 각 인자값에 해당하는 매개변수명을 명시적으로 지정하는 것을 named arguments라고 한다.
각 인자값의 의미를 한눈에 파악할 수 있어 혼란을 줄일 수 있다.

Default parameter values

자바에서 오버로딩을 구현해야 하는 상황을, 코틀린에선 매개변수에 디폴트 값을 지정해 더 간단히 해결할 수 있다.

fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ", ", // default value 지정
        prefix: String = "", // default value 지정
        postfix: String = "" // default value 지정
): String

joinToString(list, ", ", "", "") // 1, 2, 3
joinToString(list) // 1, 2, 3
joinToString(list, "; ") // 1; 2; 3

인자값을 넘기지 않으면 디폴트 값이 사용되므로, 인자값을 넘기지 않는 상황에 대해 별도의 오버로딩이 필요하지 않다.

다음과 같이 named arguments와도 함께 활용 가능하다.

joinToString(list, suffix = ";", prefix = "# ") // # 1, 2, 3;

@JvmOverloads
자바는 default parameter values라는 개념을 갖지 않는다.
코틀린에서 개발 시 함수에 @JvmOverloads 어노테이션을 지정하면, 자바에서 해당 함수를 호출할 때 컴파일러가 상응하는 오버로딩 함수를 자체 구현한다.
따라서 자바에서 빈번히 호출되는 함수라면 해당 어노테이션을 지정해주어 자바 개발자에게 편리함을 제공할 수 있다.

최상위 함수 및 프로퍼티

최상위 함수

자바에서 모든 함수는 클래스 안에 위치해야 한다.
따라서 함수 선언을 위해 불필요한 클래스 선언을 할 때도 있다.
하지만 코틀린은 최상위 함수를 제공해 의미없는 클래스 생성을 방지한다.

최상위 함수란, 클래스 안에 속하지 않은 함수로 파일 맨 윗단에 선언한다.

package strings

fun joinToString(...): String { ... }

코틀린의 모든 최상위 함수는 클래스의 static 함수로 컴파일 된다.

@JvmName
최상위 함수에 대해 생성되는 클래스명을 @JvmName 어노테이션을 사용해 변경할 수 있다.
@JvmName 어노테이션은 다음과 같이 패키지명 위에 선언한다.

@file:JvmName("StringFunctions")

package strings

fun joinToString(...): String { ... }

최상위 프로퍼티

마찬가지로, 클래스 안에 속하지 않은 프로퍼티를 선언할 수 있다.
이를 최상위 프로퍼티라고 부른다.
최상위 함수와 동일하게 파일 맨 윗단에 선언하며, static field로 저장된다.

var opCount = 0 // 최상위 프로퍼티

fun performOperation() {
    opCount++
	// ... 
}

fun reportOperationCount() {
    println("Operation performed $opCount times")
}

확장 함수 및 프로퍼티

확장 함수

확장 함수란 클래스 밖에 선언되지만, 해당 클래스 내에 선언된 함수처럼 사용가능한 함수이다.
확장 함수는 다음과 같이 선언한다.

package strings

// 확장하려는 클래스 혹은 인터페이스명을 함수 앞에 붙여준다.
fun String.lastChar(): Char = this.get(this.length - 1)

이 때, 확장되는 클래스명을 receiver type, 확장 함수의 호출 대상을 receiver object라고 지칭한다.

확장 함수는 내부적으로 receiver object를 인자값으로 받는 static 함수로 컴파일된다.
또한 확장 함수는 오버라이드 될 수 없다.

만약 클래스 내에 있는 함수와 확장 함수의 이름 및 매개변수가 일치하면, 클래스 내 멤버 함수가 무조건 우선권을 지닌다.

확장 프로퍼티

마찬가지로 확장 프로퍼티를 선언할 수 있다.
확장 프로퍼티는 일반 프로퍼티와 다르게 backing field가 없다.
즉, 상태를 지닐 수 없으며 초기화될 수도 없다.

확장 프로퍼티는 다음과 같이 선언할 수 있다.

// StringBuilder의 값을 수정하므로 var로 선언
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }
    
println("Kotlin".lastChar) // n
val sb = StringBuilder("Kotlin?")
sb.lastChar = '!'
println(sb) // Kotlin!

collections 다루기

collection을 더 쉽게 다루기 위해 코틀린 표준 라이브러리에서 제공하는 기능을 살펴보자.

Java Collections API 확장하기

앞서 언급했듯 코틀린의 collection 클래스들은 자바 클래스들과 동일하나, 일부 확장된 API를 갖는 형태이다.

이 때, 확장된 API는 위에서 살펴보았던 확장 함수로 구현된다.

fun <T> List<T>.last(): T { /* returns the last element */ }
fun Collection<Int>.max(): Int { /* finding a maximum in a collection */ }

varargs

varargs는 함수에서 가변 인자를 받기 위한 키워드이다.
예를 들어 다음과 같이 list를 선언한다고 하자.

val list = listOf(2, 3, 5, 7, 11)

보다시피 넘겨주어야 하는 인자의 개수가 고정되어있지 않다.
이는 listOf() 함수의 인자가 가변 인자로 선언되어 있기 때문이다.

fun listOf<T>(vararg values: T): List<T> { ... }

또한 인자값으로 배열을 넘겨줄 때, spread operator를 사용해 배열을 자동으로 언패킹할 수 있다.

fun main(args: Array<String>) {
    val list = listOf("args: ", *args) // *이 spread operator에 해당한다.
    println(list)
}

infix calls와 destructuring variables

코틀린에서 map을 다음과 같이 선언할 수 있다.

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

여기서 사용되는 to가 바로 infix call에 해당한다.
내부적으로 봤을 때, to는 다음과 같이 구현된다.

infix fun Any.to(other: Any) = Pair(this, other)

infix calls는 일반 함수 및 확장 함수에 사용 가능하며, infix modifier를 함수 앞에 추가해 선언한다.

이렇게 infix call을 사용한 값은 아래와 같이 변수 할당이 가능하다.

val (number, name) = 1 to "one" // number = 1, name = "one"

이를 destructuring declarations이라 지칭한다.

문자열 처리

코틀린은 문자열 처리를 위한 여러 기능을 제공한다.

  • split(): 문자열을 쪼개기 위한 함수이다.
  • triple quoted strings: "" 대신 """""" 안에 문자열을 선언하면 escaping letters를 생략할 수 있다.
  • toRegex(): 문자열을 정규표현식으로 변환하는 함수이다.

로컬 함수

한 함수 내에서 중복되는 코드가 있을 때, 이를 함수 내 함수로 다시 도출할 수 있다.
해당 함수를 로컬 함수라고 부른다.

// 로컬 함수 적용 전
fun saveUser(user: User) {
    if (user.name.isEmpty()) {
     throw IllegalArgumentException(
        "Can't save user ${user.id}: empty Name")
	}
    if (user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Address")
	}
}

// 로컬 함수 적용 후
fun saveUser(user: User) {
    fun validate(user: User, value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
        "Can't save user ${user.id}: empty $fieldName")
        } 
    }
    validate(user, user.name, "Name")
    validate(user, user.address, "Address")
}

로컬 함수는 상위 함수의 모든 매개변수에 접근할 수 있다.

profile
안드로이드/코틀린

0개의 댓글