코틀린2 - 함수 정의와 호출,
3장. 함수 정의와 호출
컬렉션 만들기
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")
- to는 특별한 키워드가 아니라 일반함수다. (중위 호출 infix)
println(set.javaClass) // javaClass는 자바 getClass()에 해당하는 코틀린 코드다.
// 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
- (자바에 없는) last와 max함수는 어디에 정의되어 있는걸까?
함수를 호출하기 쉽게 만들기
val list = listOf(1, 2, 3)
println(list)
// [1, 2, 3] 으로 출력된다. (자바 toString 구현에 의해)
// 만약 이 출력양식을
// (1; 2; 3) 처럼 바꾸고 싶다면?
- 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(seperator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")"))
// (1; 2; 3)
- 잘동작한다.
- 하지만 어떻게 하면 이 함수를 호출하는 문장을 덜 번잡하게 만들 수 있을까?
- 호출할 때마다 매번 네 인자를 모두 전달하는 것은 불필요하다.
이름 붙인 인자
- 함수 호출 부분의 가독성 개선
joinToString(collection, seperator = " ", prefix = " ", postfix = ".")
- 함수에 전달하는 인자 중 일부의 이름을 명시할 수 있다. 호;출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.
디폴트 파라미터 값
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ", // 디폴트 파라미터
prefix: String = "",
postfix: String = ""
): String{...}
- 이제 함수를 호출할 때 모든 인자를 쓸 수도 있고, 일부를 생략할 수도 있다.
joinToString(list)
// 1, 2, 3
joinToString(list, "; ") // 뒷부분 인자 생략
// 1; 2; 3
joinToString(list, postfix = "; ", prefix = "# ") // 중간 인자 생략
// # 1, 2, 3;
최상위 함수와 프로퍼티
함수를 클래스 안에 선언하지 않아도 된다.
- 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다.
- 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지는 않은 경우
- 다양한 정적 메소드를 모아두는 역할만 담당, 특별한 상태나 인스턴스 메소드 없는 클래스가 생겨난다. -> JDK의 Collections 클래스
- 코틀린에서는 이런 무의미한 클래스가 필요 없다.
- 대신 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다.
joinToString Strings 함수를 최상위 함수로 선언하기 -> strings 패키지에 직접 넣기
package strings
fun joinToString(...): String {...}
- 어떻게 실행? -> JVM이 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일러는 이 파일을 컴파일할 때 새로운 클래스를 정의해준다.
//자바
package strings;
public class JoinKt {
public static String joinToString(...) {...}
}
- 코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 들어있떤 코틀린 소스 파일의 이름과 대응한다.
- 코틀린 파일의 모든 최상위 함수는 이 클래스의 정적인 메소드가 된다.
- 따라서 자바에서 joinToString을 호출하기는 쉽다.
//자바
import strings.JoinKt;
...
JoinKt.joinToString(list, ", ", "", "");
최상위 프로퍼티
- 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 흔하지는 않지만, 그래도 가끔 유용할 때가 있다.
var opCount = 0
fun performOperation(){
opCount++
}
fun reportOperationCount(){
//최상위 프로퍼티의 값을 읽는다.
println("Operation performed $opCount times")
}
- 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메소드를 통해 자바 코드에 노출된다. 겉으론 상수처럼 보이는데, 실제로는 게터를 사용해야 한다면 자연스럽지 못하다. 더 자연스럽게 사용하려면 이 상수를 public static final 필드로 컴파일해야 한다. const 변경자를 추가하면 프로퍼티를 public static final 필드로 컴파일하게 만들 수 있다.(원시 타입과 String만 가능)
const val UNIX_LINE_SEPARATOR = "\n"
//자바
public static final String UNIX_LINE_SEPARATOR = "\n";
joinToString을 좀 더 개선해보자
메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
- 확장함수 - 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여려 편리한 기능을 사용하는 것
- 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수
문자열의 마지막 문자를 돌려주는 메소드를 만들어보자
package Strings
fun String.lastChar():Char = this.get(this.length - 1)
/*
String -> 수신객체 타입
this -> 수신객체
*/
- 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙인다.
- 클래스 이름을 수신 객체 타입이라 부르며, 확장 함수가 호출되는 대상이 되는 값을 수신 객체라고 부른다.
println("Kotlin".lastChar())
// "Kotlin" -> 수신객체
-
String 클래스에 새로운 메소드를 추가하는 것과 같다.
-
String 클래스의 소스코드를 소유한 것이 아니고, 직접 작성한 코드도 아니지만 여전히 원하는 메소드를 String 클래스에 추가할 수 있다.
-
확장 함수 본문에서도 일반 메소드와 마찬가지로 this를 생략할 수 있다.
package Stgrings
fun String.lastChar:Char = get(length - 1)
//수신 객체 멤버에 this 없이 접근 할 수 있다.
- 확장 함수가 캡슐화를 깨지는 않는다.
- 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다.
임포트와 확장함수
- 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다.
import strings.*
val c = "Kotlin".lastChar()
자바에서 확장 함수 호출
- 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드다.
- 그래서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다.
- 이런 설계로 인해 자바에서 확장 함수를 사용하기도 편한다.
- 정적 메소드를 호출하면서 첫 번째 인자로 수신 객체를 넘기기만 하면 된다.
확장 함수로 유틸리티 함수 정의 (joinToString 최종 버전)
- 이 함수는 코틀린 라이브러리가 제공하는 함수와 거의 같아졌다.
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
) : String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()){ // this -> 수신 객체
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
- 이제 joinToString을 마치 클래스의 멤버인 것처럼 호출 할 수 있따.
val list = arrayListOf(1, 2, 3)
println(list.joinToString(" "))
// 1 2 3
-
확장 함수는 정적 메소드 호출에 대한 문법적인 편의(syntatic sugar)일 뿐이다.
-
확장 함수가 정적 메소드와 같은 특징을 가지므로, 확장 함수를 하위 클래스에서 오버라이드할 수는 없다.
확장 함수는 오버라이드할 수 없다.
멤버 함수 오버라이드하기
open class View {
open fun click() = println("View clicked")
}
class Button : View() {
override fun click() = println("Button clicked")
}
val view:View = Button()
view.click()
// 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의 확장 함수가 호출된다.
- 확장 함수는 첫 번째 인자가 수신 객체인 정적 자바 메소드로 컴파일한다.!!
// 자바
View view = new Button();
ExtensionKt.showOff(view);
// showOff 함수를 extensions.kt 파일에 정의했다
// I'm a view!
확장함수는 왜 오버라이드 할 수 없을까
내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드이다. 예를들어 다음과 같은 Util 클래스가 있다고 가정해보자
//자바
class Util{
static int plus(int a, int b){
return a + b;
}
static int plus(int a, float b){
return a + b;
}
}
위의 두 메소드에서 첫번째 인자의 타입이 같으므로 다음과 같이 사용할 수도 있다.
class Int{
int plus(int b){
return this + b;
}
int plus(float b){
return this + b;
}
}
만약 컴파일러에서 plus 메소드를 첫번째 인자가 수신 객체인 static 메소드로 바꿔준다면? -> 그것이 확장함수가 하는 역할이다. static 메소드이니 오버라이드가 불가능한 것은 당연하다.
확장 프로퍼티
- 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
- 기존 클래스의 인스턴스 객체에 필드를 추가할 방법은 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.
- 하지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어서 편한 경우가 있다.
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)/ // 프로퍼티 세터
}
컬렉션 처리
가변 인자 함수 vararg
val list = listOf(1, 2, 3, 4, 5)
라이브러리에서 listOf 함수를 열어보면
fun listOf<T>(vararg values:T):List<T>{...}
가변 길이 인자 - 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때
- 자바에서는 배열을 그냥 넘김
- 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. (스프레드 연산자 *)
fun main(args: Array<String>){
val list = listOf("args: ", *args) // 스프레드 연산자가 배열의 내용을 펼쳐준다.
println(list)
}
중위(infix) 함수
val map mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
- 여기서 to라는 단어는 코틀린 키워드가 아니다.
- 중위 호출이라는 특별한 방식으로 to라는 일반 메소드를 호출한 것이다.
- 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다.(사이에 공백 필요)
1.to("one") // 일반적인 방식으로 호출
1 to "one" // 중위 호출 방식으로 호출
- 인자가 하나뿐인 일반 메소드 or 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
- 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.
infix fun Any.to(other: Any) = Pair(this, other)
- to 함수는 Pair의 인스턴스를 반환한다. Pair는 코틀린 표준 라이브러리 클래스로, 두 원소로 이뤄진 순서쌍을 표현한다.
val (number, name) = 1 to "one"
Pair의 내용으로 두 변수를 즉시 초기화할 수 있는데, 이런 기능을 구조 분해 선언이라고 부른다.
구조 분해 선언(destructuring declaration)
Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있다. (7.4장)
- 루프 withIndex
for ((index, element) in collection.withIndex()){
println("$index: $element")
}
mapOf
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
listOf와 마찬가지로 mapOf에도 원하는 개수만큼 인자를 전달할 수 있따. 하지만 mapOf의 겨우에는 각 인자가 키와 값으로 이뤄진 순서쌍이어야 한다.
코틀린이 맵에 대해 제공하는 특별한 문법인 것처럼 느껴지지만 실제로는 일반적인 함수를 더 간결한 구문으로 호출하는 것뿐이다.
문자열과 정규식 다루기
문자열 나누기
자바 split 메소드로는 점(.)을 사용해 문자열을 분리할 수 없다.
//자바
System.out.println("12.345.A".split("."));
// [] 빈배열
// ["12", "345", "A"]가 아니다
split의 구분 문자열은 정규식이기 때문. 따라서 마침표는 모든 문자를 나타내는 정규식으로 해석된다.
코틀린에서는 split 확장 함수를 제공한다. 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받는다. 따라서 코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.
println("12.345-6.A".split("\\.|-".toRegex())) // 정규식을 명시적으로 만든다.
// ["12", "345", "6", "A"]
split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
println("12.345-6.A".split(".", "-"))
// ["12", "345", "6", "A"]
정규식 + 3중 따옴표 문자열
파일의 전체 경로명 -> 디렉터리 / 파일이름 / 확장자로 구분하기
"/Users/yole/kotlin-book/chapter.adoc"
fun parsePath(path: String){
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
parsePath("/Users/yole/kotlin-book/chapter.adoc")
// Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하지만 나중에 알아보기 힘든 경우가 많다.
코드다듬기: 로컬 함수와 확장
요약
- 코틀린은 자체 컬렉션 클래스를 정의하지 않지만 자바 클래스를 확장해서 더 풍부한 API를 제공한다.
- 함수 파라미터의 디폴트 값을 정의하면 오버로딩한 함수를 정의할 필요성이 줄어든다.
- 이름붙인 인자를 사용하면 함수의 인자가 많을 때 함수 호출의 가독성을 더 향상시킬 수 있다.
- 코틀린 파일에서 클래스 멤버가 아닌 최상위 함수와 프로퍼티를 직접 선언할 수 있다. 이를 활용하면 코드 구조를 더 유연하게 만들 수 있다.
- 확장 함수와 프로퍼티를 사용하면 외부 라이브러리에 정의된 클래스를 포함해 모든 클래스의 API를 그 클래스의 소스코드를 바꿀 필요 없이 확장할 수 있다.
- 확장 함수를 사용해도 실행 시점에 부가 비용이 들지 않는다.
- 중위 호출을 통해 인자가 하나 밖에 없는 메소드나 확장 함수를 더 깔끔한 구문으로 호출할 수 있다.
- 코틀린은 정규식과 일반 문자열을 처리할 때 유용한 다양한 문자열 처리 함수를 제공한다.
- 자바 문자열로 표현하려면 수많은 이스케이프가 필요한 문자열의 경우 3중 따옴표 문자열을 사용하면 더 깔끔하게 표현할 수 있다.
- 로컬 함수를 쓰면 코드를 더 깔끔하게 유지하면서 중복을 제거할 수 있다.
참고자료
- Kotlin in Action
- http://try.kotl.in/
- https://kotlinlang.org/docs/tutorials/