본문 바로가기

Kotlin

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

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

코틀린이 지원하는 convention 사용법에 대해 알아보자!

 

산술 연산자 오버로딩

  • 자바 : + 연산자를 이용할 수 있는 타입은 정해져 있다.
  • 코틀린 : 어디서든 일반 산술 연산자를 선언할 수 있다.
data class Point(var x: Int, var y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

>> p1+p2 // 와 같은 연산이 가능

a + b 는 컴파일 시점에는 자바처럼 a.plus(b)로 변한다.

operator fun Point.plus(other:Point):Point {
    return Point(x+other.x, y+other.y)
}
함수 이름
* times
+ plus
/ div
- minus
% mod

두 피연산자의 타입이 다른 연산자 정의하기

operator fun Point.times(scale: Double):Point {
    return Point((x*scale).toInt(),(y*scale).toInt())
}
  • 코틀린에서는 교환 법칙 지원하지 않음 p*1.5는 되지만, 1.5*p 는 안됨

복합 대입 연산자 오버로딩 ( compound assignment )

>>> var point = Point(1,2)
>>> point += Point(2,3)

단항 연산자 오버로딩

함수 이름
+a unaryPlus
-a unaryMinus
!a not
++a, a++ inc
--a, a-- dec

비교 연산자 오버로딩

- 동등자 연산자 : equals()

a == b -> a?.equals(b) ?: ( b==null) 로 컴파일이 됨

 

data class Point(var x: Int, var y: Int) {

    override fun equals(obj: Any?): Boolean {
        if (obj === this) return true    // 최적화
        if (obj !is Point) return false

        return obj.x == x && obj.y == y
    }
}
  • operator를 안 붙여도 되는 이유 ? 상위 클래스에 정의된 메소드를 오버라이딩 하기 때문

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

get, set convention

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

>>> var point = Point(10,20)
>>> println(point[0])
20


data class MutuablePoint(var x:Int, var y:Int)

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

>>> var point = Point(10,20)
>>> point[0]=20
>>> println(point)
MutuablePoint(x=20,y=20)

in convention

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
}

rangeTo convention

>>> val now = LocalDate.now()
>>> val vacation = now .. now.plusDays(10) // = now.rangeTo(now.plusDays(10))
>>> println(now.plusWeeks(1) in vacation ) 
true
  • rangeTo 함수는 Comparable에 대한 확장 함수

for 루프를 위한 iterator 관례

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

구조 분해 선언과 component 함수

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

val ( x,y ) = p 는 아래와 같이 컴파일 된다

val a = p.component1()
val b = p.component2()
// 구조 분해 선언을 사용해 여러 값 반환하기
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])
}

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

구조 분해 선언과 루프

fun printEntries(map : Map<String, String>) {
	for( (key,value) in map) {
    	println("$key -> $value")
    }
}

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

코틀린이 제공해주는 관례중 독특하면서도 가장 중요한 기능이 위임 프로퍼티(delegated property)라고 한다.

 

위임 프로퍼티를 사용하면 어떻게 코드를 작성할 수 있는지 살펴보자

 

1. 직접 구현하기

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 // backing field
            field = newValue
            changeSupport.firePropertyChange("age", oldValue, newValue)
        }

    var salary: Int = salary
        set(newValue) {
            val oldValue = field // backing field
            field = newValue
            changeSupport.firePropertyChange("salary", oldValue, newValue)

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