코틀린 스터디 3장 정리 - 함수 정의와 호출

Posted by 김순철 on October 14, 2018

Chapter 3. 함수 정의와 호출

스터디 날짜 : 2018-10-04(목)

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") /* 맵 */

/* 코틀린에서 사용하는 컬렉션은 자바의 컬렉션이다. */
>>> println(set.javaClass)
class java.util.HashSet
>>> println(list.javaClass)
class java.util.ArrayList
>>> println(map.javaClass)
class java.util.HashMap

/* 자바에는 없는 컬렉션 기능을 제공한다. */
>>> val strings = listOf("first", "second", "fourteenth")
>>> println(string.last()) /* 마지막 요소를 반환 */
fourteenth

>>> val numbers = setOf(1, 14, 2)
>>> println(numbers.max()) /* 최대값 반환 */
14

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

>>> val list = listOf(1, 2, 3)
>>> println(list)
[1, 2, 3]
  • 아래 소스는 컬렉션의 원소 사이에 구분자(Separator)를 추가하고, 접두사(Prefix)와 접미사(Postfix)를 추가한다.
fun <T> joinToString(collection: Collection<T>,
                    separator: String,
                    prefix: String,
                    prefix: 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)

3.2.1 이름 붙인 인자

  • 위 함수를 joinToString(collection, " ", " ", ".")와 같이 호출한다고 하면 각 문자열이 무슨 역할을 하는지 구분이 모호하다.
  • 자바에서는 아래와 같이 코딩하기도 한다.
/* 자바 */
joinToString(collection, /* seprarator */ " ", /* prefix */ " ", /* postfix */ ".");
  • 코틀린에서는 다음과 같이 표현 할 수 있다.
/* 코틀린 */
joinToString(collection, separator = " ", prefix = " ", postfix = ".")

3.2.2 디폴트 파라미터 값

  • 코틀린에서는 함수의 파라미터에 디폴트 값을 지정할 수 있다.
fun <T> joinToString(collection: Collection<T>,
                    separator: String = ", ", /* 디폴트 값 지정 */
                    prefix: String = "",
                    prefix: String = ""
) : String

/* 디폴트 값 선언으로 아래와 같이 파라미터를 생략하여 호출 가능하다 */
>>> joinToString(list)
1, 2, 3
>>> joinToString(list, "; ")
1; 2; 3

/* 인자의 이름을 붙여 순서와 관계 없이 호출할 수 있다. */
>> joinToString(list, postfix = ";", prefix = "#")
# 1, 2, 3;
  • 자바에서는 디폴트 파라미터 개념이 없어, 코틀린 함수를 자바에서 호출하는 경우에는 모든 인자를 명시해야 한다.
  • 또는 @JvmOverloads 애노테이션을 추가하게 되면 코틀린 컴파일러가 자동으로 오버로딩한 자바 메소드를 생성 해준다.

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

  • 코틀린은 클래스 밖에 함수를 위치 시키는 것이 가능하다. 자바는 클래스 안에 모든 메소드를 선언해야 한다.
  • joinToString 함수를 strings 패키지에 넣는 방법은 아래와 같다.
/* join.kt */
package strings

fun joinToString(...): String { ... }
  • 위 코틀린 코드를 자바 코드 변환하면 아래와 같다.
package strings;

pulbic class JoinKt { /* 코틀린 파일명으로 클래스 이름 생성 */
    public static String joinToString(...) {
        ...
    }
}

최상위 프로퍼티

  • 함수와 마찬가지로 프로퍼티도 최상위 수준에 놓을 수 있다.
var opCount = 0

fun performOperation() {
    opCount++
    // ...
}
  • 기본적인 최상위 프로퍼티도 게터와 세터로 접근해야 한다.
  • 상수처럼 보이는 값의 경우는 const 변경자를 추가하면 자바의 public static final 필드로 컴파일 해준다.
const val UNIX_LIN_SEPARATOR = "\n"

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

  • 확장 함수Extension function는 기존 자바 API를 재작성하지 않고 기능을 추가할 수 있는 것을 말한다.
/* 확장 함수를 통해 문자열의 마지막 문자를 돌려주는 메소드 추가 */
package strings

fun String.lastChar(): Char = this.get(this.length - 1)
  • 확장 함수를 만드려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.
  • 클래스 이름을 수신 객체 타입receiver type, 확장 함수가 호출되는 대상이 되는 값을 수식 객체receiver object 라고 한다.
fun String.lastChar(): Char = this.get(this.length - 1)
                                       
   수식 객체 타입                수신객체   수신객체


/* 호출은 일반 클래스 멤버를 호출하는 구문과 동일하다 */
>>> println("kotlin".lastChar())
n
  • 함수 본문에서 this 생략이 가능하다.
package strings

/* 확장 함수 본문에는 this를 생략할 수 있다. */
fun String.lastChar(): Char = get(length - 1)
  • 확장 함수는 기존 메소드 내부의 캡슐화를 깨지 않는다.
  • 기존 메소드의 privateprotected 멤버를 사용할 수 없다.

3.3.1 임포트와 확장 함수

  • 확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수 있지 않다.
  • 임포트는 아래와 같이 한다.
