Kotlin in Action 정리 (2)

Operator overloading and other conventions

  • 함수를 통해서 연산자 오버로딩
    • operator fun times
  • 확장으로도 가능
    • operator fun Char.times(count: Int)
  • Nullable 타입에 대해서도 적용 가능(equal 제외)

산술 연산자

  • binary 연산자
    • times, div, mod, plus, minus
    • 비트연산자는 기호 없음
  • coumpound assignment 연산자
    • timesAssign, divAssign, modAssign, plusAssign, minusAssign
  • unary 연산자
    • unaryPlus, unaryMinus, not, inc, dec
    • 전치, 후치 증감 연산(inc, dec) 정상적으로 진행됨
1
2
3
4
5
6
7
8
// binary
operator fun Point.plus(other: Point): Point

// compound
operator fun Point.plusAssign(other: Point)

// unary
operator fun Point.inc()

비교 연산자

  • 동치 비교 연산자
    • a == ba?.equals(b) ?: (b == null)로 연산함
    • null도 동치를 비교해야하기 때문
    • 그러므로 equal은 nullable type에 대해서 적용 불가
    • 레퍼런스를 비교하는 ===는 오버로딩 불가
    • 오버로딩 시 override를 써야함, Any 클래스에 이미 정의되어 있기 때문
  • 순서 비교 연산자
    • 비교가 가능하려면 Comparable을 상속받아야함
    • a >= b -> a.compareTo(b) >= 0
    • 위의 식은 Comparator의 정의를 기가막히게 보여주는 식이라고 생각함
    • `co
1
2
3
4
5
6
7
8
9
10
// equals
override fun equals(obj: Any?): Boolean

// compareTo
class Person(
  val firstName: String, val lastName: String
) : Comparable<Person> {
  override fun compareTo(other: Person): Int = 
    compareValuesBy(this, other, Person::lastName, Person::firstName)
}

기타 연산자 혹은 Convention 들

  • 접근 연산자: get, set: x[a, b] = c -> x.set(a, b, c)
  • 포함 연산자: contains: a in c -> c.contains(a)
  • 범위 연산자: rangeTo: start..end -> start.rangeTo(end)
  • 순회 연산자: Iterator
    • 인터페이스 Iterator 를 구현한 객체를 리턴해야 함
    • hasNext, next 두 함수를 구현해야함
  • 구조 분해 선언
    • component1, component2 등을 선언해주어야 함
    • array나 collection, data class에는 이미 선언되어 있음
1
2
3
4
5
6
// 접근 연산자
operator fun Point.get(index: Int): Int
operator fun MutablePoint.set(index: Int, value: Int)

// 포함 연산자
operator fun Rectangle.contains(p: Point): Boolean

delegated properties

  • by를 통해서 프로퍼티를 위임할 수 있음
  • 프로퍼티 산출이 복잡하거나, 가능하면 늦게 계산하고 싶다거나 할때 유용함
  • 위임을 담당하는 객체는 복잡한 상태를 소유할수도 있음
  • 프로퍼티 리스트를 map으로 보관하는 등의 응용도 가능
1
2
3
4
5
6
7
8
9
10
11
import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String { }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { }
}

class Foo {
  var p: Type by Delegate()
}

Higher-order functions

  • HOF: 람다를 파라미터로 받거나 리턴
  • Function Type: (T, U) -> R
    • 자체로 Null이 될수 있음 ((T, U) -> R)?
    • 인자로 받는 경우, 기본 인자를 람다로 줄 수 있다.

inline functions

  • inline 함수
    • 기존 다른 언어들에서와 의미가 같음
    • 람다를 인자로 받으면 그 람다까지도 inline
    • noinline을 붙이면 특정 람다는 인라인 시키지 않을 수 있음
    • inline된 람다에서는 외부 함수에 대한 리턴이 가능함
  • inline과 generic
    • generic과 함께 쓸 때 타입이 전달되지 않음 (함수이므로)
    • 단 파라미터화 타입에 reified를 붙일경우 inline시에 타입 전달됨
  • inline과 최적화
    • collection의 filter함수 등은 인라인 되어서 성능 이득을 봄
    • 코드 사이즈 증가에 대해 주의할 것
  • inline과 resource
    • python의 with, java의 try-with-resource처럼 활용 가능
    • 락을 잡고 풀어줘야할때 이를 인라인 안에 숨기고, 내부 로직만 집중 가능
1
2
3
4
val l: Lock = ...
l.withLock {
  // access the resource protected by this lock
}

HOF control flow

  • labeled 리턴
    • 람다에 대한 리턴인지, 외부 함수에 대한 리턴인지 라벨로 지정
    • 라벨 안붙이고, 함수 이름으로도 가능
  • 단 람다가 아니고 익명함수를 선언한 경우는 labeled 리턴 불가능함
1
2
3
4
5
6
7
8
9
// labeled return
people.forEach label@{
  if (it.name == "Alice") return@label
}

// labeled return with function name
people.forEach {
  if (it.name == "Alice") return@forEach
}

Generics

  • Java generic과 일맥상통
  • 함수, 클래스, 인터페이스에 대해서 적용 가능
  • Type parameter constraints
    • 타입에 대한 upper bound를 정한다.
    • Java <T extends Number> -> <T : Number>
    • 타입이 여러 인터페이스를 구현해야 할 경우는 where을 쓴다.
  • 기본 upper bound는 Any?이다.
    • 만약 non-null 타입을 원하면 Any혹은 다른 non-null 타입을 지정한다.
1
2
3
4
5
// 두가지 인터페이스 모두 구현한 타입이어야 할 때
// 가독성이 떨어져서 안쓰는 편이 좋지 않을까?
fun <T> funcName(seq: T) where T:TypeA, T:TypeB {
  ...
}

Generics at runtime

  • Java와 같다. 문법적 설탕
  • 런타임에는 모두 지워짐
  • 일반적으로 불가: value is List<String>
    • is 연산자로 제너릭 타입 체크 불가
    • 런타임에 지워지기 때문
    • 다만 코틀린 컴파일러가 이미 Generic 내부 타입을 알 경우에는 사용 가능
  • star projection
    • 타입을 사용할 때 쓸 수 있음
    • 대표적으로는 value is List<*>
    • Java에서는 * 대신 ? 였음
  • reified type
    • inline 함수에서 Generic의 타입 정보를 보존함
    • 실질적으로 runtime 체크가 아니다.
    • 인라인 되었을 때에 가능한 수준에서 사용 가능
    • typecast, check, reflection, type of argument로 사용 가능
    • 객체 생성, 함수 정의에 사용 불가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 컴파일 단계에서 보장되었으므로 가능한 타입 비교
// 원래 c is List<Int>는 에러이나
// c가 Collection<Int> 인것을 알고 있으므로 가능함
fun printSum(c: Collection<Int>) {
	if (c is List<Int>) {
		println(c.sum())
	}
}

// reified
// inline 되었다면 T의 타입은 자명해지므로 T::class.java 같이 호출 가능
inline fun <reified T> printSum(c: Collection<T>) {
	if (Integer::class.java == T::class.java) println("Can!")
	else println("Cant!")
}

Declare-site variance

  • covariant class/interface Producer
    • out으로 지정
    • 타입 T를 가진 객체를 produce할 뿐, consume하지 않는다.
    • 즉, generic 타입 T를 리턴에만 이용함
    • Cat를 리턴하는 객체는, Animal을 리턴하는 객체 일 수 있다.
      • 리턴된 값은 모두 Cat이므로, 전부 Animal에 속한다.
    • List<Int>List<Number>가 될 수 있다.
      • List<Int>의 모든 원소는 Int이므로 Number에 속한다.
  • contravariant class/interface Consumer
    • in으로 지정
    • 타입 T를 가진 객체를 consume할 뿐, produce하지 않는다.
    • 즉, generic 타입 T를 인자의 타입에만 이용함
    • Animal을 받는 객체는, Cat을 받는 객체일 수 있다.
      • Animal을 받는 객체는 Cat도 받을 수 있다.
    • Comparable<Number>Comparable<Int>가 될 수 있다.
      • Number를 비교할 줄 아는 객체는 Int도 비교할 줄 안다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open class Animal
class Cat: Animal()

class Herd<in T> {
    fun join(x: T) {}
}

fun main() {
  // Covariant
	var catList = listOf<Cat>(Cat(), Cat());
  var animalList: List<Animal> = catList

  // Contravariant
  var animalHerd = Herd<Animal>()
  animalHerd.join(Animal())
  animalHerd.join(Cat())
  var catHerd: Herd<Cat> = animalHerd
  catHerd.join(Cat())
}

Use-site variance

  • out Type: java의 ? extends Type를 대체
  • in Type: java의 ? super Type를 대체
1
2
fun copy(from: Array<out Number>, to: Array<Number>) { ... }
fun fill(dest: Array<in String>, value: String) { ... }

Annotation

  • 일단 자바에서와 맥락이 거의 같다
  • 3가지 용도로 사용된다.
    • compiler에 줄 정보를 기록
    • Source code processing tools에 줄 정보를 기록
    • 런타임에 app이 사용할 수 있도록 metadata를 기록
  • use-site: 적용 대상을 특정
    • Java 함수와 Kotlin 함수가 1:1 매칭이 되지 않기 때문
    • property, field, get, set
  • annotation 선언
    • Java에서는 @interface로 선언
      • 파라미터를 받는 것을 함수로 표현
    • Kotlin에서는 annotation class로 선언
      • 생성자 사용하듯 자연스럽게 쓰면 됨
  • meta annotation
    • annotation class에 적용되는 annotation
  • annotation parameter
    • class의 경우 Person::class처럼 바꿔서 인자로 사용 가능
  • reflection
    • annotation이나 필드, 프로퍼티등을 런타임에 접근할 수 있음
1
2
3
4
// annotation 선언
annotation class JsonName(val name: String)
// Java
public @interface JsonName { String value(); }