본문 바로가기
kotlin

7. 연산자 overloading과 관례

by AsCE_hyunseung 2022. 1. 23.

관례(convention): 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법

  • 기존 자바 클래스가 구현하는 인터페이스는 이미 고정되어있기 때문에 코틀린쪽에서 확장 함수를 사용해서 기존 클래스에 새로운 메소드를 추가할 수 있게 만들었다

7.1 산술 연산자 오버로딩

  • 연산자를 오버로딩하는 함수 앞에는 operator를 붙여줘야한다

7.1.1 이항 산술 연산 오버로딩

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

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    val p3 = p1 + p2 // p1.plus(p2)
    println(p3) // Point(x=40, y=60)
}
  • operator X
data class Point(val x: Int, val y: Int) {
    fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    val p3 = p1 + p2
    println(p3) // 'operator' modifier is required on 'plus' in 'Point'
}
  • 오버로딩 가능한 이항 산술 연산자
    • 오버로딩을 하더라도 연산자간 우선순위는 달라지지않는다.

식 함수 이름

a/b div
a*b times
a%b mod(1.1부터 rem)
a+b plus
a-b minus
  • 연산자를 오버로딩 하더라도 교환 법칙을 지원하지 않는다.
data class Point(val x: Int, val y: Int) {
    operator fun times(scale: Double): Point {
        return Point((x * scale).toInt(), (y * scale).toInt())
    }
}

fun main() {
    val p1 = Point(10, 20)
    println(p1 * 1.5) // Point(x=15, y=30)
}
data class Point(val x: Int, val y: Int) {
    operator fun times(scale: Double): Point {
        return Point((x * scale).toInt(), (y * scale).toInt())
    }
}

fun main() {
    val p1 = Point(10, 20)
    println(1.5 * p1) // compile error
}

7.1.2 복합 대입 연산자 오버로딩

  • +=, -= 같은 연산자를 복합 대입 연산자라 부른다.
data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

fun main() {
    var p1 = Point(10, 20)
    p1 += Point(3, 4) // p1.plus(Point(3,4))
    println(p1) // Point(x=13, y=24)
}
  • 코틀린 표준 라이브러리는 변경 가능한 컬렉션에 대해 plusAssign을 정의한다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

fun main() {
   val arr = arrayListOf(10, 20)
    arr += 30
    println(arr) // [10, 20, 30]
		// arr.plusAssign(30)
}

7.1.3 단항 연산자 오버로딩

data class Point(val x: Int, val y: Int)

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

fun main() {
    val p = Point(10, 20)
    println(-p) // Point(x=-10, y=-20)
		// p.unaryMinus()
}

식 함수 이름

+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
—a, a— dec

7.2 비교 연산자 오버로딩

7.2.1 동등성 연산자

  • 코틀린은 == 연산자 호출을 equals 메소드 호출로 컴파일한다
  • 내부에서 인자가 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;
    }
}

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(10, 20)
    println(p1 == p2) //true
		// p1?.equals(p2) ?: (p2 == null)
}
  • equals 함수는 Any에 정의된 메소드이므로 override가 필요하다
  • 상위 클래스의 메소드에서 operator가 붙어있어서 안붙이고 override해도된다
public open class Any {
  public open operator fun equals(other: Any?): Boolean
}

7.2.2 순서 연산자

  • 비교 연산자는 compareTo 호출로 컴파일된다.
class Person(val lastName: String): Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return compareValuesBy(this, other, Person::lastName)
    }
}

fun main() {
    val p1 = Person("Alice")
    val p2 = Person("Bob")
    println(p1 < p2) // true
		// p1.compareTo(p2) < 0
}

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

7.3.1 인덱스로 원소에 접근

  • 원소를 읽는 연산은 get 연산자 메소드로 변환된다.
data class Point(val x: Int, val y: Int) {
    operator fun get(index: Int): Int {
        return when (index) {
            0 -> x
            1 -> y
            else ->
                throw IndexOutOfBoundsException("Invalid coordinate $index")
        }
    }
}

fun main() {
    val p = Point(10, 20)
    println(p[1]) // 20
		// p.get(1)
}
  • 원소를 쓰는 연산은 set 연산자 메소드로 변환된다.
data class MutablePoint(var x: Int, var y: Int) {
    operator fun set(index: Int, value: Int) {
        when (index) {
            0 -> x = value
            1 -> y = value
            else -> throw IndexOutOfBoundsException("invalid")
        }
    }
}

fun main() {
    val p = MutablePoint(10, 20)
    p[1] = 42 // == p.set(1, 42)
    println(p) // MutablePoint(x=10, y=42)
}

7.3.2 in

  • in 연산자와 대응하는 함수는 contains 함수다
data class Point(val x: Int, val y: Int)

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

fun main() {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect) // true
		// == rect.contains(Point(20, 30))
}

