Kotlin In Action #2
함수 정의와 호출
모든 프로그램에서 핵심이라 할 수 있는 함수 정의와 호출 기능을 코틀린이 어떻게 개선했는지 살펴보자.
추가로 확장 함수와 프로퍼티를 사용해 자바 라이브러리를 코틀린 스타일로 적용하는 방법을 살펴보자.
코틀린에서 컬렉션 만들기
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")
map에서 사용된 to는 언어가 제공하는 키워드가 아닌 일반 함수.
fun main(args: Array<String>) {
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")
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)
}
해당 코드를 실행해 보면 아래와 같은 결과를 얻을수 있음.
class java.util.HashSet
class java.util.ArrayList
class java.util.HashMap
코틀린은 자신만의 컬렉션 기능을 제공하지 않음.
코틀린이 자체 컬렉션을 제공하지 않는 이유는 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기 훨씬 쉽기 때문.
코틀린은 자바보다 더 많은 기능을 제공.
println(set.last())
println(list.maxOrNull())
함수 호출하기 쉽게 만들기
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()
}
해당 함수는 제네릭 함.
어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있음.
해당 함수를 계속해서 사용하기에는 매번 인자 4개를 모두 전달해야 해서 번잡함이 있다. 이를 해결해보자.
이름 붙인 인자
joinToString(list, separator = "; ", prefix = "(", postfix = ")")
IDE 도움을 받아 시그니처를 표시할 수 있겠지만, 코드 자체가 모호하기 때문에 인자의 이름을 명시 할 수 있음.
이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자의 이름을 꼭 명시해야 함.
디폴트 파라미터 값
자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있음.
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 사용해 오버로드 중 상당수를 피할 수 있음.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String
정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티
- 자바
- 모든 코드를 클래스의 메소드로 작성해야 함.
- 어느 한 클래스에 포함시키기 어려운 코드가 많음.
- 정적 메소드만 모아두는 역할을 담당하는 특별한 상태나 인스턴스가 없는 클래스가 생겨남.
- 코틀린
- 무의미한 클래스가 필요 없음.
- 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치 시키면 됨.
최상위 프로퍼티
함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있음.
var opCount: Int = 0
fun performOperation() {
opCount++
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}
최상위 프로퍼티 값은 정적 필드에 저장됨.
최상위 프로퍼티를 활용해 상수를 추가할 수 있음.
const val UNIX_LINE_SEPARATOR = "\n"
그냥 val만 사용하면 게터가 생김.
const를 붙이면 public static final 필드로 컴파일.
메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
자바 라이브러리 기반 혹은 자바 프로젝트와 통합하는 경우 코틀린으로 직접 변환할 수 없거나 미처 변환하지 않은 기존 자바 코드를 처리할 수 있어야 함.
기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 사용할 수 있게 해주는 확장 함수.
fun String.lastChar(): Char = this.get(this.length - 1)
확장 함수를 만들려면 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 됨.
여기서 String은 수신 객체 타입이고, this는 수신 객체.
수신 객체 멤버에 this없이 접근 가능.
fun String.lastChar(): Char = get(length - 1)
확장 함수가 캡슐화를 깨지 않음.
클래스 안에서 정의한 메서드와 달리 확장 함수 안에서는 클래스 내부에서만 사용 가능한 private 혹은 protected 멤버를 사용할 수 없음.
임포트와 확장 함수
확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야 사용 가능.
import strings.lastChar
fun main(args: Array<String>) {
println("Kotlin".lastChar())
}
import strings.*
fun main(args: Array<String>) {
println("Kotlin".lastChar())
}
as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수 있음.
여러 패키지의 동일한 함수를 가져와야 하면 충돌을 막기 위해 사용.
import strings.lastChar as last
fun main(args: Array<String>) {
println("Kotlin".last())
}
자바에서 확장 함수 호출
lastChar() 함수의 파일명을 LastKt로 했으면 아래와 같이 사용하면 됨.
char c = LastKt.lastChar("dddd");
joinToString() 함수 확장 함수로 정의
package strings
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()
}
확장 함수는 오버라이드할 수 없다
open class View {
open fun click() = println("View")
}
class Button: View() {
override fun click() = println("Button")
}
fun main(args: Array<String>) {
val v: View = Button()
v.click()
}
- Button이 View의 하위 타입이기 때문에 View 타입 변수를 선언해도 Button 타입 변수 대입 가능.
- Button 클래스가 click 함수를 오버라이드했다면 Button이 오버라이드한 click 함수가 호출 됨.
fun View.showOff() = println("View2")
fun Button.showOff() = println("Button2")
fun main(args: Array<String>) {
val v: View = Button()
v.showOff()
}
확장 함수는 정적으로 결정되기 때문에 View 클래스의 showOff 함수가 호출 됨.
확장 프로퍼티
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있음.
상태를 저장할 적절한 방법이 없기 때문에 확장 프로퍼티는 아무 상태도 가질 수 없음.
val String.lastChar: Char
get() = get(length - 1)
변경 가능한 프로퍼티
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
- vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있음.
- 중위 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출할 수 있음.
- 구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있음.
자바 컬렉션 API 확장
코틀린에서의 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공.
대표적인 예시로 last나 maxOrNull 함수가 있음.
fun main(args: Array<String>) {
val list = listOf<String>("test1", "test2", "test3")
println(list.last())
val numbers = setOf<Int>(9, 1, 10, 5, 6)
println(numbers.maxOrNull())
}
public fun <T> List<T>.last(): T {
if (isEmpty())
throw NoSuchElementException("List is empty.")
return this[lastIndex]
}
public fun <T : Comparable<T>> Iterable<T>.maxOrNull(): T? {
val iterator = iterator()
if (!iterator.hasNext()) return null
var max = iterator.next()
while (iterator.hasNext()) {
val e = iterator.next()
if (max < e) max = e
}
return max
}
last나 maxOrNull은 확장 함수라는 것.
가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
리스트를 생성하는 listOf 함수를 보자.
public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
- 가변 길이 인자는 메서드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 컴파일러가 배열에 그 값들을 넣어주는 기능.
- 자바의 ...과 비슷. 코틀린에서는 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")
1.to("one")
1 to "one"
- 1.to("one") <- "to" 메서드를 일반적인 방식으로 호출
- 1 to "one" <- "to" 메서드를 중위 호출 방식으로 호출
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
- 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가.
- to 함수는 Pair의 인스턴스를 반환.
- Pair는 코틀린 표준 라이브러리 클래스로, 두 원소로 이루어진 순서쌍을 표현.
val (number, name) = 1 to "one"
- Pair 클래스를 사용하여 두 변수를 즉시 초기화.
- 해당 기능을 구조 분해 선언이라 부름.
문자열과 정규식 다루기
코틀린 코드가 만들어낸 문자열을 아무 자바 메서드에 넘겨도 되고, 자바 코드에서 받은 문자열을 아무 코틀린 표준 라이브러리 함수에 전달해도 문제 없음.
특별한 변환과 자바 문자열을 감싸는 별도의 래퍼가 필요 없음.
문자열 나누기
println("324.23gerag23.23549.234gw-ew".split("\\.|-".toRegex())) // 정규식을 명시적으로 만듬.
println("324.23gerag23.23549.234gw-ew".split(".", "-")) // 여러 구분 문자열을 지정.
정규식과 3중 따옴표로 묶은 문자열
String 확장 함수를 사용해 경로 파싱
fun String.parsePath(): String {
val directory = this.substringBeforeLast("/")
val fullName = this.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
return "Dir: $directory, name: $fileName, ext: $extension"
}
정규식 사용해 경로 파싱
fun String.parsePath(): String {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(this)
if (matchResult != null) {
val (directory, fileName, extension) = matchResult.destructured
return "Dir: $directory, name: $fileName, ext: $extension"
}
return ""
}
3중 따옴표 문자열에서는 역슬래시(\)를 포함한 어떤 문자도 이스케이프할 필요가 없음.
여러 줄 3중 따옴표 문자열
fun main(args: Array<String>) {
val text = """|ddd
.|ddddfeafe
.|wefw
"""
println(text.trimMargin("."))
}
이스케이프를 피하기 위해서만 아닌 줄 바꿈을 쉽게 표현하기 위해서도 사용.
코드 다듬기: 로컬 함수와 확장
함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 잇음.
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.address} empty Address"
)
}
}
로컬 함수를 사용해 중복 코드 줄이기
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.name}: empty $fieldName"
)
}
}
validate(user.name, "Name")
validate(user.address, "Address")
}
로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있음.
'Programming > Kotlin' 카테고리의 다른 글
[당근마켓 밋업 정리] - Kotlin Coroutines 톺아보기 (0) | 2022.01.09 |
---|---|
Kotlin In Action #4 (0) | 2022.01.05 |
Kotlin In Action #3 (0) | 2021.12.22 |
Kotlin In Action #1 (0) | 2021.12.06 |
Kotlin In Action #0 (0) | 2021.12.05 |