ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3장 - 함수 정의와 호출
    Kotlin/코틀린인액션요약 2022. 9. 11. 00:01
    728x90

    3장에서 다루는 내용

    - 컬렉션, 문자열, 정규식을 다루기 위한 함수

    - 이름 붙인 인자, 디폴트 파라미터 값, 중위 호출 문법 사용

    - 확장 함수와 확장 프로퍼티를 사용해 자바 라이브러리 적용

    - 최상위 및 로컬 함수와 프로퍼티를 사용해 코드 구조화

     

    코틀린에서 컬렉션 만들기

    var set = hashSetOf(1,7,53) // java.util.HashSet
    var list = arrayListOf(1,7,53) // java.util.ArrayList
    var map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three") // java.util.HashMap
    
    val Strings = listOf("first","second","fourteenth");
    println(strings.last())
    //출력 : fourteenth
    
    val numbers = setOf(1,14,2)
    println(umbers.max())
    //출력: 14

    이는 코틀린이 자신만의 컬렉션 기능을 제공하지 않는다는 뜻입니다.

    하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있습니다. (last, max 등)

     

     

    코틀린이 자체 컬렉션을 제공하지 않는 이유는?

    표준 자바 컬렉션을 사용하면 자바 코드와 상호작용하기 훨씬 더 쉽습니다.

    자바에서 코틀린 함수를 호출하거나 코틀린에서 자바 함수를 호출할 때 컬렉션을 서로 변환할 필요가 없습니다.

     

    함수 호출 변환시키기

    자바 컬렉션에는 디폴트 toString 구현이 들어있습니다.

    ,로 인자를 구분해서 출력해주는 것으로 고정되어 있습니다.

     

    하지만 디폴트 구현과 달리 원소 사이를 ;으로 구분하고 싶다면 어떻게 해야 할까요?

     

    자바에서는 구아바나 아파치 커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야 합니다.

     

    하지만 코틀린에서는 이런 요구사항을 간단하게 만들 수 있습니다.

    fun main(){
        var set = hashSetOf(1,7,53) // java.util.HashSet
        println(joinToString(set, "; ","(" ,")"))
    }
    
    
    fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
    ) : String{
        var result = StringBuffer(prefix)
        for((index, element) in collection.withIndex()){
            if(index >0) result.append(separator)
            result.append(element)
        }
        result.append(postfix)
        return result.toString()
    }

     

    하지만 이렇게 함수를 호출하게 되면 인자로 어떤 값이 들어가는지 헷갈리게 됩니다.

    이런 이유로 자바에서는 빌더패턴을 사용합니다.

     

    하지만 코틀린에서는 다음과 같이 사용할 수 있습니다.

    println(joinToString(collection = set, separator = "; ", prefix = "(" , postfix = ")"))

     

    디폴트 파라미터 값

    자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있습니다.

     

    예를 들어 java.lang.Thread에는 8가지 생성자가 있습니다.

     

    코틀린에서는 함수 선언에서 파라미터 디폴트 값을 지정하여 이런 오버로드 중 상당수를 피할 수 있습니다.

     

    fun main(){
        var set = hashSetOf(1,7,53) // java.util.HashSet
        println(joinToString(set))
        println(joinToString(set,"; "))
    }
    
    
    fun <T> joinToString(
        collection: Collection<T>,
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
    ) : String{
        var result = StringBuffer(prefix)
        for((index, element) in collection.withIndex()){
            if(index >0) result.append(separator)
            result.append(element)
        }
        result.append(postfix)
        return result.toString()
    }

     

    자바에서는 디폴트 파라미터 값이라는 개념이 없어 코틀린 함수를 자바에서 호출해야 하는 경우에는 모든 인자를 명시해주어야 합니다.

     

    자바에서 코틀린 함수를 자주 호출한다면 @JvmOverloads 애노테이션을 추가하면 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 생성자를 만들어줍니다.

     

    최상위 프로퍼티

    어떤 데이터를 클래스 밖에 위치시켜야 하는 경우는 흔하지 않지만 가끔 유용할 때가 있습니다.

     

    const를 사용하여 public static final처럼 사용할 수 있습니다.

    const val UNIX_LINE_SEPARATOR = "\n"

     

     

    확장 함수와 확장 프로퍼티

    어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수입니다.

     

    다음은 어떤 문자열의 마지막 문자를 돌려주는 메서드입니다.

    package strings
    fun String.lastChar(): Char = this.get(this.length - 1)
    
    println("Kotlin".lastChar())

     

    확장 함수를 만들기 위해서는 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 됩니다.

     

    매우 편리해 보이지만 확장 함수가 private 멤버나 protected 멤버에 접근할 수 없습니다.

     

    확장 함수의 충돌

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

    as 키워드를 사용하여 import 한 클래스나 함수를 다른 이름으로 부를 수 있습니다.

    한 파일 안에서 여러 패키지에 속해있는 이름이 같은 함수를 불러와야 할 때 유용하게 사용됩니다.

     

     

    자바에서 확장 함수 호출

    StringUtil.kt 파일에 확장 함수를 정의했다면 다음과 같이 호출할 수 있습니다.

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

     

    joinToString()을 확장으로 정의하기

    fun main(){
        var set = hashSetOf(1,7,53) // java.util.HashSet
        println(set.joinToString())
        println(set.joinToString("; "))
    }
    
    
    fun <T> Collection<T>.joinToString(    
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
    ) : String{
        var result = StringBuffer(prefix)
        for((index, element) in this.withIndex()){
            if(index >0) result.append(separator)
            result.append(element)
        }
        result.append(postfix)
        return result.toString()
    }

    이제 Collection 클래스의 멤버인 것처럼 호출할 수 있습니다.

     

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

    정적 메서드와 같은 특징을 가지기 때문에 오버라이드 할 수 없습니다.

    확장 함수는 클래스의 일부가 아니라 밖에 선언됩니다.

     

    따라서 이름과 파라미터가 완전히 동일한 확장 함수를 정의해도 실제로 확장 함수를 호출할 때 수신 객체로 지정된 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되며 동적으로 결정되지 않습니다.

     

    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

     

    확장 프로퍼티

    기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있습니다.

     

    프로퍼티라는 이름으로 불리지만 상태를 저장할 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없습니다.

     

    그럼에도 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 있습니다.

    var StringBuilder.lastChar: Char
        get() = get(length -1) //프로퍼티 게터
        set(value: Char){
            this.setCharAt(length -1, value) //프로퍼티 세터
        }
    
    fun main() {
        val sb = StringBuilder("Kotlin?")
        println(sb.lastChar)
        sb.lastChar = '!'
        println(sb)
    }
    
    //출력 
    //?
    //Kotlin!

    필드가 존재하지 않기 때문에 게터는 꼭 정의해야 합니다.

     

     

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

    자바 컬렉션 API가 확장될 수 있는 이유 중 하나는 바로 위에서 다루었던 확장 함수 때문에 가능해집니다.

     

    가변 인자 함수

    자바의 가변 길이 인자처럼 메서드를 호출할 때 원하는 개수만큼 값을 넘기면 됩니다.

    자바와 차이점은 타입 뒤에 ...을 붙이는 대신 코틀린에서는 파라미터 앞에 vararg 변경자를 붙이면 됩니다.

     

    이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 배열 앞에 *를 붙여줘야 하는 차이점이 있습니다.

     

    중위 호출

    중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣어줍니다.

    1.to("one") //to 메서드를 일반적인 방식으로 호출
    1 to "one" //to 메서드를 중위 호출

     

    문자열과 정규식 다루기

    코틀린의 문자열을 자바와 동일하며 단순히 다양한 확장 함수를 제공할 뿐입니다.

     

    자바 개발자라면 String의 split 메서드를 잘 알고 있습니다.

     

    자바의 split 메서드를 사용하면 "."을 구분자로 이용해서 문자열을 분리할 수 없습니다.

     

    왜냐면 .은 정규식에서 모든 문자를 나타내는 것입니다.

     

    따라서 코틀린은 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받아서 이를 해결합니다.

     

    정규식을 명시적으로 만들어 줍니다.

    println("12.345-6.A".split("\\.|-".toRegex()))

     

    또한 간단한 경우에 굳이 정규식을 쓸 필요가 없습니다

    println("12.345-6.A".split(".","-"))

    확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있습니다.

     

    또한 삼중따옴표(""")를 활용한 정규식 사용과 확장 함수를 사용하여 문자열 파싱을 수행할 수 있습니다.

     

    로컬 함수와 확장

    Don't Repeat Yourself 원칙은 개발자들이 중요하게 생각합니다.

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

     

    필드를 검증하는 부분이 중복된 코드

    class User(val id: Int, val name: String, val address: String)
    fun saveUser(user : User){
        if(user.name.isEmpty()){
            throw IllegalArgumentException("")
        }
        if(user.address.isEmpty()){
            throw IllegalArgumentException("")
        }
        //user를 DB에 저장
    }

     

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

    class User(val id: Int, val name: String, val address: String)
    fun saveUser(user : User){
        fun validate(user: User, value : String, filedName: String){
            if(value.isEmpty()){
                throw IllegalArgumentException("empty ${filedName}")    
            }
        }
        validate(user, user.name, "Name")
        validate(user, user.address, "Address")
        //user를 DB에 저장
    }

    하지만 User 객체를 로컬 함수에게 하나하나 전달하는 것이 아쉽습니다.

     

    로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있는 특성이 있습니다.

     

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

    class User(val id: Int, val name: String, val address: String)
    fun saveUser(user : User){
        fun validate(value: String, filedName: String){
            if(value.isEmpty()){
                throw IllegalArgumentException("${user.id} empty ${filedName}")
            }
        }
        validate(user.name, "Name")
        validate(user.address, "Address")
        //user를 DB에 저장
    }

    여기서 조금 더 개선하고 싶다면 검증 로직을 User 클래스를 확장한 함수로 만들 수 있습니다.

     

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

    class User(val id: Int, val name: String, val address: String)
    
    fun User.validateBeforeSave() {
        fun validate(value: String, filedName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("${id} empty ${filedName}")
            }
    	}
    
    
    	validate(name, "Name")
    	validate(address, "Address")
    }
    
    //user를 DB에 저장
    fun saveUser(user: User) {
    	user.validateBeforeSave()
    }

    User는 라이브러리에 있는 클래스가 아니라 사용자가 만든 클래스입니다.

    이 경우 검증 로직은 User를 사용하는 다른 곳에서는 쓰이지 않습니다.

    따라서 User를 간결하게 유지하면 더 쉽게 코드를 파악할 수 있습니다.

     

    하지만 중첩이 깊어지면 가독성이 안 좋아지므로 일반적으로 한 단계만 함수를 중첩시키기를 권장합니다.

    728x90

    'Kotlin > 코틀린인액션요약' 카테고리의 다른 글

    6장 - 코틀린 타입 시스템  (0) 2022.09.16
    5장 - 람다로 프로그래밍  (0) 2022.09.13
    4장 - 클래스, 객체, 인터페이스  (1) 2022.09.12
    2장 - 코틀린 기초  (1) 2022.09.10
    1장 - 코틀린소개  (0) 2022.09.08

    댓글

Designed by Tistory.