7.3.3 rangeTo

  • ..연산자는 rangeTo 함수를 간략하게 표현하는 방법이다
fun main() {
    val now = LocalDate.now()
    val vacation = now..now.plusDays(10) // now.rangeTo(now.plusDays(10))
    println(now.plusWeeks(1) in vacation) // true
}

7.3.4 iterator

  • for 루프 안에서의 in은 list.iterator()를 호출해서 이터레이터를 얻은 다음, hasNext, next 호출을 반복하는 식으로 변환된다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> {
    var current = start

    override fun hasNext() = current <= endInclusive

    override fun next() = current.apply {
        current = plusDays(1)
    }
}

fun main() {
    val newYear = LocalDate.ofYearDay(2022, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) {
        println(dayOff)
        // 2021-12-31
        // 2022-01-01
    }
}

7.4 구조 분해 선언과 component 함수

구조 분해 선언

  • 복합적인 값을 분해해서 여러 다른 변수를 초기화 할 수 있다.
data class Point(val x: Int, val y: Int);

fun main() {
    val p = Point(10, 20);
    val (x, y) = p;
    println(x) // 10
    println(y) // 20
}
  • 구조 분해 선언에도 관례를 사용할 수 있다.
  • 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다
class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
};

fun main() {
    val p = Point(10, 20);
    val (x, y) = p;
    // val x = p.component1()
    // val y = p.component2()
}
  • 코틀린 표준 라이브러리에서는 맨 앞 다섯 원소에 대한 componentN를 제공한다. 이로 인해서 원소 갯수에 따라서 에러 발생 시점이 달라지게 되는데
    • 컬렉션 크기를 벗어나는 위치에 원소에 대한 구조 분해 선언을 사용하면 런타임 에러가 발생한다.
    • 여섯 개 이상의 변수를 사용하는 구조 분해를 컬렉션에 대해 사용하면 컴파일 에러가 발생한다.
fun main() {
    val x = listOf(1, 2);
    val (a,b,c) = x; //runtime error(Index 2 out of bounds for length 2)
    val (d,e,f,g,h,i) = x; //compile error(Destructuring declaration initializer of type List<Int> must have a 'component6()' function)
}

7.4.1 구조 분해 선언과 루프

  • 변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 할 수 있다.
fun printEntries(map: Map<String, String>) {
    for((key, value) in map) {
        println("$key -> $value")
    }
}

fun main() {
    val map = mapOf("key1" to "value1", "key2" to "value2")
    printEntries(map)
    //key1 -> value1
    //key2 -> value2
}
  • 코틀린 라이브러리는 Map.Entry에 대한 확장 함수로 component1, component2를 제공한다.
fun printEntries(map: Map<String, String>) {
    for((key, value) in map) {
        println("$key -> $value")
    }
		//for (entry in map.entries) {
    //    val key = entry.component1()
    //    val value = entry.component2()
    //    println("$key -> $value")
    //}
}

public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value

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

위임(delegate)

  • 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴
  • 이때 작업을 처리하는 도우미 객체를 위임 객체라고 부른다.

7.5.1 위임 프로퍼티 소개

class Foo {
    var p: Type by Delegate() // by 키워드는 프로퍼티와 위임 객체를 연결한다.
		// private val delegate = Delegate() (컴파일러가 생성한 숨겨진 도우미 프로퍼티)
    // var p: Type
    // set(value : Type) = delegate.setValue(..., value: Type)
    // get() = delegate.getValue(...)
}
  • p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다.
  • 컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화한다.
  • 프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValue와 setValue 메소드를 제공해야한다.
class Delegate {// Delegate 클래스를 단순화한 코드이다
	operator fun getValue(...) {...}
	operator fun setValue(..., value: Type) {...}
}

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

fun main() {
    val foo = Foo()
    val oldValue = foo.p // delegate.getValue() 호출
    foo.p = newValue // delegate.setValue() 호출
}

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

지연 초기화

  • 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴이다.
  • 초기화 과정에 자원을 많이 사용하거나, 객체를 사용할 때마다 초기화하지 않아도 되는 프로퍼티에 대해 사용할 수 있다.

위임 프로퍼티 사용하기 전 로직

  • thread-safe하지 않다
fun loadEmails(person: Person): List<Email> {
    // 대충 이메일 리스트 가져오는 로직
    return listOf(...)
}

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

fun main() {
    val p = Person("alice")
    println(p.emails) //emails를 처음 읽을때만 요청하게 된다
    println(p.emails) 
}

위임 프로퍼티 사용한 후 로직

fun loadEmails(person: Person): List<Email> {
   // 대충 이메일 리스트 가져오는 로직
   return listOf(...)
}

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

fun main() {
    val p = Person("alice")
    println(p.emails) //emails를 처음 읽을때만 요청하게 된다
    println(p.emails) 
}

