Kotlin in Action

[Kotlin in Action] part3_Defining and calling functions

지 슈 2022. 11. 4. 15:24

코틀린에서 컬렉션 만들기

코틀린은 자체 컬렉션이 없고 표준 자바 컬렉션을 사용한다. 표준 자바 컬렉션을 사용하는 것이 자바 코드와 상호작용하기가 더 수월하기 때문이다. 코틀린 컬렉션이 자바 컬렉션과 완전히 동일하지만 코틀린에서는 더 많이 활용할 수 있다.

예를 들어, 리스트의 마지막 요소를 찾거나(last) 숫자들 사이에서 최댓값(max)을 찾을 수 있다. (자바에서는 못하나봄?)

>>> val strings = listOf("first", "second", "fourteenth")
>>> println(strings.last())
fourteenth
>>> val numbers = setOf(1, 14, 2)
>>> println(numbers.max())
14

 

함수를 쉽게 호출

내용을 출력 : 아주 단순해 보이지만 중요한 개념을 포함한다.

코틀린의 자바 컬렉션은 toString 구현이 디폴트로 되어있다.

>>> val list = listOf(1, 2, 3)
>>> println(list)
[1, 2, 3]

toString 출력 형식 대신 다른 형식이 필요한 경우,

예를 들어 (1;2;3) 를 출력하는 함수를 구현하고 싶은 경우, 이렇게 구현할 수 있다. 

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

>>> val list = listOf(1, 2, 3)
>>> println(joinToString(list, "; ", "(", ")"))
(1; 2; 3)

 

이 함수의 선언부를 덜 번잡하게 만들 수 있을까? 

 

1. 호출 부분의 가독성 해결

코틀린에서는, 이렇게 할 수 있다!

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

코틀린으로 작성한 함수를 호출할 때는달하는 인자의 이름으로 명시할 수 있다.

어느 하나라도 인자의 이름을 명시하면 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.

WARNING 자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없다. 

 

 

2. 디폴트 파라미터 값

자바에서 오버로딩한 메서드가 많아지면 여러 가지 이유로 불편하다. 

하지만 코틀린에서는 함수 선언할 때 파라미터의 디폴트 값을 지정할 수 있으므로 이런 불편함을 피할 수 있다.

위의 함수를 디폴트 값을 사용해서 개선해보면, 이렇게 구현할 수 있다.

콤마(,)로 원소를 구분하기 위해 디폴트 값 설정한다!

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

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

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

함수의 디폴트 파라미터 값은 함수 선언에서 지정되기 때문에 어떤 함수의 정의된 부분에서 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 다시 컴파일하면 값을 지정하지 않은 모든 인자는 바뀐 디폴트 값이 적용된다는 것을 기억해야 한다.

 

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

자바에서는 특별한 상태나 인스턴스 메서드는 없고 정적 메서드를 모아두기만 하는 클래스가 생길 수 있다.

하지만 코틀린에서는 이런 무의미한 클래스 대신 함수를 소스파일의 최상위 수준, 모든 클래스의 밖에 위치시키면 된다. 

다른 패키지에서 그 함수를 사용하고 싶은 경우 패키지를 임포트하면 된다.

package strings
fun joinToString(...): String { ... }
//joinToString 함수를 string 패키지에 넣기
//join.kt 파일

코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 포함되는 코틀린 소스파일의 이름과 같다.

만약 파일에 대응하는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 애노테이션 추가하라.

@file:JvmName("StringFunctions")
package strings
fun joinToString(...): String { ... }

 

최상위 프로퍼티

함수 뿐만 아니라 프로퍼티도 파일의 최상위에 놓일 수 있다.

어떤 연산의 수행 횟수를 저장하는 var 프로퍼티를 만들 수 있다.

var opCount = 0  //최상위 프로퍼티 선언
fun performOperation() {
opCount++
// ...
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}

최상위 프로퍼티를 사용해서 코드에 상수를 추가할 때는,,

val UNIX_LINE_SEPARATOR = "\n"  //게터를 사용해야 해서 부자연스럽다면,
const val UNIX_LINE_SEPARATOR = "\n"  //const를 변경자를 추가하면 public static final 필드로 컴파일하게 할 수 있다.

 

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

확장함수: 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수

 

확장함수 만들기     수신 객체 타입 + 추가하려는 함수 이름

수신 객체 타입: 함수가 확장할 클래스

수신 객체: 확장 함수가 호출되는 대상이 되는 값

fun String.lastChar(): Char = this.get(this.length - 1)
//수신 객체 타입              //수신 객체

일반 메서드와 마찬가지로 this를 생략할 수 있다.

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

(이 책에선 앞으로 클래스 안의 메서드와 확장 함수를 모드 메서드라고 부른다고 한다)

 

임포트와 확장 함수

확장 함수를 사용하려면 그 함수를 임포트해야 한다. 