import strings.lastChar
val c = "kotlin".lastChar()
import strings.*
val c = "kotlin".lastChar()
import strings.lastChar as last
val c = "kotlin".last()
  • 확장 함수는, 자바에서는 전체 패키지 경로를 사용하여 호출 가능 하지만, 코틀린의 경우는 반드시 import를 사용해야 하며, 다른 패키지에 같은 이름의 함수가 있는 경우는 as를 사용하여 함수 이름을 바꾸는 것 유일한 방법이다.

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

 /* joinToString 함수의 최종 버전 */
 
 fun <T> Collection<T>.joinToString(
     separator: String = ", ",
     prefix: String = "",
     postfix: String = ""
 ): String {
     val result = StringBuilder(prefix)

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

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

 >>> val list = listOf(1, 2, 3)
 >>> println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
 (1; 2; 3)

 >>> val list2 = arrayListOf(1, 2, 3)
 >>> println(list.joinToString(" "))
 1 2 3
  • 특정 타입에 대한 수신 객체 타입을 지정할 수 있다. 문자열 컬렉션에 대해서만 호출할 수 있는 join 함수는 아래와 같다.
 fun Collection<String>.join(
     separator: String = ", ",
     prefix: String = "",
     postfix: String = ""
 ) = joinToString(separator, prefix, postfix)

 >>> println(listOf("one", "two", "eight").join(" "))
 one two eight

/* 정수 타입의 컬렉션에 대해서는 오류가 발생한다. */
 >>> listOf(1, 2, 8).join()
 Error: Type mismatch: inferred type is List<Int> but Collection<String>

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

  • 확장 함수는 오버라이드 불가능하다.
 /* 일반적인 오버라이드를 구현한 코드 */
 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!

### 3.3.5 확장 프로퍼티

  • 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가 할 수 있다.
  • 하지만 기존 클래스에 필드를 추가할 방법이 없으므로 상태 저장은 불가능하다.
 /* 확장 프로퍼티 선언하기 */
 val String.lastChar: Char
    get() = get(length -1)
 /* 변경 가능한 확장 프로퍼티 선언하기 */
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!

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

  • vararg 키워드를 사용하면 함수의 인자 개수에 관계없이 호출 가능한 함수를 정의할 수 있다.
  • 구조 분해 선언destructuring declaration을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.

3.4.1 자바 컬렉션 API 확장

val strings: List<String> = listOf("first", "second", "fourteenth")
>>> strings.last()
fourteenth
>>> val numbers: Collection<Int> = setOf(1, 14, 2)
>>> numbers.max()
14
  • 위 코드에서 lastmax는, 자바에는 없는, 코틀린에서 Java 컬렉션에 추가한 확장 함수이다.

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

  • 리스트를 생성하는 코드를 보면 함수를 호출할 때 매개변수의 개수가 일정하지 않다.
val list = listOf(2, 3, 5, 7, 11)
  • 라이브러리에서 이 함수의 정의를 보면 아래와 같다.
fun listOf<T>(vararg values: T): List<T> { ... }
  • 코틀린에서는 가변 길이 인자vararg를 생성할 때는 vararg 키워드를 붙이면 매개변수는 개수에 관계 없이 인자를 받을 수 있다. 자바의 경우 타입 뒤에 ...을 붙인다.
  • 가변 길이 인자에 배열을 넣는 경우 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어 전달해야 하며, 이를 가능하게 해주는 스프레드spread 연산자 사용하면 된다.
fun main(args: Array<String>) {
    val list = listOf("args: ", *args)
    println(list)
}

3.4.3 값의 쌍 다루기: 구조 분해 선언

  • 구조 분해 선언destructuring declaration을 사용하면, 아래 예제와 같이 index와 value를 동시에 변수에 담을 수 있다.
for ((index, element) in collection.withIndex()) {
    println("${index}: ${element}")
}

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

  • 코틀린에서는 함수를 함수 내부에 중첩시킬 수 있다. 이를 로컬 함수라고 부르며, 코드 중복을 제거하는데 유용하게 쓰인다.
/* 코드 중복을 보여주는 예 */

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

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")
    }
    
    ...
}

>>> saveUser(User(1, "", ""))
java.lang.IllegalArgumentException: Can't save user 1: empty Name
  • 위 코드를 로컬 함수로 변경하면 중복을 없애는 동시에 코드 구조를 깔끔하게 유지할 수 있다.
/* 로컬 함수를 사용해 코드 중복 줄이기 */

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

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {  /* 필드 검증하는 로컬 함수를 선언 */
        if(value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: "  /* 바깥 함수의 파라미터(user)에 직접 접근 가능하다. */
                 +"empty ${fieldName}")
        }
    }

    validate(user.name, "Name")    
    validate(user.address, "Address")    
    ...
}
  • 위 예제를 좀 더 개선하고 싶은 경우 validate함수를 User 클래스의 확장 함수로 만들 수도 있다.
/* 검증 로직을 확장 함수로 추출하기 */

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

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if(value.isEmpty()) {
            throw IllegalArgumentException(
                "Can't save user ${id}: empty ${fieldName}") /* User의 프로퍼티를 직접 사용할 수 있다. */ 
        }
    }

    validate(user.name, "Name")    
    validate(user.address, "Address")    
}

fun saveUser(user: User) {
    user.validateBeforeSave()  /* 확장 함수 호출 */
    ...
}