ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코틀린 - 함수의 정의와 호출
    Kotiln 2019. 7. 24. 11:23

    코틀린에서 컬렉션 만들기

    setOf, arrayListOf, hashMapOf 등의 함수를 이용하여 컬렉션을 생성할 수 있다.
    코틀린은 자신만의 컬렉션을 기능을 제공하지 않는다. 기존 자바의 컬렉션을 활용할 수 있다.

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

    이름 붙인 인자

    함수 호출 부분의 가독성을 늘리기 위해 인자에 이름을 붙일 수 있다.

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

    호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.

    디폴트 파라미터 값

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

    디폴트 파라미터를 설정하면 함수를 호출할 때 모든 인자를 쓸 수도 있고 일부를 생략할 수도 있다.

    코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다. 자바에서 코틀린 함수를 자주 호출해야 한다면 자바 쪽에서 @JvmOverloads 애노테이션을 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 만지막 파라미터로부터 파라미터를 하나씩 생략한 오버로딩한 자바 메소드를 추가해준다.

    최상위 함수와 프로퍼티

    코틀린에서는 함수를 최상위 수준에 위치시킬 수 있다.
    코틀린 파일의 모든 최상위 함수는 이 클래스의 정적인 메소드가 된다.

    코틀린 최상위 함수가 포함되는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 애노테이션을 추가한다. @JvmName 애노테이션은 파일의 맨 앞, 패키지 이름 선언 이전에 위치해야 한다.

    @file:JvmName("StringFunctions")
    package strings
    fun joinToString(...): String { ... }

    자바에서는 다음과 같이 호출할 수 있다.

    import strings.StringFunctions;
    Stringfunctions.joinToString(list, ",", "","");

    함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다.
    기본적으로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메소드를 통해 자바 코드에 노출된다. (val의 경우 getter, var 의 경우gettersetter가 생성된다. )
    const 변경자를 추가하면 프로퍼티를 public static final 필드로 컴파일 하게 만들 수 있다. (단, 원시타입과 String 타입의 프로퍼티만 const로 지정할 수 있다.

    확장 함수와 확장 프로퍼티

    확장 함수extension function는 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수이다.
    확장 함수를 만들려면 춤가하는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다. 클래스의 이름을 객체 타입receiver type 이라 부르며 확장 함수가 호출되는 대상이 되는 값을 수신 객체 receiver object 라고 부른다.

    fun String.lastChar() : Char = this.get(this.length - 1 )
        ------                      ----     ----
          수신 객체 타입                     수신 객체

    일반 메소드의 본문에서 this를 사용할 때와 마찬가지로 확장 함수 본문에도 this를 쓸 수 있다. 마찬가지로 생략할 수 있다.
    클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할수 있는 private 멤버나 protected 멤버를 사용할 수 없다.

    임포트와 확장 함수

    확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야 한다.

    import strings.lastChar

    as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.

    import strings.lastChar as last
    val c = "Kotlin".last()

    자바에서 확장 함수 호출

    내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드다. 자바에서 호출할 때는 첫번째 인자로 수신 객체를 넘기기만 하면 된다. 확장 함수를 StringUtil.kt 파일에 정의했다면 다음과 같이 호출할 수 있다.

    char c = StringUtilKt.lastChar("Java");

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

    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 =. istOf(1, 2, 3)
    println(list.joinToSTring(separator = "; ", prefix= "(", postfix = ")"))
    
    > (1;2;3)  

    joinToString 메소드를 Collection<T> 의 멤버인 것처럼 호출할 수 있다.

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

    확장 함수는 클래스의 일부가 아니며 클래스 밖에 선언된다.
    확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.

    정적은 컴파일 시점을 의미하고 동적은 실행 시점을 의미한다.

    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!")
    
    val view: View = Button()
    view.showOff()
    
    > I'm a view!

    view가 가리키는 객체의 실제 타입이 Button이지만, 이 경우 view의 타입이 View 이기 때문에 무조건 View의 확장 함수가 호출된다.

    어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다.

    확장 프로퍼티

    확장 프로퍼티 를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
    확장 프로퍼티는 실제로 아무 상태도 가질 수 없다.

    val String.lastChar: Char
        get () = get(length - 1)
    
    println("abcd".lastChar)
    
    > d

    확장 함수는 뒷받침하는 필드가 없어서 getter 는 꼭 정의해야 하며 초기화 코드는 쓸 수 없다.

    var StringBuilder.lastChar: Char
        get() = get(length - 1)
        set(value: Char) {
            this.setCharAt(length - 1, value)
        }
    
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = "!"
    println(sb)
    
    > Kotlin!

    자바에서 확장 프로퍼티를 사용하고 싶다면 항상 StringUtilKt.getLastChar(“Java”)처럼 gettersetter를 명시적으로 호출해야 한다.

    컬렉션 처리

    자바 컬렉션 API 확장

    val strings: List<String> = listOf("first", "second", "fourteenth")
    
    strings.last()
    > fourteenth
    
    val numbers: Collction<Int> = setOf(1, 14, 2)
    numbers.max()
    > 14

    lastmax는 확장 함수이다.

    가변 인자 함수

    자바의 가변 길이 인자 는 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
    자바에서는 타입 뒤에 ... 을 붙였지만 코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.

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

    이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 스프레드spread 연산자가 그런 작업을 해준다.
    전달하려는 배열 앞에 *를 붙이기만 하면 된다.

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

    중위 호출과 구조 분해 선언

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

    여기서 to 는 코틀린 키워드가 아닌 중위 호출infix call 이라는 방식으로 to 라는 일반 메소드를 호출한 것이다.
    중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다. (이때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 한다.)

    다음 두 호출은 동일하다.

    1.to("one")
    1 to "one"

    인자가 하나뿐인 일반 메소드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
    함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

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

    Pair는 코틀린 표 준 라이브러리 클래스로 두 원소로 이뤄진 순서쌍을 표현한다.
    Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.

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

    이런 기능을 구조 분해 선언restructuring declaration 라고 한다.

    문자열과 정규식 다루기

    문자열 나누기

    자바 split 메소드의 구분 문자열은 실제로는 정규식이기 때문에 . 을 사용해 문자열을 분리할 수 없다.
    코틀린에서는 자바의 split 대신에 여러 가지 조합의 파라미터를 받는 split 확장 함수를 제공함으로써 혼동을 야기하는 메소드를 감춘다.
    정규식을 파라미터로 받는 함수는 String 이 아닌 Regex 타입의 값을 받는다.

    println("12.345-6.A".split("\\.|-".toRegex()))
    > [12, 345, 6, A]

    간단한 경우는 정규식을 사용하지 않아도 된다.

    println("12.345-6.A".split('.', '-')
    > [12, 345, 6, A]

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

    코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다.
    "/Users/yole/kotlin-book/chapter.adoc" 경로를 파싱해보자

    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")
    }
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
    
    > Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc

    같은 작업을 정규식을 활용해서 해보자

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

    3중 따옴표 문자열을 사용해 정규식을 썼다. 3중 따옴표 문자열에서는 \를 포함한 어떤 문자도 이스케이프할 필요가 없다.

    여러 줄 3중 따옴표 문자열

    3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이나 이스케이프 없이 그대로 들어간다.

    val kotlinLogo = """|  //
                       .| //
                       .|/ \"""
    println(kotlinLogo.trimMargin("."))
    >|  //
    >| //
    >|/ \

    들여쓰기의 끝부분을 특별한 문자열로 표시하고 trimMargin을 사용해 그 문자열과 그 직전의 공백을 제거한다.
    3중 따옴표 문자열 안에 $를 넣어야 한다면 var price = “””${‘$’}99.9””” 처럼 문자열 템플릿 안에 '$' 문자를 넣어야 한다.

    로컬 함수와 확장

    코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다.

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

    위의 코드에서 검증하는 부분이 중복된다. 로컬 함수로 분리하여 중복 코드를 없애면 다음 코드와 같다

    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}: empty $fieldName")
            }
        }
        validate(user, user.name, "Name")
        validate(user, user.address, "Address")
    }        

    로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다.
    따라서 아래의 코드로 바꿀 수 있다.

    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}: empty $fieldName")
            }
        }
        validate(user.name, "Name")
        validate(user.address, "Address")
    }        

    또는 검증 로직을 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")
            }
        }
        validate(user.name, "Name")
        validate(user.address, "Address")
    }
    
    fun saveUser(user: User){
        user.validateBeforeSave()
    }        
    반응형

    댓글

Designed by Tistory.