본문 바로가기

Dev Book/Kotlin IN ACTION

CH3(3.1~3.3). 함수 정의와 호출(1)

3.1 코틀린에서 컬렉션 만들기

- 코틀린은 자신만의 컬렉션 기능을 제공하지 않는다. 자바 컬렉션과 똑같은 클래스를 사용한다.(상호호환성!) 

 


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

- 만약 자바 컬렉션이 제공하는 디폴트 출력 형식과 다르게 원소를 출력하고 싶다면 어떻게 해야 할까? joinToString()함수를 구현하여 이 문제를 해결할 수 있다.

 

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

 

해당 함수의 다음의 문제점이 존재한다.

- 함수의 가독성: 함수를 호출 할 때마다 매번 네 인자(collectoin, separator, prefix, postfix)를 모두 전달하지 않을 수는 없을까?

이름을 붙인 인자를 사용하자!

- 코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부(또는 전부)의 이름을 명시할 수 있다. 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시하도록 한다.

 

- 디폴트 파라미터 값

코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있다. 이를 통해 오버로딩 메소드가 가져오는 중복 문제를 피할 수 있다.

 

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

 

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

자바에서는 다양한 정적 메소드를 모아두는 역할만 담당하며 특별한 상태나 인스턴스 메소드가 없는 클래스가 존재한다.(Collections 클래스의 경우) 코틀린에서는 이런 무의미한 클래스가 필요없다. 대신 함수를 소스파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다. 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 최상위 프로퍼티를 이용해 코드에 상수를 추가할 수 있는데, 이 경우 다른 모든 프로퍼티처럼 접근자 메소드를 통해 자바 코드에 노출된다. 상수처럼 사용하기 위해서는(getter를 쓰지 않게 하기 위해서) const 변경자를 추가해 public static final 필드로 컴파일하게 만들면 된다.

 


3.3 확장 함수와 확장 프로퍼티

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

 

fun String.lastChar() : Char = this.get(this.lenth-1)
// this 생략 가능

 

>>println("Kotlin".lastChar())
n

 

- 위 함수에서 String이 수신 객체 타입, "Kotlin"가 수신 객체가 된다.

- 확장 함수 내부에서는 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 수신 객체의 메소드나 프로퍼티를 바로 사용할 수 있다. 하지만 확장 함수가 캡슐화를 깨지는 않기 때문에 클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 private 멤버나 protected 멤버를 사용할 수 없다.

 

- 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야 한다. 이 때, 한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 as 키워드를 사용해 이름을 바꿔서 임포트하면 충돌을 막을 수 있다.

 

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

 

fun <T> Collection<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()
}

 

>>> val list = arrayListOf(1,2,3)
>>> println(list.joinToString(""))
1 2 3

 

- 컬렉션에 대한 확장을 만듦으로써 joinToString을 마치 클래스의 멤버인 것처럼 호출할 수 있다!

 

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

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

 

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

 

- 확장 프로퍼티

확장 프로퍼티의 경우 기본 게터 구현을 제공할 수 없어 최소한 게터는 꼭 정의해야 한다. 

var StringBuilder.lastChar: Char
	get() = get(length-1) //프로퍼티 게터
    set(value:Char) {
    	this.setCharAt(length-1, value) //프로퍼티 세터
    }