7. 연산자 오버로딩과 기타 관례
CS/코틀린 인 액션

7. 연산자 오버로딩과 기타 관례

7장부터 Part 2로 코틀린답게 사용하기가 시작된다..

별개로 여기 장에서 중간에 나오는 철학이 일단 보기 편하게 작성하고, 확장해야할 때 그때 수정하자가 나온다.

0. 7장에서 다루는 내용

- 연산자 오버로딩

- 관례: 여러 연산을 지원하기 위해 특별한 이름이 붙은 메서드

- 위임 프로퍼티

 

기본적인 것들은 1부에서 다 했으니 고급 기본 알려주는 느낌?

 

7.1 산술 연산자 오버로딩

에 들어가기 전에 알아야 개념은 코틀린은 자바의 타입을 가져다 쓰며 자바를 홰손하지 않기 위해 코틀린으로 가져와 확장 함수 형태로 만들어 사용한다.

 

+, -, +=, -=, ++, -- 이런것들

이런것들을 사용하려면 함수 앞에 저 기호를 사용하게 한다는 operator를 써야 한다.

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(1, 2)
val p2 = Point(3, 4)
println(p1 + p2)

// Point(x=4, y=6)

a + b => a.plus(b)

로 치환됨

 

다음은 가능한 오퍼레이터 목록들

함수 이름
a * b times
a / b div
a % b mod(1.1부터 rem)
a + b plus
a - b minus

확장함수로도 정의 가능

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

val p = Point(10, 20)
println(p * 1.5)
// println(1.5 * p)

조심할건 교환법칙은 안된다는 건데, 숫자를 앞에 두고 계산하게 하려면 저 곱셈 기본 타입 Times에서 확장해서 사용해야 한다.

 

operator fun Char.times(count: Int): String {
    return toString().repeat(count)
}

println('a' * 3) // 'aaa'

 

+= 같은건 복합 대입이라고 부르는데, 기본적으로 val로 정의하기 때문에 자체의 값을 바꾼다는 +=는 별로 의미가 없다. 그래서 copy하거나 내부 프로퍼티만 변경하고 싶을 땐 var로 정의해야 함.

컬렉션 같은 거엔 코틀린이 기본적으로 짜놨음

val numbers = ArrayList<Int>()
numbers += 42
println(numbers[0])

그래서 위 코드는 이 코드가 내장되어 있다고 보면 된다.

operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

주의할 점은 plus와 plusAssign은 동시에 정의하면 안된다고 한다.

 

 

그 다음은 단항연산자

operator fun Point.unaryMinus(): Point {
    return Point(-x, -y)
}

val p = Point(10, 20)
println(-p)
함수 이름
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec

a+ => a.unaryPlus()

로 변환됨.

 

7.2 비교 연산자 오버로딩

equals는

a == b => a?.equals(b) ?: (b == null)

로 치환됨.

a에도 ?가 붙은 이유는 a가 null인 것도 고려해야 하므로, 즉 null == null 일 수도 있잖아?

 

class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        if (other === this) return true
        if (other !is Point) return false
        return other.x == x && other.y == y
    }
}

근데 아까는 operator를 쓰더니 여기선 왜 override를 쓰느냐? 그건 이미 상위 클래스인 Any의 equals에 operator가 붙어있기 때문에, operator를 실행하면 자동으로 상위 클래스의 operator 지정이 적용되기 때문임.

==는 값이 같은거, ===는 참조가 같은거

data class를 사용할 경우 컴파일러가 자동으로 생성해준다.

 

compareTo는 Comparable을 상속해서 여기 안의 compareTo를 정의해서 사용하는 거다.

a >= b  ->  a.compareTo(b) >= 0

으로 치환됨. 얘를 정의하면 boolean이 아닌 숫자로 반환하기 때문에 비교 연산자 <, >, <=, >=도 자동으로 된다.

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

val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
println(p1 < p2) // true

compareValuesBy는 첫번째 비교하고 두번째 비교하고 다 같으면 0을 반환하고 아니면 숫자로 표시하는 내장함수.

이미 Comparable 인터페이스를 구현하는 모든 자바 클래스를 코틀린에서느 간결한 연산자 구분으로 비교할 수 있다.

println("abc" < "bac") // true

 

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

