오늘도 더 나은 코드를 작성하였습니까?

함수 정의와 호출 본문

Kotlin in Action/코틀린 기초

함수 정의와 호출

hik14 2020. 8. 8. 18:33

코틀린에서 컬렉션 만들기

- 코틀린은 자신만의 컬렉션 기능을 제공하지 않고 자바 컬렉션을 사용한다 그러므로 자바 코드와 상호작용하기 편하다.

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

 

fun main() {
    
    val set = hashSetOf(1, 7, 53)
    val list = arrayListOf(1, 7, 53)
    val map = hashMapOf(1 to "ond", 7 to "seven", 53 to "fifty-three")
    val strings = listOf("first", "second", "fourteenth")

    println(list.max())
    print(strings.last())
}

* hashMapof(  key to value )  여기서 to는 키워드가 아니라 함수다.

 

함수 호출하기 쉽게 만들기 

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

    val list = arrayListOf(1, 2, 3, 4, 5)
    
    print(joinToString(list, ",","@","&"))
    print(joinToString(collection = list, prefix = "@", separator = ",", postfix = "$"))
    print(joinToString(list, prefix = "@", separator = ",", postfix = "$"))

}

 

이름을 붙인 인자

 

- 인자에 이름을 붙여 호출하면 가독성이 좋아지고 어떤 역할을 하는지 구분하기 쉬워진다.

- 코틀린은 함수 호출 시  함수에 전달하는 인자 중 일부 또는 전부에 이름을 명시할 수 있다.

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

- 이름을 명시할 시 인자의 순서가 바뀌어도 상관이 없다. 

 

디폴트 파라미터 값

 

자바에서는 일부 클래스에서 오버 로딩한 메서드가 너무 많아지는 문제가 있고 인자 중 일부가 생략된 오버로드 함수를 호출할 때 어떤 함수가 불릴지 모호한 경우가 생긴다. 

 

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

 

- 이름을 붙인 인자와 함께 사용 시 인자의 목록의 중간에 있는 인자 목록을 생략하고 지정하고 싶은 인자의 이름을 붙여 순서와 상관없이 지정할 수 있다.

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() {
    
    val list = arrayListOf(1, 2, 3, 4, 5)

    print(joinToString(list))
    print(joinToString(list, separator = "/"))
    print(joinToString(list, prefix = "@", postfix = "%"))
}

 

*JvmOverloads

 

- 자바에는 디폴트 파라미터 값이 없는데 코틀린 함수를 자바에서 호출할 시 코틀린 함수가 지정한 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시하고 호출해야 된다. 자바에서 코틀린 함수 호출을 좀 더 편하게 하려면 @JvmOverloads을 함수에 추가하면 코틀린 컴파일러가 자동으로 마지막 파라미터로부터 파라미터를 하나씩 생략한 자바 메서드를 오버 로딩한다. 

 

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

 

- 객체지향 언어인 자바는 모든 코드를 클래스의 메서드로 작성해야 된다. 하지만 실전에서는 한 클래스에 포함시키기 어려운 코드가 많이 생기기 마련이다. 이런 경우 정적인 메서드만 모아둔 클래스가 생기기도 한다.

 

- 코틀린에서는 이러한 클래스가  필요 없다. 함수를 직접 소스 팔의 최상위 수준 모든 다른 클래스 밖에 위치시킨다.

 

코틀린 컴파일러는 생성하는 클래스 이름은 최상위 함수가 들어있던 코틀린 소스파일의 이름과 대응하며 모든 최상위 함수는 이클래스의 정적 메서드가 된다.

public class JoinKt {
    public static String joinToString(...) { ... }
}

*코틀린 최상위 함수가 포함되는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName을 파일의 맨 앞 패키지 이름 선언 위에 위치시킨다.

 

함수와 마찬가지로 최상위 프로퍼티 역시 선언할 수 있다.

var opCount = 0

fun performOperation(){
    opCount++
}

fun reportOperationCount(){
    println("Operation performed $opCount times")
}

이러한 프로퍼티의 값은 정적 필드에 저장된다. 

기본적으로 최상위 프로퍼티도 접근자 메서드를 통해 자바 코드에 노출된다. (val은 get() var은 get, set)

 

상수를 정의하고 싶으면 const 변경자를 추가하여 프로퍼티를 public static final 필드로 컴파일하게 할 수 있다.

const val UNIX_LINE_SEPARATOR = "\n"
public static final String UNIX_LINE_SEPARATOR ="\n"

 

