본문 바로가기
kotlin/Kotlin In Action

2. 함수 정의와 호출

by 유저혀 2021. 6. 11.
반응형

Kotlin IN ACTION 책을 공부하면서 정리한 내용입니다.

 

http://www.acornpub.co.kr/book/kotlin-in-action

 

Kotlin in Action

이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다.

www.acornpub.co.kr

 

 

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

// javaClass는 자바 getClass()에 해당하는 코틀린 코드다.
>> println(set.javaClass)  // class java.util.HashSet
>> println(list.javaClass) // class java.util.ArrayList
>> println(map.javaClass)  // class java.util.HashMap

 

하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있다.

예를 들어 리스트의 마지막 원소를 가져오거나 수로 이루어진 컬렉션에서 최댓값을 찾을 수 있다.

val strings = listOf("first", "second", "fourteenth")
println(strings.last())  // fourteenth

val numbers = setOf(1, 14, 2)
println(numbers.max())   // 14

 


2.  함수를 호출하기 쉽게 만들기

fun<T> joinToString(
  collection: Collection<T>,
  seperator: String,
  prefix: String,
  postfix: String
): String {
  val result = StringBuilder(prefix)
  for((index, element) in collection.withIndex()){
    if(index > 0) result.append(seperator)
    result.append(element)
  }
  result.append(postfix)
  return result.toString()
}

 

2.1. 디폴트 파라미터 값

코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있다.

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


joinToString(list, ", ", "", "")
joinToString(list, "; ")  // seperator를  ";"로 지정. prefix와 postfix 생략
joinToString(list, postfix="; ", prefix="# ")  // #1, 2, 3;

이제 함수를 호출할 때 모든 인자를 쓸 수도 있고, 일부를 생략할 수도 있다.

 

 


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

자바는 모든 코드를 클래스의 메소드로 작성해야 한다. 그 결과 다양한 정적 메소드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메소드가 없는 클래스도 생겨난다. JDK의 collections 클래스나 Util이 이름에 들어있는 클래스가 그 예이다.

코틀린에서는 이런 무의미한 클래스가 필요없다. 대신 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다. 그런 함수들은 여전히 그 파일의 맨 앞에 정의된 패키지의 멤버함수이므로 다른 패키지에서 그 함수를 사용하고 싶을 때는 그 함수가 정의된 패키지를 import 해야한다.

 

// join.kt
package strings
fun joinToString(...): String {...}

 

JVM은 클래스 안에 들어있는 코드만을 컴파일 할 수 있으므로 위 파일을 컴파일하면 새로운 클래스를 정의해준다.

join.kt 파일을 컴파일한 결과와 같은 자바 클래스 파일은 아래와 같다.

/* 자바 */
package strings;

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

 

최상위 프로퍼티

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 이런 프로퍼티의 값은 정적 필드에 저장된다.

최상위 프로퍼티를 활용해 코드에 상수를 추가할 수 있다.

val UNIX_LINE_SEPERATOR ="\n"

 

기본적으로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메소드를 통해 자바 코드에 노출된다. (val는 게터, var는 게터와 세터가 생김) 겉으로는 상수처럼 보이는데 실제로 게터를 사용해야 한다면 자연스럽지 못하므로 더 자연스럽게 사용하려면 public static final 필드로 컴파일 해야 한다. 이를 위해 const 변경자를 추가한다.

 

const val UNIX_LINE_SEPERATOR = "\n"

// 자바
public static final UNIX_LINE_SEPERATOR = "\n";

 

 

3.1. 메소드를 다른 클래스에 추가: 확장 함수, 확장 프로퍼티

확장 함수(extension function)는 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다. 

package strings

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

 

위 함수는 아래와 같이 호출한다.

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

 

확장함수는 캡슐화를 깨지 않는다. 그러므로 클래스 내부에서만 사용할 수 있는 private이나 protected 멤버를 사용할 수는 없다.

 

 

3.2. 자바에서 확장 함수 호출

확장함수를 StringUtil.kt 파일에 정의했다면 자바에서는 다음과 같이 호출한다.

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

 

 

3.3. 확장함수로 유틸리티 함수 정의

fun<T> Collection<T>.joinToString(  // Collectio<T>에 대한 확장함수
  seperator: String = ", ",  // 파라미터 디폴트 값 설정
  prefix: String = "",
  postfix: String = ""
): String {
  val result = StringBuilder(prefix)
  for((index, element) in this.withIndex()){
    if(index > 0) reulst.append(seperator)
    result.append(element)
  }
  result.append(postfix)
  return result.toString()
}

 