갑자기 컬렉션이라서 헷갈렸는데, 리스트랑 맵 같은거. 얘내들도 get, set을 정의해서 p[0] 이런식으로 접근 가능해진다.

operator fun Point.get(index: Int) : Int {
    return when(index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

val p = Point(10, 20)
println(p[1]) // 20

컴파일러가 다음으로 치환함

x[a,b] => x.get(a,b)

 

set도 정의 가능하다.

data class MutablePoint(var x: Int, var y: Int)
operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

val p = MutablePoint(10, 20)
p[1] = 42
println(p) // MutablePoint(x=10, y=42)

x[a, b] = c  ->  x.set(a, b, c)

 

 

그 다음은 in

data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x &&
            p.y in upperLeft.y until lowerRight.y
}

val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect) // true
println(Point(5, 5) in rect) // false

a in c  ->  c.contains(a)

 

 

rangeTo

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

start..end -> start.rangeTo(end)

 

 

for는 애초에 list같은 애들이 Iterator를 상속하고 내부적으로 hasNext와 next를 반복하여 호출하는 식으로 구성되어있었다. 

operator fun ClosedRange<LocaleDate>.iterator(): Iterator<LocaleDate> = 
    object : Iterator<LocaleDate> {
        var current = start
        override fun hasNext() = current <= endInclusive
        override fun next() = current.apply { current = plusDays(1) }
    }

val newYear = LocaleDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) {
    println(dayOff)
}

/*
2016-12-31
2017-01-01
*/

 

 

7.4 구조 분해 선언과 component 함수

val (x,y) = p

val p = Point(10, 20)
val (x, y) = p

이런거 정의

componentN이라는 함수를 호출한다. data class는 자동으로 componentN 함수를 만들어준다.

data class NameComponents(val name: String,
                            val extension: String)

fun splitFilename(fullName: String): NameComponents {
    val result = fullName.split('.', limit = 2)
    return NameComponents(result[0], result[1])
    // return result[0], result[1] 이런건 안되기 때문
}

val (name, ext) = splitFilename("example.kt")
println(name) // example
println(ext) // kt

 

 

이미 코틀린 내부적으로도 루프같은 구조 분해 선언을 지원하는 곳이 있음. map 같은거

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
    for ((key, value) in map) {
        println("$key = $value")
    }

 

 

 

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

사실 여기가 반 분량이다.

클래스의 프로퍼티를 저장하는 걸 아얘 다른 저장소에 저장할 수도 있고, 저장하면서 아얘 notify용 클래스에게 넘겨주어 얘한테 저장하게 시킨다던지 한다.

이걸 알려주기 위한 사전 작업들이다.

type by 해서 다른 클래스한테 저장을 위임한다. 이때 넘기는 기능을 위임 프로퍼티(delegated property) 라고 하며 위임해주는 도우미 객체를 위임 객체(delegate) 라고 부른다.

class Delegate {
    operator fun getValue(...) {...}
    operator fun setValue(..., newValue: Type) {...}
}
class Foo {
    var p: Type by Delegate()
}

val foo = Foo()
val oldValue = foo.p // calls Delegate.getValue(...)
foo.p = newValue // calls Delegate.setValue(..., newValue)

 

by lazy()를 통한 초기화 지원은 전에 봤던 lazy를 위임 프로퍼티에서도 쓸 수 있다는 것이다.

by를 쓰지 않고 풀어서 쓰면

data class Email(val address: String)
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(Email("${person.name}@test.kt"))
}

class Person(val name: String) {
    private var _emails: List<Email>? = null
    val emails: List<Email>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this)
            }
            return _emails!!
        }
}

val p = Person("Alice")
p.emails // Load emails for Alice
p.emails // "No loading"

저렇게 되는데 by lazy를 사용하면 저 작업을 컴파일러가 짜준다.

data class Email(val address: String)
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(Email("${person.name}@test.kt"))
}

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}

val p = Person("Alice")
p.emails // Load emails for Alice
p.emails // "No loading"

 

 

위임 프로퍼티는 다음과 같이 구현한다.

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

지금은 어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶다. 자바에서는 PropertyChangeSupport와 PropertyChangeEvent 크래스를 사용해 이런 통지를 처리한다. 일단 자바 식으로 구현하고 코틀린으로 어떻게 하는지 알아볼려고 한다.

 

