본문 바로가기

코틀린

[Kotlin In Action] 7장. 연산자 오버로딩과 기타 관례

산술 연산자 오버로드

관례란?

어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 말한다. 
ex) 산술 연산자 - 어떤 클래스에 plus라는 메서드를 정의할 때 그 클래스의 인스턴스에 대해 + 연산자를 사용할 수 있다. 

이항 산술 연산자 

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

>>> val p1 = Point(10, 20)
>>> val p2 = Point(30, 40)
>>> println(p1 + p2)
Point(x=40, y=60)
  • operator 키워드와 지정한 함수 이름을 사용해서 연산자를 오버로딩
  • 기본적으로 연산자 오버로딩을 사용할 때는 operator 키워드를 명시해야 함
    • 이는 이 함수가 어떤 관례를 따르고 있는지 명시하기 위함
operator fun Point.plus(other:Point):Point {
    return Point(x + other.x, y + other.y)
}

operator fun Point.times(scale: Double): Point { ... }
  • 확장함수로 만드는 것도 가능
  • 여러 타입에 대한 연산자 오버로딩도 가능
Expression Function name
a*b times
a/b div
a%b mod(1.1부터 rem)
a+b plus
a-b minus

 

복합 대입 연산자 오버로딩

  • plus와 minus와 같은 연산자를 오버로딩하면 코틀린은 관련있는 +=, ‐=의 복합 대입 연산자도 자동으로 지원
var point = Point(1, 2)
point += Point(3, 4) // point = point + Point(3, 4)

 

변경 가능한 컬렉션에 원소를 추가하는 경우

operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}
  • 반환 타입이 Unit인 plusAssign 함수를 정의할 수 있음
  • 해당 컬렉션이 읽기 전용일 경우 변경을 적용한 복사본을 반환 

+=에 대해 plus와 plusAssign 둘 다 정의할 시 컴파일 오류가 발생한다. 
변수를 val로 한다면 plusAssign을 사용하자. 

단항 연산자 오버로딩

Expression Function name
+a unaryPlus
-a unaryMinus
!a not
++a,a++ inc
–a,a– dec
operator fun BigDecimal.inc() = this + BigDecimal.ONE
var bd = BigDecimal.ZERO
println(bd++)

 

비교 연산자 오버로딩

코틀린에서는 원시 타입 뿐만 아니라 모든 객체에 대해 비교 연산을 수행할 수 있다. 
자바와 달리 equals, compareTo를 호출하지 않고도 == 비교 연산자를 직접 사용할 수 있다. 

동등성 연산자: "equals"

== 연산자 호출은 equals 메서드 호출로 컴파일한다. 
Any의 equals에 이미 operator가 있으므로 붙일 필요가 없다.
식별자 비교 연산자인 ===을 사용해서 두 객체간의 참조가 같은지 비교할 수 있다. 

순서 연산자: "compareTo"

  • Comparable 인터페이스의 compareTo 메서드 호출 관례를 지원해줌
  • <, <=, >, >= 연산자를 Comparable#compareTo 호출로 컴파일
  • a >= b : a.compareTo(b) >= 0
  • compareTo는 Int 값을 반환
class Person(val firstName: String, val lastName: String) : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other, Person::firstName, Person::lastName)
    }
}
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
println(p1 < p2)

 

컬렉션과 범위에 대해 쓸 수 있는 관례

  • x[a, b] : x.get(a, b)
  • x[a, b] = c : x.set(a, b, c)
  • a in c : c.contains(a)

in 관례

  • 객체가 컬렉션에 들어있는지 검사
  • in 연산자와 대응하는 함수는 contains()

rangeTo 관례

  • .. 구문으로 범위를 생성
    • val range = 1 .. 10
  • rangeTo 함수 호출로 컴파일 됨
    • start..end : start.rangeTo(end)
  • rangeTo의 리턴 값은 ClosedRange<T: Comparable>
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

for 루프를 위한 iterator 관례

  • kotlin.collections.Iterator를 리턴하는 iterator() 함수는 for 루프에서 사용 가능
    • for(x in list) { ... } 같은 경우는 list.iterator()를 호출해 iterator를 얻음
    • 이 iterator에 대해 hasNext와 next 호출을 반복하는 식으로 변환됨
operator fun ClosedRange<LocalDate>.iterator() : Iterator<LocalDate> =
    object : Iterator<LocalDate> {
        var current = start
        override fun hasNext(): Boolean = current <= endInclusive
        override fun next(): LocalDate = current.apply {
            current = plusDays(1)
        }
    }
    
fun main(args: Array<String>) {
    val newYear = LocalDate.ofYearDay(2018, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) { println(dayOff) }
}

구조 분해 선언과 component 함수

구조 분해 선언과 루프

  • destructuring, declaration
  • 복합적인 값을 분해해서 여러 변수를 한꺼번에 초기화할 수 있음
val p = Point(10,20) // Point는 data 클래스
val (a, b) = p
/* 아래와 같이 컴파일 됨
 * val a = p.component1()
 * val b = p.component2()
 */

for ((key, value) in map) { // Map.Entry

}
  • data 클래스는 컴파일러가 각 프로퍼티에 대해 component 함수 자동 생성
  • 콜렉션의 경우 맨 앞 5개 원소에 대한 component 확장 함수 제공

data 클래스가 아닌 일반 클래스일 경우 아래와 같이 구현할 수 있음

class Point(val x: Int, val y: Int){
    operator fun component1() = x
    operator fun component2() = y
}

구조 분해 선언과 루프 관례를 동시에 사용한 예시는 다음과 같다.

for((key, value) in map) {
    println("$key -> $value")
}

자바와는 다르게 코틀린에서는 Map에 iterator를 사용할 수 있다. 

프로퍼티 접근자 로직 재활용: 위임 프로퍼티

위임 프로퍼티 소개

  • 프로퍼티 접근을 다른 객체에게 위임하는 기능 
  • 위임을 사용해 자신의 값을 필드가 아닌 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있음
class Foo {
    var p: Type by Delegate()
// p 프로퍼티에 대한 접근을 Delegate 객체에 위임
// p: 위임할 프로퍼티
// by 오른쪽 : 위임 객체
}

// 아래와 같이 컴파일 됨
class Foo {
    private val delegate = Delegate()
    var p: Type
    set(value: Type) = delegate.setValue(..., value)
    get() = delegate.getValue(...)
}
  • 위임한 프로퍼티를 읽고 쓸 때마다 위임 객체의 getValue/setValue 를 호출

위임 객체의 함수 규칙

  • operator getValue(obj: 프로퍼티_포함_타입, prop: KProperty<*>): 프로퍼티_타입
  • operator setValue(obj: 프로퍼티_포함_타입, prop: KProperty<*>, newValue:프로퍼티_타입)

위임 프로퍼티 사용 : by lazy()를 사용한 프로퍼티 초기화 지연

  • lazy initialization은 객체의 일부분을 초기화하지 않고 남겨두었다가 해당 값이 필요한 시점에 초기화 할 경우 사용
  • lazy 함수는 기본적으로 스레드 안전
// 프로퍼티 초기화 지연
class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}

public inline operator fun <T> Lazy<T>.getValue(
    thisRef: Any?, property: KProperty<*>): T = value

➕ 더 읽어보면 좋을 글

프로퍼티 값을 맵에 저장

  • 자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 사용할 수 있음
class Person {
    private val _attributes = hashMapOf<String, String>()
    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }
    val name: String by _attributes
}
  • 위 코드가 가능한 이유는 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장 함수를 제공하기 때문