BottleH Blog

Kotlin in Action - 3장 함수 정의와 호출

    Tags

  • Kotlin
Kotlin in Action - 3장 함수 정의와 호출 thumbnail

📖 3.1 코틀린에서 컬렉션 만들기

fun main() { val set = setOf(1, 7, 53) val list = listOf(1, 7, 53) val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three") println(set.javaClass) println(list.javaClass) println(map.javaClass) }
  • 코틀린은 표준 자바 컬렉션 클래스를 사용한다.
    • 코틀린 컬렉션 인터페이스는 디폴트로 읽기 전용이다.
  • 코틀린 컬렉션은 자바 컬렉션보다 더 많은 기능을 쓸 수 있다!
    • 원소 뒤섞기 등

📖 3.2 함수를 호출하기 쉽게 만들기

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() }
  • 덜 번잡하게 만들 수는 없을까?

🔖 3.2.1 이름 붙인 인자

자바의 일부 코딩스타일에서는 method 호출 시, 파라미터 이름을 주석에 넣으라고 요구하기도 한다.

  • 코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부(또는 전부)의 이름을 명시할 수 있다.
    • 모든 인자의 이름을 지정한다면 인자 순서도 변경 가능하다!

🔖 3.2.2 디폴트 파라미터 값

자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아질 수 있다.

fun <T> joinToString( collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = "" ): String
  • 코틀린에서는 함수 선언에서 파라미터의 기본값을 지정할 수 있어, 오버로딩을 상당히 피할 수 있다.
  • 함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언쪽에 인코딩된다.

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

  • 코틀린에서는 함수를 모든 다른 클래스의 밖에 위치시키면 된다.
    • 함수들은 여전히 그 파일의 맨 앞에 정의된 패키지의 멤버 함수이므로 다른 패키지에서 그 함수를 사용하고 싶을 때는 그 함수가 정의된 패키지를 임포트해야만 한다.
    • 클래스이름을 내포하지 않아도 된다.
  • 코틀린 컴파일러가 생성하는 클래스의 이름이 최상위 함수가 들어있던 코틀린 소스파일의 이름과 대응한다.
    • join.kt -> JoinKt

최상위 프로퍼티

  • 함수와 마찬가지로 프로퍼티도 파일 최상위 수준에 놓을 수 있다.
  • 상수와 변수 모두 설정가능하다.
var opCount = 0 val UNIX_LINE_SEPARATOR = "\n" const val UNIX_LINE_SEPARATOR = "\n" // java code에 public static final 필드로 노출하고 싶다면 const 변경자 추가

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

확장 함수

fun String.lastChar(): Char = this.get(this.length - 1)
  • 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수
  • 수신 객체 타입: 확장이 정의될 클래스의 타입
  • 수신 객체: 호출하는 대상 값(객체)

🔖 3.3.1 임포트와 확장 함수

  • 확장 함수를 쓰려면 다른 클래스나 함수와 마찬가지로 해당 함수를 임포트해야만 한다.
    • 이름 충돌을 막기 위함
import strings.lastChar as last val c = "Kotlin".last()
  • as 키워드를 사용하면 다른 이름으로 부를 수 있다.
  • 확장 함수는 코틀린 문법상 반드시 짧은 이름을 써야 한다.

🔖 3.3.2 자바에서 확장 함수 호출

  • 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다.
char c = StringUtilKt.lastChar("Java");
  • 확장 함수가 StringUtil.kt 파일에 정의했다고 가정하면 자바에서 위와 같이 호출 가능하다.

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

fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수 선언 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() }
  • joinToString을 클래스의 멤버인 것처럼 호출할 수 있다.
  • 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다.

🔖 3.3.4 확장 함수는 오버라이드할 수 없다