이제 joinToString을 마치 클래스의 멤버인 것처럼 호출할 수 있다.

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

 

만약 문자열 타입의 컬렉션에 대해서만 호출하고 싶다면 다음과 같이 작성하면 된다.

fun Collection<String>.join(
  seperator: String = ", ",
  prefix: String = "",
  postfix: String = ""
) = joinToString(seperator, prefix, postfix)

 

 

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

// 멤버 함수 오버라이드 하기
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!")

val view: View = Button()
view.showOff()  // I'm a View!

view가 가리키는 실제 타입은 Button이지만 이 경우 view의 타입이 View이므로 무조건 View의 확장 함수가 호출된다.

 

 

3.5. 확장 프로퍼티

앞의 lastChar() 함수를 프로퍼티로 정의해보자.

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

 

StringBuilder와 같은 프로퍼티를 정의한다면 StringBuilder의 맨 마지막 문자는 변경가능하므로 var로 만든다.

var StringBuilder.lastChar: Char
  get() = get(length-1)
  set(value:Char){
    this.setCharAt(length-1, value)
  }
 
println("Kotlin".lastChar)  // n
val sb = StringBuilder("Kotlin?")
sb.lastChar = "!"
println(sb)   // Kotlin!

 

 


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

컬렉션을 처리할 때 유용하게 사용할 수 있는 코틀린 표준 라이브러리 함수 몇가지를 소개한다.

  • vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다
  • 중의(infix) 함수 구문을 사용하면 인자가 하나뿐인 메소드를 간편하게 호출할 수 있다
  • 구조 분해 선언(destucturing declaration)을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다

 

4.1. 자바 컬렉션 API 확장

val strings: List<String> = listOf("first", "second", "fourteenth")
println(strings.last()) //fourteenth

val numbers: Collection<Int> = setOf(1, 14, 2)
println(numbers.max())  // 14

 

이 코드가 가능했던 이유는 last max는 모두 확장함수이기 때문이다.

fun<T> List<T>.last(): T { /* 마지막 원소 반환 */ }
fun Collection<Int>.max() : Int { /* 컬렉션의 최대값을 찾음 */ }

 

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

리스트를 생성하는 함수를 호출할 때 원하는 만큼 원소를 전달할 수 있다

var list = listOf(2, 3, 5, 7, 11)

 

라이브러리에서 이 함수의 정의는 다음과 같다.

fun listOf<T> (vararg values: T): List<T> { ... }

 

이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바는 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되도록 해야 한다. 전달하려는 배열 앞에 *를 붙이기만 하면 된다.

fun main(args: Array<String>) {
  val list = listOf("args: ", *args)
  println(list)
}

 

 

4.3. 값의 쌍 다루기: 중위(infix) 호출과 구조 분해 선언

Infix 함수는 두개의 변수 가운데 오는 함수를 말한다. 코틀린에서 기본적으로 정의된 Infix 함수들 중에 Pair를 만드는 to가 있다.

val map1 = mapOf(Pair(1, "one"), Pair(2, "two"))
val map2 = mapOf(1 to "one", 2 to "two")

 

중위 호출은 인자가 하나뿐인 일반 메소드나 인자가 하나뿐인 확장 함수에서 중위 호출을 사용할 수 있다. 함수를 중위 호출에 사용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.

infix fun Any.to(other: Any) = Pair(this, other)

 

클래스 내 infix 함수는 다음과 같이 정의할 수 있다.

class MyString {
    var string = ""
    infix fun add(other: String) {
        this.string = this.string + other
    }
}

val myString = MyString()
myString add "Hello"
myString add "World"
myString add "Kotlin"
println(myString.string) // HelloWorldKotlin

 

위의 to 중의 호출 메소드는 Pair 쌍의 내용으로 두 변수를 즉시 초기화 할 수 있다. 이런 기능을 구조 분해 선언(destucturing declaration)이라고 부른다.

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

 

루프에서도 구조 분해 선언을 사용할 수 있다.  joinToString에서 봤듯이 withIndex를 구조 분해 선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.

for((index, element) in collection.withIndex()){
  println("$index: $element")
}

 

 

728x90

'kotlin > Kotlin In Action' 카테고리의 다른 글

4. 람다로 프로그래밍  (0) 2021.06.17
3. 클래스, 객체, 인터페이스  (0) 2021.06.14
1. 코틀린 기초  (0) 2021.06.10

댓글