확장 함수와 확장 프로퍼티

확장 함수

 

- 확장 함수는 어떤 클래스의 멤버 메서드인 것처럼 호출하지만 그 클래스 밖에 선언된 함수이다.

 

- 확장 함수를 추가하려면 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다.

클래스의 이름을 수신 객체 타입이라 하고 확장 함수가 호출되는 대상이 되는 객체를 수신 객체라고 부른다.

 

fun String.lastChar():Char = this.get(this.length-1)


fun main() {
    
    val str = "kotlin"
    println(str.lastChar())
}

여기서 수신 객체 타입은 String이며  수신 객체는 this(kotlin)이다.

 

수신 객체를 생략하고 사용되며 이러면 함수가 더 간결해진다. 

fun String.lastChar():Char = get(length-1)

 

- JVM언어로 작성된 클래스, 자바 클래스로 컴파일한 클래스 파일이 존재한다면 그 클래스에 원하는 대로 확장 함수를 추가시킬 수 있다.

 

- 확장 함수 내부에서는 일반적인 인스턴스 메서드의 내부에서 처럼 수신 객체의 메서드 프로퍼티를 바로 사용할 수 있다.

하지만 확장 함수가 캡슐화를 깨지는 않기 때문에 private 및 protected 멤버는 사용할 수가 없다

 

- 특정 클래스에서 확장 함수를 다른 확장 함수에서도 사용할 수 있다.

 

 

확장 함수 import

import strings.lastChar // 함수만 임포트

import strings.* // strings 전체 임포트

import strings.lastChar as last // 함수만 임포트 이후 별명지정. 동일한 확장함수의 이름 충돌 방지.

 

자바에서 확장 함수 사용하기