fun View.showOff() = println("I'm a view") fun Button.showOff() = println("I'm a button") fun main() { val view: View = Button() view.showOff() // 확장함수는 정적으로 결정된다. // I'm a view }
  • 확장 함수는 클래스의 일부가 아니다.
  • 즉, 코틀린은 호출될 확장 함수를 정적으로 결정하기 때문에 오버라이딩이 적용되지 않는다.

🔖 3.3.5 확장 프로퍼티

val String.lastChar: Char get() = get(length - 1)
  • 확장 프로퍼티 또한 단지 수신 객체 클래스가 추가됐을 뿐이다.
  • 뒷받침하는 필드가 없어 기본 게터 구현을 제공할 수 없으므로 꼭 게터는 정의해야 한다.
  • 확장 프로퍼티를 사용하는 방법은 멤버 프로퍼티를 사용하는 방법과 같다.

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

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

🔖 3.4.1 자바 컬렉션 API 확장

  • 코틀린 표준 라이브러리는 수많은 확장 함수를 포함하고 있다.
    • last sum 등
  • 코틀린 파일에서 디폴트로 임포트 된다.

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

val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()
  • 원하는 개수만큼 여러 값을 인자로 넘기면 배열에 그 값들을 넣어주는 언어 기능인 varargs를 사용
fun main(args: Array<String>) { val list = listOf("args: ", *args) }
  • 코틀린에서는 배열을 명시적으로 풀어 전달해줘야 한다.
    • 스프레드 연산: 배열 앞에 * 붙이기

🔖 3.4.3 쌍(튜플) 다루기: 중위 호출과 구조 분해 선언

val map = mapOf(1 to "one", 2 to "two", 3 to "three")
  • to라는 단어는 코틀린 키워드가 아니다. 중위 호출(infix call) 방식으로 to 라는 일반 메서드를 호출한 것이다.
  • 중위 호출 시에는 수신 객체 뒤에 메서드 이름을 위치시키고 그 뒤에 유일한 메서드 인자를 넣는다.
infix fun Any.to(other: Any): Pair(this, other)
  • 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.
val (number, name) = 1 to "one"
  • 위와 같은 기능을 구조 분해 선언(destructuring declaration)이라고 부른다.

📖 3.5 문자열과 정규식 다루기

🔖 3.5.1 문자열 나누기

fun main() { println("12.345-6.A".split("\\.|-".toRegex())) }
  • 코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 분리하는지 쉽게 알 수 있다.
fun main() { println("12.345-6.A".split('.', '-')) }
  • 여러 문자를 받을 수 있는 확장 함수도 있다.

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

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") }
  • 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다.
fun parsePathRegax(path: String) { val regex = """(.+)/(.+)\.(.+)""".toRegex() val matchResult = regex.matchEntire(path) if (matchResult != null) { val (directory, filename, extension) = matchResult.destructured println("Dir: $directory, name: $fileName, ext: $extension") } }
  • 정규식을 활용하여 파싱할 수 있다.
  • 3중 따옴표 문자열에서는 \를 포함한 어떤 문자도 이스케이프할 필요가 없다.
  • destructured 프로퍼티: 그룹별로 분해한 매치 결과를 의미

🔖 3.5.3 여러 줄 3중 따옴표 문자열

  • 3중 따옴표를 쓰면 줄 바꿈이 들어있는 텍스트를 쉽게 포함시킬 수 있다.
  • 들여쓰기도 모두 포함된다.
  • 문자열 템플릿을 사용할 수도 있다.
  • trimIndent 확장 함수를 호출하면 문자열의 모든 줄에서 가장 짧은 공통 들여쓰기를 찾아 각 줄의 첫 부분에서 제거하고, 공백만으로 이뤄진 첫 번째 줄과 마지막 줄을 제거해준다.
  • 여러 줄 문자열은 테스트에도 유용하다.

📖 3.6 코드 깔끔하게 다듬기: 로컬 함수와 확장

DRY: Don't Repeat Yourself(반복하지 말라)

  • 코틀린에서는 깔끔하게 코드를 조직할 수 있는 방법이 있다.
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") } }
  • 코드 중복을 보여주는 예제이다.
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") }
  • 조금 나아졌다.
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.id}: " + "empty $fieldName") } } validate(user.name, "Name") validate(user.address, "Address") }
  • 바깥 함수의 파라미터에 직접 접근할 수 있다.
class User(val id: Int, val name: String, val address: String) fun User.validateBeforeSave() { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException("Can't save user ${id}: empty $fieldName") } } validate(name, "Name") validate(address, "Address") }
  • 확장함수로 추출하기
  • 확장함수를 로컬 함수로 정의할 수도 있다. 다만, 내포된 함수의 깊이가 깊어지면 읽기가 어려워 일반적으로 한 단계만 함수를 내포시키라고 권장한다.
Written by@BottleH
Back-End Developer

GitHub