Language/Kotlin

[KotlinInAction] 3장-함수 정의와 호출(2)컬렉션처리, 문자열다루기, 코드 다듬기(로컬함수/확장)

개랭갱깽스타 2021. 11. 3. 06:29

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

 

자바 컬렉션 API 확장

가변 인자 함수

값의 쌍 다루기

 

5. 문자열과 정규식 다루기

자바 문자열 = 코틀린 문자열
(변환 X, Wrapper X)

 

문자열 나누기(split)

정규식을 파라미터로 받는 함수는 Regex 타입의 값을 받는다.

    @Test
    fun ch3_5_2_split_print_use_regex() {
        print( "12.345-6.A".split("\\.|-".toRegex()))
    }

Strings.kt

더보기
/**
 * Splits this char sequence around matches of the given regular expression.
 *
 * @param limit Non-negative value specifying the maximum number of substrings to return.
 * Zero by default means no limit is set.
 */
@kotlin.internal.InlineOnly
public inline fun CharSequence.split(regex: Regex, limit: Int = 0): List<String> = regex.split(this, limit)
#실행결과

[12, 345, 6, A]

 

구분 문자열을 하나 이상 인자로 받는 함수가 있다.

    @Test
    fun ch3_5_2_split_print_not_ues_regex() {
        print( "12.345-6.A".split(".", "-"))
    }

Strings.kt

더보기
/**
 * Splits this char sequence to a list of strings around occurrences of the specified [delimiters].
 *
 * @param delimiters One or more strings to be used as delimiters.
 * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`.
 * @param limit The maximum number of substrings to return. Zero by default means no limit is set.
 *
 * To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from
 * the beginning to the end of this string, and matches at each position the first element in [delimiters]
 * that is equal to a delimiter in this instance at that position.
 */
public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List<String> {
    if (delimiters.size == 1) {
        val delimiter = delimiters[0]
        if (!delimiter.isEmpty()) {
            return split(delimiter, ignoreCase, limit)
        }
    }

    return rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) }
}
#실행결과

[12, 345, 6, A]

 

  

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

"User/yole/kotlin/book/chapter.adoc"

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

정규식 사용 - 3중 따옴표 문자열

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

(.+) : 정규표현식

더보기

(.+) = 앞 문자가 하나 이상인 임의의 한 문자

()
소괄호 안의 문자를 하나의 문자로 인식

.
임의의 한 문자 (문자의 종류 가리지 않음)
단, \ 는 넣을 수 없음

+
앞 문자가 하나 이상

https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Regular_Expressions

https://highcode.tistory.com/6

둘 다 테스트 결과는 아래와 같이 나온다.

    @Test
    fun ch_3_5_3_parse_path() {
        parsePath("/Users/yole/kotlin-book/chapter.adoc")
    }
# 실행결과

dir: /Users/yole/kotlin-book, name: chapter, ext: adoc

 

여러 줄 3중 따옴표 문자열

모든 문자가 들어간다.(들여쓰기, 줄 바꿈, ...)

    @Test
    fun ch_5_3_3_multi_regex() {
        // .를 구분 문자열로 사용
        val kotlinLogo = """| //
                           .| //
                            .| / \""".trimMargin(".")    // . 와 직전의 공백 제거(확장함수)


        println(kotlinLogo)
    }

 

#실행결과

| //
| //
| / \

 

\ 문자열을 이스케이프 할 필요가 없다.

    @Test
    fun ch_3_5_3_can_include_special_symbol() {
        /*
            책에서는 """C:\Users\yole\kotlin-book""" 로 되어있는데.. 오타가 난 걸로 보인다..? 아니라면 답글 부탁드립니다!
         */
        val windowPath = """C:\\Users\\yole\\kotlin-book"""

        println(windowPath)
    }

 

#실행결과

C:\\Users\\yole\\kotlin-book

 

문자열 템플릿의 시작을 표현하는 $ 를 3중 따옴표 문자열 안에 넣을 수 없다.

    @Test
    fun ch_3_5_4_can_include_template_literals_inner_string() {
        val price = """${'$'}99.9"""
//        val price = """$99.9"""
//        val dollar = '$'
//        val price = "${dollar}99.9"

        println(price)
    }

 

#실행결과

$99.9

 

언제 쓸까?

TEST

HTML, 텍스트 등을 사용할 때 이스케이프 문자로 변환하지 않고 사용할 수 있다!

 

(InteliJ) 자동완성

더보기

첫 줄의 // 뒤에 엔터를 치니,

trimMargin() 함수가 자동 생성 되었다 !

    fun ch_5_3_multi_regex() {
        val kotlinLogo = """| //
            |
        """.trimMargin()    }

 

 

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

 

중복이 없는 것

Don't Repeat Yourself (DRY-반복하지 말라)

자바의 경우, 메소드 추출 리팩토링(Extract Method) 적용

메소드 추출 리팩토링이란?

더보기

긴 메소드를 부분부분 나눠서 각 부분을 재활용 한다.

 

(마틴파울러-리팩토링: 코드 품질을 개선하는 객체지향 사고법) 책에 잘 나와 있는 것 같습니다!

https://armadillo-dev.github.io/book/refactoring-06-composing-methods/

 

[-] 코드 이해하기가 더 힘들어 질 수 있다! > inner class 안에 넣어 코드를 깔끔하게 할 수 있지만.. 불필요한 (준비) 코드가 늘어난다ㅠㅠ

- 클래스 안에 작은 메소드가 많아진다. - 각 메소드 사이의 관계를 파악하기 힘들다.

 

코틀린의 경우, 로컬함수 사용!

로컬함수

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

 

책에 나온 예제를 통해 로컬함수로 코드를 리팩토링 해 나가는 과정을 보았습니다!

원래 소스) 코드 중복

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

    //user 를 db 에 저장한다.
}

 

리팩토링)

1. 로컬함수로 분리

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} : $fieldName")
        }
    }
    
    //FIXME. user 객체를 로컬함수에 계속 전달해야 함! 불편!
    validate(user, user.name, "Name")
    validate(user, user.address, "Address")

    //user 를 db 에 저장한다.
}

 

2. 로컬함수에서 자신이 속한 바깥 함수의 모든 파라미터/변수 사용

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

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

    //user 를 db 에 저장한다.
}

 

3. 확장 함수로 추출

객체.멤버 처럼 수신 객체를 지정하지 않고, 공개된 멤버/프로퍼티/메소드에 접근할 수 있다.

[+] User 를 간결하게 유지할 수 있다 > 코드를 파악하기 EASY

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 : $fieldName")
        }
    }

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

fun saveUser(user: User) {
    user.validateBeforeSave()

    //user 를 db 에 저장한다.
}

 

 

(4. 확장함수를 로컬 함수로 정의 할 수도 있다. But.. 중첩된 함수의 깊이가 깊으면 코드 읽기가 어렵다!)

일반적으로는 1 단계만 함수를 중첩시키라고 권장한다.

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

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

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

    user.validateBeforeSave()

    //user 를 db 에 저장한다.
}

 

 

 

반응형