[Kotlin in Action] 03장 - 함수 정의와 호출


3장 함수 정의와 호출

3.1 코틀린에서 컬렉션 만들기

val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

// to는 일반함수
  • 코틀린은 기존 자바 컬렉션을 활용함
  • 자바보다 더 많은 기능 제공
    • val strings = (“first”, “second”)
    • strings.last()

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

3.2.1 이름 붙인 인자

joinToString(collection, separator= " ", prefix = " ", postfix = ".")
  • 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.

3.2.2 디폴트 파라미터 값

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

// 함수 호출 시 모든 인자 쓸 수도 있고, 일부 생략도 가능
joinToString(list, ", ", "", "")
joinToString(list)
joinToString(list, "; ")

// 이름 붙은 인자를 사용하는 경우 지정하고 싶은 인자를 이름을 붙여서 순서와 관계없이 지정할 수 있다.
joinToString(list, postfix = ";", prefix = "# ")
fun max(a: Int, b:Int): Int {
    return if (a > b) a else b
}

3.2.3 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티

최상위 함수
  • 한 클래스에 포함시키기 어려운 코드
  • 비슷하게 중요한 역할을 하는 클래스가 둘 이상
  • 최상위 함수가 들어있던 코틀린 소스 파일의 이름과 대응
// join.kt 파일 작성
package strings
fun joinToString(...): String { ... }
JoinKt.joinToString(list, ", ", "", "")
최상위 프로퍼티
  • 최상위 프로퍼티의 값은 정적 필드에 저장
  • 코드에 상수 추가
var opCount = 0

fun performOperation() {
    opCount++   // 최상위 프로퍼티 값 변경
}

fun reportOperationCount() {
    println("Operation performed $opCount times")   // 최상위 프로퍼티 값을 읽는다.
}
  • 상수로 사용
    • val UNIX_LINE_SEPARATOR = “\n”
      • 게터 생성 됨
    • const val UNIX_LINE_SEPARATOR = “\n”
      • public static final 필드로 컴파일 됨
      • 원시타입과 String 타입의 프로퍼티만 const 지정 가능

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

  • 확장 함수
    • 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수
package strings
fun String.lastChar():Char = this.get(this.length - 1)

println("Kotlin".lastChar())
  • String 클래스에 새로운 메서드를 추가하는 것과 같음

3.3.1 임포트와 확장 함수

  • 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다.
import strings.lastChar
val c = "Kotlin".lastChar()

// *를 사용한 임포트
import stirngs.*

// as 키워드를 사용하면 다른 이름으로 변경 가능
import strings.lastChar as last
val c = "Kotlin".last()

3.3.2 자바에서 확장 함수 호출

  • 정적 메소드를 호출하면서 첫 번째 인자로 수신 객체를 넘기면 됨
    • char c = StringUtilKt.lastChar(“Java”)

3.3.3 확장 함수로 유틸리티 함수 정의

  • 확장 함수는 정적 메소드 호출에 대한 문법적인 편의일 뿐
    • 클래스가 아닌 구체적인 타입을 수신 객체 타입으로 지정 가능
fun Collection<String>.join(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) = joinToString(separator, prefix, postfix)

listOf("one", "two", "eight").join(" ")
listOf(1, 2, 8).join(" ")
=> Error: Type mismatch: inferred type is List<Int> but Collection<String>

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

멤버 함수 오버라이드
open class View {
    open fun click() = println("View clicked")
}

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

val view: View = Button()
view.click()
==> Button clicked
확장 함수는 오버라이드 할 수 없다
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

val view: View = Button()
view.showOff()
==> I'm a view!
  • view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출된다.
  • 호출될 확장 함수를 정적으로 결정 되기 때문

3.3.5 확장 프로퍼티

val String.lastChar: Char
    get() = get(length -1)  // 프로퍼티 게터
    set(value: Char) {
        this.setCharAt(length - 1, value)   // 프로퍼티 세터
    }

"Kotlin".lastChar
val sb = StringBuilder("Kotlin")
sb.lastChar = '!'

3.4 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원

  • 코틀린 언어 특성
    • vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수 정의 가능
    • 중위(infix) 함수 호출 구문 사용하면 인자가 하나뿐인 메소드를 간편하게 호출 가능
    • 구조 분해 선언(destructuring declaration)을 사용하면 복합적인 값을 분해해서 여러 번수에 나눠 담을 수 있다.

