관리 메뉴

너와 나의 스토리

[Kotlin] 함수 정의와 호출1 - 컬렉션/확장 함수/로컬 함수/문자열을 정규식으로 나누기 본문

Programming Language/Kotlin

[Kotlin] 함수 정의와 호출1 - 컬렉션/확장 함수/로컬 함수/문자열을 정규식으로 나누기

노는게제일좋아! 2021. 6. 12. 12:14
반응형

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")
  • 코틀린은 자체 컬렉션을 제공하지 않다. -> 기존 자바 컬렉션을 활용할 수 있음
    • 코틀린 컬렉션은 자바 컬렉션과 똑같은 클래스지만 자바보다 더 많은 기능을 쓸 수 있다.
    • 예: 리스트의 마지막 원소 가져오기. 수로 이뤄진 컬렉션에서 최댓값 찾기
val numbers = setOf(1, 14, 2)
println(numbers.max()) // output: 14

val set = hashSetOf(1, 7, 53)
println(set.max()) // output 53

val list = arrayListOf(1, 7, 53)
println(list.last()) // output: 53

 

2. 함수

  • 예로 들 함수를 하나 구현해보려고 한다. 
    • 자바 컬렉션에는 디폴트 toString 구현이 들어있다. 이를 통해 컬렉션을 출력하면 다음과 같은 형태로 나온다
    • [1, 7, 53]
val list = listOf(1, 7, 53)
println(list) // output: [1, 7, 53]
  • 우리는 출력하는 형태를 지정하는 함수를 구현해 보려고 한다.
    • 이 함수는 generic하다. 즉, 어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있다.
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()
}
  • 이 함수를 사용해서 collection을 출력해보자
fun main(args: Array<String>) {
    val list = listOf(1, 7, 53)
    println(joinToString(list, "; ", "(", ")")) 
    // output: (1; 7; 53)
}

 

 

이름 붙인 인자

  • joinToString() 메서드를 호출해보자. 
    • joinToString(collection, " ", " ", ".")
    • 이런식으로 작성을 하면 인자로 전달한 각 문자열이 어떤 역할을 하는지 구분하기 힘들다.
  • 코틀린에서는 다음과 같이 이름을 명시할 수 있다.
    • joinToString(collection, separator = " ", prefix = " ", postfix = ".")

 

디폴트 파라미터 값

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

 

함수를 클래스 안에 작성할 필요 없다

  • 자바에서는 모든 코드를 클래스의 메서드로 작성해야 하지만 코틀린에서는 그럴 필요 없다.
  • 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다.
  • 그런 함수를 다른 패키지에서 임포트해서 사용하면 된다.

 

 

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

  • 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 사용할 수 있다. 
    • 이는 확장 함수(extension function)이 가능하기 때문이다.
  • 확장 함수: 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수이다. 
  • 예: String 클래스에 문자열의 마지막 문자를 돌려주는 메서드를 추가
    • this 생략 가능
package strings

fun String.lastChar(): Char = this.get(this.length - 1)
  • 확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.
    • 클래스 이름을 수신 객체 타입(receiver type)이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)라고 부른다.
    • 위 예에서 String이 수신 객체 타입, this가 수신 객체가 된다.
  • 위처럼 확장 함수를 정의해두면, 다음과 같이 일반 클래스 멤버를 호출하는 것처럼 해당 함수를 사용할 수 있다.
    • But, 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야 한다.
println("Kotlin".lastChar())

 

  • as 키워드를 사용하면 import한 클래스나 함수를 다른 이름으로 부를 수 있다.
import strings.lastChar as last

val c = "Kotlin".last()
  • 아까 만든 joinToString() 함수를 확장 함수로 정의해보자
fun <T> Collection<T>.joinToString( // Collection 클래스에 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 main(args: Array<String>) {
    val list = listOf(1, 7, 53)
    println(list.joinToString("; ", "(", ")"))
    // output: (1; 7; 53)
}
  • 이 함수를 '문자열의 컬렉션'에 대해서만 호출할 수 있도록 설정할 수도 있다.
fun Collection<String>.join(
    separator: String = ",",
    prefix: String = "",
    postfix: String = ""
) = joinToString(separator, prefix, postfix)


fun main(args: Array<String>) {
    val list = listOf(1, 7, 53)
//    println(list.join("; ", "(", ")")) -> error

    val list2 = listOf("abc", "def", "ghi")
    println(list2.join("; ", "(", ")"))
    // output: (abc; def; ghi)
}

 

3. 문자열을 정규식으로 나누기

  • 자바에서 문자열 나누기(String.split())
    • "12.345-6.A".split(".")를 시도하면 빈 배열이 반환된다. 
    • 이유는 split의 구분 문자열은 실제로는 정규식(regular expression)이기 때문이다.
      • 정규식을 파라미터로 받는 함수는 String이 아닌 Regax 타입의 값을 받는다.
      • 이때, 마침표(.)는 모든 문자를 나타내는 정규식으로 해석된다.
  • 코틀린에서는 split 확장 함수를 제공함으로써 구분 문자열로 String 값을 쓸 수도 regax 타입의 값을 쓸수도 있다.
  • 마침표(.)와 대시(-)로 문자열을 분리하는 예
    • 정규식 문법은 자바와 동일
println("12.345-6.A".split("\\.|-".toRegex())) // 정규식을 명시적으로 만듦
// output: [12, 345, 6, A]
println("12.345-6.A".split(".","-")) // 구분 문자열을 일반 string으로 정의
// output: [12, 345, 6, A]

 

4. 로컬 함수

  • 좋은 코드의 중요한 특징 중 하나는 중복이 없는 것이다. 
    • 많은 경우 메서드 추출(Extract Method) 리팩토링을 적용해서 긴 메서드를 부분 부분 나눠서 각 부분을 재활용할 수 있다. 
    • 하지만 그렇게 코드를 리팩토링하면 클래스 안에 작은 메서드가 많아지고 각 메서드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려워질 수도 있다.
    • 리팩토링을 진행해서 추출한 메서드를 별도의 내부 클래스 안에 넣으면 코드를 깔끔하게 조직할 수는 있지만, 그에 따른 불필요한 준비 코드가 늘어난다.
  • 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다. 
    • 이렇게 하면 문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
  • 아래의 예제를 참고하여 코드 중복을 로컬 함수를 통해 제거해보자.
  • 예: 
    • 다음 리스트에는 사용자를 데이터베이스에 저장하는 함수가 있다.
    • 이때 데이터베이스에 사용자 객체를 저장하기 전에 각 필드를 검증해야 한다.
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"
        )
    }
}

fun main(args: Array<String>) {
    saveUser(User(1, "",""))
    // output: Can't save user 1: empty Name
}
  • User 클래스의 각 필드마다 비슷한 코드가 반복되게 된다. 
  • 이 부분을 로컬 함수로 분리하면 중복을 없애는 동시에 코드 구조를 깔끔하게 유지할 수  있다.
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, "Adress")
}
  • 이 코드를 더 개선하고 싶다면 검증 로직을 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(name, "Name")
    validate(address, "Adress")
}

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

 

 

 

 

 

출처:

- [Kotlin IN ACTION]

 

반응형
Comments