Chapter 3. 함수 정의와 호출

sua·2021년 7월 12일
0

Kotlin In Action

목록 보기
3/9
post-thumbnail

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

fun main(args: Array<String>) {
    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) // javaClass = getClass()
    println(list.javaClass)
    println(map.javaClass)
}

/* <결과>
class java.util.HashSet
class java.util.ArrayList
class java.util.HashMap
*/
  • 코틀린 자체 컬렉션을 제공하지 않고 기존 자바 컬렉션을 활용 but, 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있음
    [예시]
val strings = listOf("first", "second", "fourteenth")
println(strings.last()) // fourteenth
val numberes = setOf(1, 14, 2)
println(numbers.max()) // 14



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

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 main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, "; ", "(", ")")) // (1; 2; 3)
}

3.2.1 이름 붙인 인자

코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부(또는 전부)의 이름 명시 가능

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, separator = "; ", prefix = "(", postfix = ")")) // (1; 2; 3)
}

3.2.2 디폴트 파라미터 값

함수 선언에서 파라미터의 디폴트 값을 지정 가능 -> 함수 호출할 때 인자 일부 생략 가능해짐

fun <T> joinToString (
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String
fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list)) // 1, 2, 3
    println(joinToString(list, "; ")) // 1; 2; 3
}

이름 붙인 인자 사용하면 지정하고 싶은 인자를 이름 붙여서 순서와 관계없이 지정 가능

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, postfix = ";", prefix = "# ")) // # 1, 2, 3;
}

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

  • 최상위 함수 : 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치 시킨 함수
[Join.kt]
package strings

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

[TopLevelFunction.kt]
package ch03

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(strings.joinToString(list))
}
  • 최상위 프로퍼티 : 소스 파일의 최상위 수준에 놓인 프로퍼티
var opCount = 0 // 최상위 프로퍼티 선언

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

fun reportOperationCount() {
    println("Operation performed $opCount times") // 최상위 프로퍼티 값 읽음
}

fun main(args: Array<String>) {
    reportOperationCount() // Operation performed 0 times
    performOperation()
    reportOperationCount() // Operation performed 1 times
}

-> 최상위 프로퍼티로 코드에 상수 추가 가능

val UNIX_LINE_SEPARATOR = "\n" // 게터가 생김
const val UNIX_LINE_SEPARATOR = "\n" // == public static final 필드로 컴파일하게 함 -> 게터 생기지 x




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

확장 함수 : 클래스 밖에 선언되어있지만 어떤 클래스의 멤버메소드인 것처럼 호출할 수 있는 함수
수신 객체 타입 : 확장 함수를 만들기 위해 함수 이름 앞에 덧붙이는 함수가 확장할 클래스 이름 / 확장이 정의될 클래스의 타입
수신 객체 : 확장 함수가 호출되는 대상이 되는 값(객체) / 클래스에 속한 인스턴스 객체

[Extension.kt]
package strings

fun String.lastChar() : Char = this.get(this.length - 1)
[ExtensionFunction.kt]
package ch03

import strings.lastChar

fun main(args: Array<String>) {
    println("Kotlin".lastChar()) // n
}

확장 함수 안에서는 클래스 내부에서만 사용 가능한 private 멤버나 protected 멤버 사용 불가

3.3.1 임포트와 확장 함수

확장 함수를 사용하기 위해서는 그 함수를 임포트해야함

  • import 패키지명.함수명
  • import 패키지명.*
  • import 패키지명.함수명 as 다른 이름 : 임포트한 클래스타 함수를 다른 이름으로 부를 수 있음
[ExtensionFunction.kt]
package ch03

//import strings.lastChar
// import string.*
import strings.lastChar as last

fun main(args: Array<String>) {
    //println("Kotlin".lastChar()) // n
    println("Kotlin".last()) // n
}

3.3.2 자바에서 확장 함수 호출

정적 메소드를 호출하면서 첫 번째 인자로 수신 객체를 넘겨야함

  • 파일이름.함수(수신객체);
char c = StringUtilKt.lastChar("Java"); // StringUtil.kt 파일에 정의된 확장 함수 호출

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

[JoinToStringExtensionFunction.kt]
package ch03

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

fun Collection<String>.join (
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) = joinToString(separator, prefix, postfix)

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(list.joinToString(separator = "; ", prefix = "(", postfix = ")"))
    println(listOf("one", "two", "eight").join(" ")) // one two eight
    // listOf(1, 2, 8).join() : 오류. 객체의 리스트에 대해 호출할 수 x
}

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

확장 함수를 하위 클래스에서 오버라이드 할 수 x -> 정적 메소드와 같은 특징, 호출될 확장 함수를 정적으로 결정하기 때문에
확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메소드로 컴파일하기 때문에

[OverrideExtensionFunctionTest.kt]
package ch03

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

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

fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")

fun main(args: Array<String>) {
    val view: View = Button()
    view.click() // Button clicked

    val view2: View = Button()
    view2.showOff() // I'm a view! => 확장 함수는 정적으로 결정됨
}

3.3.5 확장 프로퍼티

확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API 추가 가능 -> 더 짧게 코드 작성 가능해짐

  • 뒷받침 필드가 x -> 기본 게터 구현 제공 x -> 게터는 꼭 정의해야 함
  • 초기화 코드에서 계산 값 담을 장소 x -> 초기화 코드 쓸수 x
[확장 프로퍼티 선언하기]
val String.lastChar: Char
    get() = get(length - 1)

[변경 가능한 확장 프로퍼티 선언하기]
var StringBuilder.lastChar: Char 
    get() = get(length - 1) // 프로퍼티 게터
    set(value: Char) {
        this.setCharAt(length - 1, value) // 프로퍼티 세터
    }