lazy

  • 위임 객체를 반환하는 표준 라이브러리 함수
  • 코틀린 관례에 맞는 시그니처의 getValue 메소드가 들어있는 객체를 반환한다
  • thread-safe 하다
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

7.5.3 위임 프로퍼티 구현

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 = newValue
            changeSupport.firePropertyChange("age", oldValue, newValue)
        }
    var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange("salary", oldValue, newValue)
        }
}

fun main() {
    val p = Person("alice", 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 = 1000 // Property salary changed from 2000 to 1000
}
  • 도우미 클래스를 통해 프로퍼티 변경 통지 구현하기
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)
    }
}

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() {
    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)
        }

}

fun main() {
    val p = Person("alice", 34, 2000)
    p.addPropertyChangeListener { event -> println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}") }

    p.age = 35 // Property age changed from 34 to 35
    p.salary = 1000 // Property salary changed from 2000 to 1000
}
  • ObservableProperty를 프로퍼티 위임에 사용할 수 있게 바꾸었다.
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와 비슷한 클래스가 있다. (Delegates.observable)
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)
}
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() {
    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)
}

fun main() {
    val p = Person("alice", 34, 2000)
    p.addPropertyChangeListener { event -> println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}") }

    p.age = 35 // Property age changed from 34 to 35
    p.salary = 1000 // Property salary changed from 2000 to 1000
}

7.5.4 위임 프로퍼티 컴파일 규칙

  • 컴파일러는 MyDelegate 클래스의 인스턴스를 감춰진 <delegate> 프로퍼티에 저장하며 그 감춰진 프로퍼티를 <delegate>라는 이름으로 부른다
  • 컴파일러는 프로퍼티를 표현하기 위해 KProperty 타입의 <property> 객체를 사용한다.
  • 컴파일러는 모든 프로퍼티 접근자 안에 getValue와 setValue 호출 코드를 생성해준다.
class C {
    var prop: Type by MyDelegate()

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

val c = C()

val x = c.prop // val x = <delegate>.getValue(c, <property>)
c.prop = x // <delegate>.setValue(c, <property>, x)

7.5.5 프로퍼티 값을 맵에 저장

  • 위임 프로퍼티를 사용하지 않은 경우
class Person {
    private val _attributes = hashMapOf<String, String>()

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

    val name: String
        get() = _attributes["name"]!!
}

fun main() {
    val p = Person()
    val data = mapOf("name" to "alice", "company" to "kakao")

    for ((attName, value) in data)
        p.setAttributes(attName, value)

    println(p.name) // alice
}
  • 위임 프로퍼티를 사용한 경우
  • 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue, setValue 확장 함수를 제공하기 때문에 더 간결하게 사용 가능하다
  • 확장 가능한 객체
    • 위임 프로퍼티를 활용해서 자신의 프로퍼티를 동적으로 정의할 수 있는 객체
class Person {
    private val _attributes = hashMapOf<String, String>()

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

    val name: String by _attributes // 위임 프로퍼티(확장 가능한 객체)
}

fun main() {
    val p = Person()
    val data = mapOf("name" to "alice", "company" to "kakao")

    for ((attrName, value) in data) {
        p.setAttributes(attrName, value)
    }

    println(p.name) // alice
		// p.name -> _attributes.getValue(p, props) -> _attributes[prop.name]
}

7.5.6 프레임워크에서 위임 프로퍼티 활용

  • 각 엔티티 속성(name, age)은 위임 프로퍼티이고, 컬럼 객체(Users.name, Users.age)를 위임 객체로 사용한다.
object Users: IdTable() {
    val name = varchar("name", length = 50).index()
    val age = integer("age")
}

class User(id: EntityID): Entity(id) {
    var name: String by Users.name
    var age: String by Users.age
}
  • 프레임워크는 Column 클래스 안에 getValue와 setValue 메소드를 정의한다.
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
    // 컬럼 값 가져오기
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
    // 컬럼 값 변경하기
}

7.6 요약

  • 코틀린에서는 미리 정해진 이름의 함수를 이용해서 산술 연산자를 오버로딩할 수 있다.
  • 비교연산자는 equals(등호)와 compareTo(부등호) 메소드로 변환된다
  • 클래스에 get, set, contains라는 함수를 정의하면 그 클래스의 인스턴스에 대해 [], in 연산을 사용할 수 있고 컬렉션 객체와 비슷하게 다룰 수 있다.
  • 위임 프로퍼티를 통해 프로퍼티 값을 저장, 초기화, 읽기, 변경할때 사용하는 로직을 재활용할 수 있다.
  • Delegates.observable 함수를 사용하면 프로퍼티 변경을 관찰할 수 있는 관찰자를 쉽게 추가할 수 있다
  • 맵을 위임 객체로 사용하는 위임 프로퍼티를 통해 다양한 속성을 제공하는 객체를 유연하게 다룰 수 있다.