코틀린에서는 클래스를 임포트할 때와 동일한 구문으로 개별 함수를 임포트할 수 있다.

import strings.lastChar  //import strings.* 도 사용 가능
val c = "Kotlin".lastChar()

//as 키워드로 임포트한 클래스나 함수를 다른 이름으로 부를 수 있음.
import strings.lastChar as last
val c = "Kotlin".last()

 

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

아까 구현했던 joinToString 함수의 최종 개선 버전을 확장으로 정의하면,

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

>>> val list = listOf(1, 2, 3)
>>> println(list.joinToString(separator = "; ",
... prefix = "(", postfix = ")"))
(1; 2; 3)

확장함수를 사용하면 joinToString()을 마치 클래스의 멤버인 것처럼 호출할 수 있다.

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

 

확장 함수는 오버라이드 불가능

확장 함수는 클래스의 밖에서 선언되기 때문에 오버라이드 불가능하다. 

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의 확장 함수가 호출된다.(?)

 

확장 프로퍼티

확장 프로퍼티는 실제로 아무 상태도 가질 수 없지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있다.

확장 프로퍼티는 일반 프로퍼티에 수신 객체 클래스를 추가하면 된다.

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

확장 프로퍼티를 사용하는 방법은 멤버 프로퍼티를 사용하는 방법과 같다.

>>> println("Kotlin".lastChar)
n
>>> val sb = StringBuilder("Kotlin?")
>>> sb.lastChar = '!'
>>> println(sb)
Kotlin!

 

 

컬렉션을 처리할 때 유용한 라이브러리 함수 몇 가지를 살펴보자.

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

코틀린 언어 특성:

  • vararg 키워드로 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
  • 중위 함수 호출 구문을 사용하면 인자가 하나인 메서드를 호출할  수 있다.
  • 구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 담을 수 있다.

 

자바 컬렉션 API 확장

코틀린 표준 라이브러리 기능을 다 알 필요는 없다. IDE 통해 알 수 있기 때문.

 

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

리스트를 생성하는 함수를 호출할 때 원하는 개수의 인자를 전달할 수 있다.

코틀린의 가변 길이 인자는 자바와 문법이 살짝 다르다. 

>>코틀린: 파라미터 앞에 vararg 변경자를 붙임

 

이미 배열에 있는 원소를 가변 길이 인자로 넘길 때도 자바와 다른 구문을 사용한다.

>>코틀린: 배열을 명시적으로 풀어서 배열의 각 원소를 인자로 전달. (스프레드 연산자 사용 배열 앞에 * 붙임.)

fun main(args: Array<String>) {
val list = listOf("args: ", *args)
println(list)
} //스프레드 연산자가 배열 원소를 언팩한다.

 

값의 쌍 다루기: 중위 호출구조 분해 선언

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

infix fun Any.to(other: Any) = Pair(this, other)  //to 함수 정의를 간략화

중위 호출로 일반 메서드를 호출.

수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣는다. (각 요소 사이는 공백)

일반 메서드를 중위 호출이 가능하게 하려면 맨 앞에 infix 변경자를 추가한다. 

to 함수는 Pair 를 리턴한다. 

이 기능을 구조 분해 선언이라고 부른다. 

구조 분해란?

객체가 가지고 있는 여러 값을 분해해서 여러 변수에 한꺼번에 초기화

 

여러 변수들을 괄호로 묶었다는 점이 일반 변수와 차이점

 

 

 

 

 

 

 

 

문자열과 정규식 다루기

문자열 나누기_split 확장 함수

자바의 split 메서드는 빈 배열을 반환하기 때문에 "12.345-6.A".split(".") [12, 345-6, A] 로 나타나지 않는다.

자바의 이런 불편함을 막기 위해서 코틀린은 split 확장 함수를 제공한다.

이 함수는 String 이 아니라 Regex 타입의 값을 받기 때문에 어떤 것으로 문자열을 분리하는지 쉽게 알 수 있다.

>>> println("12.345-6.A".split("\\.|-".toRegex()))
[12, 345, 6, A]
//마침표나 대시(-)로 문자열 분리

 

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

코틀린에서는 정규식을 사용 안하고도 코틀린 라이브러리를 사용해서 문자열을 쉽게 pathing 할 수 있다. 

(?)

 

 

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

자바를 사용하면 중복을 피하려고 리팩토링을 하다가 오히려 코드가 더 복잡해질 위험이 있다.

코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시키는 방법으로 해결 할 수 있다.

 

코드 중복을 로컬 함수 사용해서 제거

//코드 중복 예제
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")
}
// Save user to the database
}
>>> saveUser(User(1, "", ""))
java.lang.IllegalArgumentException: Can't save user 1: empty Name

위 예제에서 로컬 함수를 사용해서 중복된 코드를 제거하자

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")
// Save user to the database
}