[Extension.kt]
package strings

// 확장 프로퍼티 선언
val String.lastChar: Char
    get() = get(length - 1)

var StringBuilder.lastChar: Char
    get() = get(length - 1) // 프로퍼티 게터
    set(value: Char) {
        this.setCharAt(length - 1, value) // 프로퍼티 세터
    }
    
[ExtensionPeroperty.kt]
package ch03

import strings.lastChar

fun main(args: Array<String>) {
    println("Kotlin".lastChar) // n

    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb) // Kotlin!
}



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

3.4.1 자바 컬렉션 API 확장

코틀린 컬렉션이 더 확장된 API를 제공하는 방법 -> 확장 함수

[last, max 함수]
fun <T> List<T>.last() : T {
	/* 마지막 원소를 반환함 */
}
fun Collection<Int>.max() : Int {
	/* 컬렉션의 최댓값을 찾음 */
}

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

가변 길이 인자(varargs) : 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 컴파일러가 배열에 그 값들을 넣어주는 기능

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

스프레드 연산자 - 배열에 들어있는 원소를 가변 길이 인자로 넘길 때 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 함

fun main(args: Array<String>) {
	val list = listOf("args: ", *args)
    	println(list)
}

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

중위 호출 : 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는 것

1.to("one") // "to" 메소드를 일반적인 방식으로 호출
1 to "one" // "to" 메소드를 중위 호출 방식으로 호출
  • infix 변경자를 함수 선언 앞에 추가 -> 함수를 중위 호출에 사용하게 허용
[to 함수의 정의 요약한 코드]
infix fun Any.to(other: Any) = Pair(this, other) // Pair는 두 원소로 이뤄진 순서쌍을 표현

구조 분해 선언 : 구조를 해제하여 그 값을 각각 변수에 할당하는 것

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

for ((index, element) in collection.withIndex()) {
	println("$index: $element")
}



3.5 문자열과 정규식 다루기

3.5.1 문자열 나누기

  1. 정규식을 명시적으로 만들어서 분리
println("12.345-6.A".split("\\.|-".toRegex())) // [12, 345, 6, A]
  • toRegex 확장 함수 : 문자열을 정규식으로 변환
  1. split 확장 함수 오버로딩한 버전 사용하기 - 구분 문자열을 하나 이상 인자로 받는 함수 있음
println("12.345-6.A".split(".", "-")) // [12, 345, 6, A]

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

  1. String 확장 함수를 사용해 경로 파싱
  • 문자열.substringBeforeLast("구분 문자열") : 해당 문자열에서 마지막 구분 문자열이 나오기 전까지의 문자열을 리턴
  • 문자열.substringAfterLast("구분 문자열") : 해당 문자열에서 마지막 구분 문자열 이후의 문자열만 리턴
// String 확장 함수를 사용해 경로 파싱
fun parsePath(path: String) {
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")
    val fileName = fullName.substringBeforeLast(".")
    val extension = fullName.substringAfterLast(".")

    println("Dir: $directory, name: $fileName, ext: $extension")
}

fun main(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
    // Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
}
  1. 정규식을 사용해 경로 파싱
  • 3중 따옴표 문자열 : 역슬래시를 포함한 어떤 문자도 이스케이프할 필요가 없음
  • 패턴 . : 임의의 문자와 매치됨
[경로를 디렉터리, 파일 이름, 확장자로 분리하는 정규식]
"""(.+)/(.+)\.(.+)""" 
/ : 마지막 슬래시
\. : 마지막 점(.)
첫번째 (.+) : 마지막 슬래시까지 모든 문자
두번째 (.+) : 마지막 마침표 전까지 모든 문자
세번째 (.+) : 마지막 마치푬 후의 모든 문자
// 정규식을 사용해 경로 파싱
fun parsePath2(path: String) {
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path) // 정규식을 인자로 받은 path에 매치시킴
    if (matchResult != null) { // 매치에 성공하면
        // 매치 결과를 의미하는 destructured 프로퍼티를 구조 분해
        val (directory, filename, extension) = matchResult.destructured
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}

fun main(args: Array<String>) {
    parsePath2("/Users/yole/kotlin-book/chapter.adoc")
    // Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
}

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

3중 따옴표 문자열 - 이스케이프를 피하기 위해서만 사용하지 x -> 줄 바꿈을 표현하는 아무 문자열이나 이스케이프 없이 그대로 들어감

[3중 템플릿 안에 문자열 템플릿 사용]

  • 3중 따옴표 문자열 안에 $ 넣어야 할때
val price = """${'$'}99.9"""
println(price) // $99.9



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

로컬 함수로 분리하여 중복을 없애는 동시에 코드 구조를 깔끔하게 유지 가능

로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용 가능

[로컬 함수를 사용해 코드 중복 줄이기]

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} : $fieldName" // user.id 바깥 함수의 파라미터에 직접 접근
            )
        }
    }

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

    // 로컬 함수를 호출해서 각 필드를 검증한다.
    validate(user.name, "Name")
    validate(user.address, "Address")

    // user를 데이터베이스에 저장한다.
}

fun main(args: Array<String>) {
    saveUser(User(1, "", ""))
}

[로컬 함수로 구현한 검증 로직을 확장 함수로 추출]

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

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

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

fun saveUser2(user: User2) {
    user.validateBeforeSave() // 확장 함수를 호출

    // user를 데이터베이스에 저장한다.
}

fun main(args: Array<String>) {
    saveUser2(User2(1, "", ""))
}
profile
가보자고

0개의 댓글

관련 채용 정보