3.4.1 자바 컬렉션 API 확장

val strings: List<String> = listOf("first", "second")
strings.last()

fun <T> List<T>.last(): T { /* 마지막 원소를 반환함 */ }
  • last는 확장 함수로 작성 됨

3.4.2 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

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

fun listOf<T>(vararg values: T) : List<T> { ... }
배열에 들어있는 원소를 가변 길이 인자로 넘길 때
fun main(args: Array<String>) {
    val list = listOf("args: ", *args)  // 스프레드 연산자가 배열의 내용을 펼쳐준다.
}

3.4.3 값의 쌍 다루기: 중위 호출과 구조 분해 선언

중위 호출
  • 중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메소드 이름을 넣는다.
  • val map = mapOf(1 to “one”, 7 to “seven)
  • 다음 두 호출은 동일
    • 1.to(“one”) // 일반적인 방식으로 호출
    • 1 to “one” // 중위 호출 방식으로 호출
  • 인자가 하나뿐인 일반 메소드나 인자가 하나뿐인 확장 함수에 중위 호출 사용 가능
  • 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다
    • to 함수 정의
      • infix fun Any.to(other: Any) = Pair(this, other)
구조 분해 선언
  • Pair -> 두 원소로 이뤄진 순서쌍 표현
  • Pair 의 내용으로 두 변수 즉시 초기화 가능
    • val (number, name) = 1 to “one”
  • 컬렉션의 구조 분해 선언과 조합
    • for ((index, element) in collection.withIndex()) {
      • println(“$index: $element”)
    • }

3.5 문자열과 정규식 다루기

3.5.1 문자열 나누기

"12.345-6.A".split("\\.|-".toRegex())   // 정규식을 명시적으로 만든다.
"12.345-6.A".split(".", "-")    // 여러 구분 문자열을 지정한다.
  • 코틀린 확장 함수는 자바에 있는 단 하나의 문자만 받을 수 있는 메소드를 대신한다.

3.5.2 정규식과 3중 따옴표로 묶은 문자열

경로 파싱에 정규식 사용하기
fun parsePath(path: String) {
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if(matchResult != null) {
        val (directory, filename, extension) = matchResult.destructured
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}

parsePath("/Users/yole/kotlin-book/chapter.adoc")
=> Dir: /Users/yole/kotlin-book, name: chapter, ext:adoc
  • 3중 따옴표 문자열에서는 역슬래시()를 포함한 어떤 문자도 이스케이프할 필요가 없다.

3.5.3 여러 줄 3중 따옴표 문자열

  • 3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이나 그대로 들어간다.
ASCII 아트(글자만 사용해 그린 그림) 출력
val kotlinLogo = """| //
                   .| //
                   .|/ \"""
println(kotlinLogo.trimMargin("."))
=> 
| //
| //
| / \

3.6 코드 다듬기: 로컬 함수와 확장

코드 중복을 보여주는 예제

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if(user.name.isEmpty()) {
        throw IllegalArgumentException(
            "Cant't save user ${user.id}: empty Name")
        )
    }

    if(user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Cant't save user ${user.id}: empty Address")
        )
    }
}

로컬 함수에서 바깥 함수의 파라미터 접근하기

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    fun validate(value: String, fileName: String) {
        if(value.isEmpty()) {
            throw IllegalArgumentException(
                "Cant't save user ${user.id}: empty $fieldName")    // 바깥 함수의 파라미터에 직접 접근 가능
            )
        }
    }
    validate(user.name, "Name")
    validate(user.address, "Address")
}

검증 로직을 확장 함수로 추출하기

class User(val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
    fun validate(value: String, fileName: String) {
        if(value.isEmpty()) {
            throw IllegalArgumentException(
                "Cant't save user ${user.id}: empty $fieldName")    // 바깥 함수의 파라미터에 직접 접근 가능
            )
        }
    }
    validate(user.name, "Name")
    validate(user.address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()   // 확장 함수 호출
}
  • 검증 로직은 User를 사용하는 다른 곳에서는 쓰이지 않는 기능이기 때문에 User에 포함시키지 않는다.