Jisoo’s devlog
[Kotlin in Action] part3_Defining and calling functions 본문
코틀린에서 컬렉션 만들기
코틀린은 자체 컬렉션이 없고 표준 자바 컬렉션을 사용한다. 표준 자바 컬렉션을 사용하는 것이 자바 코드와 상호작용하기가 더 수월하기 때문이다. 코틀린 컬렉션이 자바 컬렉션과 완전히 동일하지만 코틀린에서는 더 많이 활용할 수 있다.
예를 들어, 리스트의 마지막 요소를 찾거나(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
}
'Kotlin in Action' 카테고리의 다른 글
[Kotlin in Action] part6_코틀린 타입 시스템 (1) | 2022.11.24 |
---|---|
[Kotlin in Action] part5_람다로 프로그래밍 (0) | 2022.11.18 |
[Kotlin in Action] part4_클래스, 객체, 인터페이스 (0) | 2022.11.11 |
[Kotlin in Action] part2_Kotlin basics (1) | 2022.10.07 |
[Kotlin in Action] part1_Kotlin: what and why (0) | 2022.10.05 |