- 최상위 함수와 동일하게 선언된 XXX.kt 파일의 이름으로 클래스가 만들어지고 정적 메서드로 생성된다.

 

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

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

    val result = StringBuilder(prefix)

    for ((index, element) in withIndex()){
        if(index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}


fun main() {
    val intList = listOf(1, 2, 3, 4)
    val strList = arrayListOf("one", "two", "three")
    println(intList.joinToString(separator = ";", prefix = "(", postfix = ")"))
    println(strList.joinToString(","))
}

 

구체적인 타입을 수신 객체 타입으로 지정도 가능하다

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

    val result = StringBuilder(prefix)

    for ((index, element) in withIndex()){
        if(index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

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

 

- 확장 함수는 정적 메서드 같은 특징을 지니며 실제로 자바에서 사용 시 컴파일러가 정적 메서드로 생성한다.

- 확장 함수는 클래스 밖에 선언된다.

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() {
    val view: View = Button()
    view.click()
    view.showOff()
}

View 클래스의 멤버 함수인 click은 Button클래스에서 오버라이드 되었다. 

상위 타입인 View 타입으로 Button 클래스의 인스턴스를 생성할 경우 오버라이드 된 메서드가 호출되지만

확장 함수는 View 클래스의 확장 함수가 호출된다.

 

* 실행 시점에서 객체 타입에 따라 동적으로 호출될 대상 메서드를 결정하는 방식을 동적 디스패치라고 한다.

반면 컴파일 시점에 알려진 변수 타입에 따라 정해진 메서드를 호출하는 방식은 정적 디스패치라 부른다.

일반적으로 정적은 컴파일 시점을 의미하고 동적은 실행 시점을 이야기한다. 

 

* 어떤 클래스의 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 동일하다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수가 더 우선순위가 높다.)

 

확장 프로퍼티

 

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

 

- 하지만 상태를 저장할 적절한 방법이 없다.(기존 클래스가 인스턴스 객체 필드를 추가할 수는 없다.) 그래서 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.

 

- 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편하다.

 

val String.lastChar: Char
    get() = get(length-1)


fun main() {
    val str = "kotlin"
    println(str.lastChar)
}

- 확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적인 프로퍼티와 동일하고 단지 수신 객체 클래스 가 추가됐을 뿐이다. 

 

- backing filed가 없어서 기본 게터 구현을 제공할 수가 없어서 최소한 게터는 꼭 정의해야 된다.

 

- 초기화 코드에서 계산한 값을 담을 필드가 없으므로 초기화 코드 역시 쓸 수 없다. 

 

컬렉션 처리 

 

자바 컬렉션 API 확장

 

- 코틀린 컬렉션은 자바의 컬렉션에 + 확장 함수 사용하여 더 많은 기능을 추가한 것.

 

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

 

가변 길이 인자는 메서드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 컴파일러가 배열에 그 값을 넣어서 전달하는 기능이다.

 

- 자바는 타입 뒤에... 을 붙이는 대신 코틀린은 파라미터 앞에 vararg 변경자를 붙인다. 

val list = listOf(1, 2, 3, 4, 5)

public fun <T> listOf(vararg elements: T): List<T> 
= if (elements.size > 0) elements.asList() else emptyList()

 중위 호출

   public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
   
   val map = mapOf( 1.to("one"), 2 to "two", 3 to "three")

to는 메서드이다. 

일반적인 메서드 호출은  1.to("one")이지만 1 to "one" 형태로 메서드를 호출할 수 있다.

1 to "one"(객체 공백 메서드 공백 유일한 인자)

 

인자가 하나뿐인 메서드는 중위 호출을 할 수 있는데  메서드 앞에 infix 키워드를 선언해준다.

 

구조 분해 선언

 

to 함수는 Pair의 인스턴스를 반환하는데 두 원소로 이루어진 순서쌍을 표현한다

pair 객체를 이용하여 number name 2개의 변수를 즉시 초기화한다.

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

이러한 기능을 구조 분해 선언이라 한다.

 

루프에서 구조 분해 선언 사용하기

    val list = listOf("hello", "kotlin", "java")

    for ((index, element) in list.withIndex()){
        println("${index}번 원소${element}")
    }

 

 

문자열과 정규식 다루기

 

fun main() {
    val str = "12.345-6.A"

    println(str.split("\\.|-".toRegex()))
    println(str.split(".", "-"))
}

 

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

 

String 확장 함수를 이용하여 파일 경로 파싱 하기.

fun parsePath(path: String){

    val directory = path.substringBeforeLast("/")

    val fullName = path.substringAfterLast("/")

    val fileName = fullName.substringBefore(".")

    val extension = fullName.substringAfter(".")

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

fun main() {
    val path = "/Users/hik/Desktop/professional-android/myapp.app"
    parsePath(path)
}

정규식을 이용해서 파일 경로 파싱 하기.

 

"""  """를 사용하면 어떤 문자도 이스케이프 할 필요가 없다.

삼중 따옴표는  줄 바꿈 표현 들여 쓰기를 포함한 모든 문자가 들어간다.

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

fun main() {
    val path = "/Users/hik/Desktop/professional-android/myapp.app"
    parsePath(path)
}

 

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

좋은 코드의 특징은 중복이 없는 것이다.

 

- 메서드의 추출을 통해 긴 메서드를 작은 메서드로 쪼개서 재활용할 수 있지만 안에 작은 메서드들이 늘어나고 관계 파악이 힘들어진다

 

- 내부 클래스를 만들어서 안에 넣으면 코드가 깔끔하게 할 수는 있지만 그에 따른 불필요한 코드가 늘어난다. 

 

코틀린에는 더 깔끔한 해결방안이 있다.

함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수가 있다.(로컬 함수의 개념)

 

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에 저장하는 코드가 있다고 가정한다.
    }
}

검증 로직의 중복은 사라졌고 다른 필드 추가 시 새롭게 검증할 때 역시 편하다.

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

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

        // user db에 저장하는 코드가 있다고 가정한다.
    }
}

로컬 함수는 자신이 속한 함수의 바깥 함수의 모든 파라미터 및 변수에 대한 접근이 가능하다.

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 db에 저장하는 코드가 있다고 가정한다.
        println("db에 저장 완료")
    }
}

마지막으로 확장 함수로 변경한다.

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, "Address")

        // user db에 저장하는 코드가 있다고 가정한다.
        println("db에 저장 완료")
    }

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

코드를 확장함수로 뽑아내는 기법은 매우 유용하다.

 

User 검증 로직은 user를 사용하는 다른 곳에는 쓰이지 않기에 user에 포함하고 있지 않다.

 

user를 간결하게 유지하면 생각해야 할 내용이 줄어서 코드를 쉽게 파악 가능하다.

 

한 객체만을 다루면서 객체의 비공개 데이터를 다룰 필요는 없는 함수는 확장 함수로 만들면 수신 객체를 지정하지 않고도 공개된 멤버 프로퍼티 메서드에 접근 가능하다.

 

일반적으로 함수의 중첩은 1단계가 권장된다.