본문 바로가기

Kotlin

Kotlin In Action - 3장.함수 정의와 호출

  • 함수 정의, 호출 방법
  • 확장 함수와 프로퍼티를 사용해 자바 라이브러리를 코틀린 스타일로 적용하는 방법

컬렉션 만들기

val set = hashSetOf(1,7,53)
val list = arrayListOf(1,7,53)
val map = hashMapOf(1 to "one", 2 to "two")

println(set.javaClass) // class java.util.HashSet
  • 기존 자바 컬렉션 사용
  • 이렇게 사용하는 이유는 자바 코드와 상호작용하기가 더 쉽기 때문
  • 아래와 같이 추가 기능 제공
println(strings.last()) // list
println(numbers.max()) // set

kotlin-collections의 소스이다.

/**
 * Returns an empty read-only set.  The returned set is serializable (JVM).
 * @sample samples.collections.Collections.Sets.emptyReadOnlySet
 */
@kotlin.internal.InlineOnly
public inline fun <T> setOf(): Set<T> = emptySet()

코틀린 답게 함수 만들기

자바에 익숙한 나는 처음에 함수를 아래와 같이 만들 것이다

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()
}
joinToString(collection, separator=" ",prefix=" ",postfix=".") // 가독성 증가

디폴트 파라미터 값 - 자바에서는 생성자 파라미터 별로 많이 만들어야 함...

 

코틀린에서는 아래와 같이 디폴트 파라미터 값을 정할 수 있고, 파라미터의 순서도 바꿀 수 있다.

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

joinToString(list, postfix=";", prefix="# ")
디폴트 값과 자바?
자바에는 디폴트 파라미터 개념이 없는데, 자바->코틀린 호출시 어떻게 처리 해야 하는가?

@JvmOverloads
fun joinToString(.... )

이라고 명시 해놓으면
코틀린 컴파일러가 자동으로 마지막 파라미터로부터 하나씩 생략한 오버로딩한 자바 메서드를 자동으로 만들어준다

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

자바에서 많이 보던 Util 클래스들은 코틀린에서 필요가 없다

package strings

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


// java compile 된 소스

package strings

public class JointKt {
    public static String jointToString(...) { ... }
}
@file:JvmName("Stringfunctions")와 같이 명시해주면 컴파일시 해당 파일명을 이용한다 ( 10장서 부연 설명 )
const val UNIX_LINE_SEPARATOR = "\n" // 최상위 프로퍼티

// java compiled
public static final String UNIX_LINE_SEPARATOR = "\n"

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

코틀린의 핵심 목표 중 하나 - 기존 코드와 코틀린 코드 자연스럽게 통합하기

그러기 위해서는 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러가지 편리한 기능을 사용할 수 있게 해줘야 한다.

확장함수가 그 역할을 해준다!

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

println("Kotlin".lastChar())
  • String : 수신 객체 타입 ( receiver type )
  • this : 수신 객체 ( receiver object )
  • 확장 함수 내부에서 수신 객체의 메소드나 파라미터를 바로 사용할 수 있음 -> But, 캡슐화를 깨뜨리는 건 아니다! 확장 함수내에서는 private, protected 접근 불가

기존 예제에 확장 함수를 정의하여

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

    for ((index, element) in this.withIndex()) { // 수신 객체 this
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

확장 함수는 오버라이드 불가

  • 코틀린은 확장 함수를 정적으로 결정하기 때문에 호출하는 방식은 정적 디스패치이다.
  • 기존 함수 이름 = 확장함수 이름 이라면? 기존 함수가 우선순위가 높기 때문에 확장 함수 이름 지을 때 주의해야 함
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가 출력


// compiled java
View view = new Button();
ExtensionKt.showOff(view); // showOff 함수를 Extensions.kt 파일에 작성

 

확장 프로퍼티

  • 프로퍼티 추가가 가능
  • But, 상태를 저장할 적절한 방법이 없기 때문에 아무 상태도 가질 수 없음
  • 왜 사용? 프로퍼티 문법으로 코드를 더 짧게 작성할 수 있음
val String.lastChar: Char
   get() = get(length-1)
   
var StringBuilder.lastChar: Char
   get() = get(length-1) // backing filed 사용 불가
   set(value:Char) {
      this.setCharAt(length-1,value)
   }
 

컬렉션 처리 1 - 자바 컬렉션 API 확장

last(), max() 함수가 기존 자바 라이브러리에 추가가 되었는데, 위에서 본 확장 함수였던 것이다!

 

컬렉션 처리 2- 가변 인자 함수

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

fun main(args: Array<String>) {
	val list = listOf("args : ", *args) // spread 연산자가 배열의 내용을 펼쳐줌
    println(list)
}

컬렉션 처리 3- 중위 호출과 구조 분해 선언

val map = mapOf(1 to "one")

// to 정의 코드 간략본
infix fun Any.to(other:Any) = Pair(this, other)
  • "to" 메소드는 중위 호출(infix call)
TODO - infix call 잘 쓴 코드? 실 활용 예 추가하기

아래와 같은 기능을 구조분해선언 ( destructuring declaration ) 이라 한다.

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

문자열과 정규식 다루기

println("12.345-6.A".split(".","-"))  // 12,345,6,A

var kotlinLogo = """|  //
                   .| //
                   .|//"""
                   
                   
println(kotlinLogo.trimMargin("."))

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

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

fun saveUser(user: User) {
    fun validate(user: User, value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can`t save user ${user.id}, emtpy ${fieldName}")
        }
    }

    validate(user, user.name, "Name")
    validate(user, user.address, "Address")
    // user 데이터베이스에 저장
}

// REFACTOR 2 로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수 사용 가능
fun saveUser2(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can`t save user ${user.id}, emtpy ${fieldName}")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
    // user 데이터베이스에 저장
}


// REFACTOR 3 - 확장 함수 ( 응집도 해치는거 아닌가? User 내부 코드 간결 + 사용하는쪽에서만 )
fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can`t save user ${id}, emtpy ${fieldName}")
        }
    }
    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser3(user: User) {
    user.validateBeforeSave()
    
    // user 데이터베이스에 저장
}
  • Refactor 3단계 - 확장 함수 이용하기
    • 자바 관점 : User에다가 선언해서 응집도를 높여야 하는거 아닌가? => 코틀린 관점 : 여기서만 사용한다면 User 내부 코드는 간결하게 하고, 사용하는 쪽에서만 확장 함수를 이용하자!