저렇게 짠 상속용 클래스를 상속해서 우리가 기존에 만들던 클래스를 만들자. protected를 사용해서 자식도 저 프로퍼티를 사용할 수 있음을 주목하자.

import java.beans.PropertyChangeListener
import java.beans.PropertyChangeSupport

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    var age: Int = age
    set(newValue) {
            val oldValue = field // 뒷받침하는 필드에 접근할 때 field 식별자를 사용한다. 약속된 변수명
            field = newValue
            changeSupport.firePropertyChange(
                "age", oldValue, newValue
            )
        }
    var salary: Int = salary
    set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                "salary", oldValue, newValue
            )
        }
}

fun main(args: Array<String>) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println(
                "Property ${event.propertyName} changed " +
                        "from ${event.oldValue} to ${event.newValue}"
            )
        }
    )
    p.age = 35 // Property age changed from 34 to 35
    p.salary = 2100 // Property salary changed from 2000 to 2100
}

위에서 필요에 따라 통지를 보내는 클래스를 추출해서 중복을 줄여보자

 

class ObservableProperty(
    val propName: String, var propValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    fun getValue(): Int = propValue
    fun setValue(newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(value) { _age.setValue(value) }
    val _salary = ObservableProperty("salary", salary, changeSupport)
    var salary: Int
        get() = _salary.getValue()
        set(value) { _salary.setValue(value) }
}

추출용 클래스를 빼내도 Person에서 중복이 좀 있는데, 얘도 코틀린의 위임 프로퍼티 기능을 활용하면 없앨 수 있다.

그러기 위해 일단 추출용 클래스를 코틀린 관례에 맞춰 바꾸자.

 

class ObservableProperty(
    var propValue: Int, val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    var age: Int by ObservableProperty(age, changeSupport)
    var salary: Int by ObservableProperty(salary, changeSupport)
    
}

하지만 코틀린 표준 라이브러리에는 위에서 짰던 ObservableProperty와 비슷한 클래스가 이미 있다. 다만 이 표준 라이브러리의 크래스는 PropertyChangeSuppoert와는 연결돼 있지 않다. 따라서 프로퍼티 값의 변경을 통지할 때 PropertyChangeSupport를 사용하는 방법을 앙ㄹ려주는 람다를 그 표준 라이브러리 클래스에게 넘겨야 한다.

class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    private val observer = {
            prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

 

코틀린 컴파일러에서 by가 어떻게 되는지 보면 다음과 같이 된다.

class C {
    var prop: Type by MyDelegate()
}
class C {
    private val <delegate> = MyDelegate()
    var prop: Type
        get() = <delegate>.getValue(this, <property>)
        set(value: Type) = <delegate>.setValue(this, <property>, value)
}

컴파일러는 MyDelegate의 감춰진 인스턴스를 저장해서 사용한다. 이 프로퍼티를 표현하기 위해 KProperty 타입의 객체를 사용한다.

 

맵에다가 위임하는 예시도 한번 보자. 맵에다가 저장함으로써 프로퍼티를 동적으로 확장할 수 있다.

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    // 필수정보
    val name: String
        get() = _attributes["name"]!!
}

fun main(args: Array<String>) {
    val p = Person()
    val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
    for ((attrName, value) in data) {
        p.setAttribute(attrName, value)
    }
    println(p.name)
}
class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    // 필수정보
    val name: String by _attributes
}

 

위임을 다른 프레임워크에도 할 수 있다. DB 같은거.

object Users : idTable() {
    val name = varchar("name", 50).index()
    val age = integer("age")
}

class User(id: EntityID) : Entity(id) {
    var name by Users.name
    var age by Users.age
}

operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
    // 데이터베이스에서 칼럼 값 가져오기
}

operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
    // 데이터베이스에 칼럼 값 저장하기
}

user.age += 1 이라는 식을 하면 user.ageDelegate.setValue(user.ageDelegate.getValue() + 1)와 비슷한 코드로 변환된다.

완전한 구현은 Exposed 프레임워크 소스코드에서 볼 수 있다(https://github.com/JetBrains/Exposed). 11장에서 프레임워크를 이용한 설계기법을 자세히 살펴본다고 함.