본문 바로가기
프로그래밍/코틀린

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

by 직장인 투자자 2021. 12. 14.

7.1 산술 연산자 오버로딩

7.1.1 이항 산술 연산 오버로딩

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) // --> p1.plus(p2)
// Point(x=40, y=60)
  • operator keyword: 연산자 오버로딩 함수 앞에 필수, 관례를 따르는 함수임을 명시
    • 키워드 없는 경우 사용시 오류 발생
operator fun Point.plus(other: Point): Point {
  return Point(x + other.x, y + other.y)
}
  • 확장 함수로 정의 가능
operator fun Point.times(scale: Double): Point {
  return Point((x * scale).toInt(), (y * scale).toInt())
}
val p = Point(10, 20)
println(p * 1.5)
// Point(x=15, y=30)
  • 두 피연산자의 타입이 다른 연산자 정의 가능
  • 코틀린 연산자가 교환 법칙(Commutativity)를 만족하지 않음 --> 1.5 * p는 불가
    operator fun Char.times(count: Int): String {
    return toString().repeat(count)
    }
    println('a' * 3)
    // aaa
  • return type 또한 같을 필요 없음
  • 숫자 연산과 동일하게 연산자 우선순위 적용됨

연산자 함수와 자바

  • 자바에서 연산자 오버로딩 된 것을 함수로 호출 가능
  • 자바를 코틀린에서 호출하는 경우도 함수 이름이 관례와 맞으면 연산자 식 사용 가능
    • 동일 기능이 있지만 관례와 맞이 않은 경우 확장 함수 적용하면 됨

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

  • 복합 대입(compound assignment): +=, -=
var point = Point(1, 2)
point += Point(3, 4) // point = point + Point(3, 4)
println(point)
// Point(x=4, y=6)
  • + 오버로딩하면 +=도 자동으로 지원
operator fun <T> MutableCollection<T>plusAssign(element: T) {
  this.add(element)
}

val numbers = ArrayList<Int>()
numbers += 42
println(numbers[0])
// 42
  • 반환 타입이 Unit인 plusAssign 함수 정의 --> += 연산자 오버로드
  • minusAssign, timesAssign 등 존재
  • +=, + 동시에 정의 된 경우
    • 양쪽으로 컴파일 가능한 경우 -> 오류
    • val 사용 -> plusAssign 적용 불가능하게 만드는 방법을 고려할 수 있지만...
    • plus, plusAssign을 동시에 정의하지 말라

코틀린 표준 라이브러리 컬렉션

  • +, - 는 항상 새로운 컬렉션 반환
  • +=, -=는 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태 변화
  • 읽기전용 컬렉션 -> +=, -=는 변경을 적용한 복사본 반환 (var 타입 변수만 적용 가능)
  • 피연산자: 개별 원소 / 원소 타입이 같은 컬렉션
    val list = arrayListOf(1, 2)
    list += 3 // list를 변경
    val newList = list + listOf(4, 5) // 두 리스트의 모든 원소를 포함하는 새 리스트 반환
    println(list)
    // [1, 2, 3]
    println(newList)
    // [1, 2, 3, 4, 5]

7.1.3 단항 연산자 오버로딩

operator fun Point.unaryMinus(): Point { // 파라미터 없음
  return Point(-x, -y)
}
  • 마찬가지 방법, 파라미터 없음
  • +a -> a.unaryPlus()
operator fun BigDecimal.inc() = this s+ BigDecimal.ONE

var bd = BigDecimal.ZERO
println(bd++) // println 실행 후 값증가
// 0
println(++bd) // println 실행 전 값증가
// 2
  • Int에서 ++을 실행한 값과 동일
  • 후위 ++ -> 현재의 값 반환 후 연산 적용
  • 전위 ++ -> 연산 적용 후 값 반환

오버로딩할 수 있는 단항 산수 연산자
|식|함수 이름|
|------|---|
|+a|unaryPlus|
|-a|unaryMinus|
|!a|not|
|++a, a++|inc|
|--a, a--|dec|

7.2 비교 연산자 오버로딩

  • Kotlin: equals, compareTo 호출 없이 ==, !=로 비교 가능
  • a == b --> a?.equals(b) ?: (b == null)

7.2.1 동등성 연산자: equals

class Point(val x: Int, val 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
  }
}
  • equals는 Any에 정의된 함수이기 때문에 override 필요
  • 상위(Any)에 정의된 equalsoperator가 붙어있어 operator지정 불필요
  • 상속받은 equals가 확장 함수보다 우선순위 높음 --> 확장 함수로 equals 정의 불가

7.2.2 순서 연산자: compareTo

  • 자바에서 정렬, 최댓값 등 대소비교 필요한 경우 --> Comparable 인터페이스 구현 (compareTo 메서드 구현)
  • 코틀린도 Comparable 인터페이스 지원 + compareTo 메서드 호출 관례 제공
    • 비교연산자 (<, >, <=, >=)는 compareTo 호출로 컴파일 됨
    • a >= b -> a.compareTo(b) >= 0
class Person(val firstName: String, val lastName: String): Comparable<Person> {
  override fun compareTo(other: Person): Int {
    return compareValuesBy(this, other, Person::lastName, Person::firstName) // 인자로 받은 함수를 차례로 호출하며 값 비교
  }
}
  • Comparable 인터페이스 구현한 것 -> 자바 쪽 컬렉션 정렬 메서드 등에도 사용 가능
  • equals와 마찬가지로 operator 붙일 필요 없음 (ComparablecompareTo에 붙어 있으므로)
println("abc" < "bac")
// true
  • Comparable 인터페이스를 구현하는 모든 자바 클래스를 코틀린에서 연산자 구문으로 비교 가능

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

  • 인덱스 연산자: a[b]
  • in 연산자: 원소가 컬렉션이나 범위에 속하는지 검사 / 원소를 이터레이션

7.3.1 인덱스로 원소에 접근: get과 set

mutableMap[key] = newValue
  • get 연산자 메서드: 인덱스 연산자를 사용해 원소를 읽는 연산
  • set 연산자 메서드: 인덱스 연산자를 사용해 원소를 쓰는 연산
  • Map, MutableMap 인터페이스에는 이미 그 두 메서드가 정의되어 있음
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)
  • 파라미터가 Int 타입일 필요 없음
  • 파라미터가 여러 개일 수 있음
    • operator fun get(rowIndex: Int, colIndex: Int) -> matrix[row, col] 처럼 호출 가능
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
    2 -> 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)

7.3.2 in 관례

  • 멤버십 테스트(membership test): 객체가 컬렉션에 들어있는지 검사
  • contains 연산자 메서드: 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
}

```kotlin
val rect = Rectangle(Point(10, 20), Point (50, 50))
println(Point(20, 30) in rect)
// true
println(Point(5, 5) in rect)
// false
  • in의 우항: contains 메서드의 수신 객체
  • in의 좌항: conatins 메서드의 인자
  • a in c -> c.contains(a)

7.3.3 rangeTo 관례

  • .. 구문을 사용해 범위 생성
  • rangeTo 연산자 메서드: ..이 수행하는 연산
  • start..end -> start.rangeTo(end)
  • Comparable 인터페이스를 구현한 경우 -> rangeTo 정의 불필요
    • 코틀린 표준 라이브러리에 모든 Comparable 객체에 적용 가능한 rangeTo함수 정의됨
      operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
val now = LocalDate.now()
val vacation = now..now.plusDays(10)
println(now.plusWeeks(1) in vacation)
// true
  • now..now.plusDays(1) -> now.rangeTo(now.plusDays(10))
  • LocalDate가 Comparable을 구현하기 때문에 rangeTo가 확장 함수로 존재
  • 범위 연산자는 우선순위가 낮으니 혼동 방지를 위해 괄호 치면 좋다 (e.g. 0..(n+1), (0..n).forEach { print(it) }

7.3.4 for 루프를 위한 iterator 관례

  • in 연산자: for loop에 사용
  • for (x in list) { ... }: list.iterator()를 호출해 이터레이터를 얻고, hasNext 및 next 호출을 반복하는 식으로 변환
  • **iterator 연산자 메서드**: for loop에 사용되는 in에 사용됨
// String의 상위 클래스 CharSequence에 대한 iterator 확장 함수
operator fun CharSequence.iterator(): CharIterator
  • for (c in "abc") { .. } 처럼 사용 가능
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
  object : Iterator<LocalDate> { // LocalDate 원소에 대한 Iterator 구현
    var current = start
    override fun hasNext() = current <= endInclusive
    override fun next() = current.apply { current = plusDays(1) }
}
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) { println(dayOff) }
// 2016-12-31
// 2017-01-01
  • ClosedRange<LocalDate>에 대한 iterator 정의 -> LocalDate의 범위 객체를 for loop에 사용

7.4 구조 분해 선언과 component 함수

class Point(val x: Int, val y: Int) {
  operator fun component1() = x
  operator fun component2() = y
}
val p = Point(10, 20)
val (x, y) = p
println(x)
// 10
println(y)
// 20
  • 구조분해선언(destructing declaration): 여러 변수를 괄호로 묶어 동시에 선언
  • componentN 연산자 함수: 이 또한 관례 적용하여 이루어짐
  • val (a, b) = p -> val a = p.component1()+ val b = p.component2()
  • data class의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수 생성
data class NameComponents(val name: String, val extension, String)

fun splitFilename(fullName: String): NameComponents {
  val result = fullName.split('.', limit = 2)
  return NameComponent(result[0], result[1])
}
val (name, ext) = splitFilename("example.kt")
println(name)
// example
println(ext)
// kt
  • 함수에서 여러 값을 반환할 때 유용 (python에서는 흔히 볼 수 있는 형식인듯..)
data class NameComponents(val name: String, val extension, String)

fun splitFilename(fullName: String): NameComponents {
  val (name, extension) = fullName.split('.', limit = 2)
  return NameComponent(name, extension)
}
  • 컬렉션에 대한 구조 분해도 가능
  • 코틀린 표준 라이브러리에서는 맨 앞 다섯 원소에 대한 componentN 제공
  • 표준 라이브러리의 Pair, Triple 클래스를 사용하면 별도 클래스 선언 없이 더 간단히 반환 가능

7.4.1 구조 분해 선언과 루프

fun printEntries(map: Map<String, String>) {
  for ((key, value) in map) {  // 구조분해 선언 적용
    println("$key -> $value")
  }
}
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
// Oracle -> Java
// JetBrains -> Kotlin
  • 루프 안에서도 구조 분해 가능
  • in, 구조분해적용 -> Iterator<Map.Entry>, Map.Entry에 대한 확장 함수 component1, component2
  • 위 코드는 아래 처럼 표현 가능
    for (entry in map.entries) {
    val key = entry.component1()
    val value = entry.component2()
    }

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

  • 단순히 필드에 저장하는 것이 아니라 위임 객체(delegate)를 두고 그에 대한 접근을 위임

7.5.1 위임 프로퍼티 소개

class Foo {
  var p: Type by Delegate()
}
  • p 프로퍼티 접근자 로직을 다른 객체에 위임: Delegate 클래스의 인스터스를 위임 객체로 사용
  • by 뒤의 식을 계산해서 위임 객체 생성
class Foo {
  private val delegate = Delegate()
  var p: Type
  set(value: Type) = delegate.setValue(..., value)
  get() = delegate.getValue(...)
}
  • 컴파일러가 만드는 코트를 표현하자면 위와 같다: 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화
class Delegate {
  operator fun getValue(...) { ... }
  operator fun setValue(..., value: Type) { ... }
}
  • 위임 프로퍼티 관례 적용을 위해 Delegate 클래스는 getValue, setValue 메서드 혹은 확장 함수가 필요
val foo = Foo()
val oldValue = foo.p
foo.p = newValue
  • 내부 동작은 아래와 같다
    • foo.p -> delegate.getValue(...)
    • foo.p = newValue -> delegate.setValue(..., newValue)

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

  • 지연 초기화(lazy initialization): 실제로 값이 필요한 경우 초기화

그냥 구현하는 경우...

class Person(val name: String) {
  private var _emails: List<Email>? = null // backing property
  val emails: List<Email>
    get() {
      if (_emails == null) {
        _emails = loadEmails(this)
      }
      return _emails!!
    }
}
val p = Person("Alice")
p.emails // 최초로 emails를 읽을 때 단 한 번만 가져옮
p.emails
  • 직접 구현: 성가신 구현법, thread safe 하지 않음

아래는 위임 프로퍼티를 사용하는 경우: 오직 한 번만 초기화됨을 보장

class Person(val name: String) {
  val emails by lazy { loadEmails(this) }
}
  • lazy: 지연 로딩을 위한 표준 라이브러리 함수, thread safe
  • { loadEmails(this) }: lazy 함수 파라미터로 전달된 람다
  • lazy 함수에 사용할 락 전달 등도 가능

7.5.3 위임 프로퍼티 구현

  • 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지
  • 자바에서는 PropertyChangeSupport, PropertyChangeEvent 클래스르 사용해 처리
  • 코틀린에서 위임 프로퍼티 없이 구현 후 이후 위임 프로퍼티를 사용하도록 리팩토링 해보자
    class Person(val name: String, age: Int, salary: Int): PropertyChangeAware() {
    var age: Int = age
      set(newValue) {
        val oldValue = field // field: backing field 접근을 위한 식별자 키워드
        field = newValue
        changeSupport.firePropertyChange("age", oldValue, newValue)
      }
    var salary: Int = salary
      set(newValue) {
        val oldValue = field
        field = newValue
        changeSupport.firePropertyChange("salary", oldValue, newValue)
      }
    }
    val p = Person("Dmitry", 34, 2000)
    p.addPorpertyChangeListener(
    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
  • open class PropertyChangeAware { protected val changeSupport = PropertyChangeSupport(this) fun addPropertyChangeListener(listener: PropertyChangeListener) { changeSupport.addPropertyChangeListener(listener) } fun removePropertyChangeListener(listener: PropertyChangeListener) { changeSupport.removePropertyChangeListener(listener) } }
  • field keyword: backing field 접근을 위한 식별자 키워드 (4.2.4에서 다룬 내용)
  • 중복이 많은 코드
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)
  val salary: Int
    get() = _salary.getValue()
    set(value) { _salary.setValue(value) }
}
  • 코틀린의 위임이 실제로 동작하는 방식과 비슷
  • 여전히 발생하는 위임 준비 코드를 코틀린 위임으로 제거 가능
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(propName, 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)
}
  • getValue, setValueoperator 변경자 추가
  • getValue, setValue는 프로퍼티가 포함된 객체, 프로퍼티를 표현하는 객체(KProperty 타입)을 인자로 받음
  • KProperty.name을 통해 메서드가 처리할 프로퍼티 이름을 알 수 있음: KProperty에 대해서는 10.2에서 자세히 다룸
  • 위와 비슷한 기능을 하는 표준 라이브러리 클래스: 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, obserever)
  var salary: Int by Delegates.observable(salary, observer)
}

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

class C {
  var prop: Type by MyDelegate()
}
val c = C()
  • MyDelegate 클래스 인스턴스 -> 감춰진 프로퍼티 <delegate>에 저장
  • 컴파일러는 프로퍼티를 표현하기 위해 KProperty 타입의 객체 사용, <property>라고 부름
    class C {
    private val <delegate> = MyDelegate()
    var prop: Type
      get() = <delegate>.getValue(this, <property>)
      set(value: Type) = <delegaet>.setValue(this, <property>, value)
    }
  • 컴파일러에 의해 생성된 코드
  • val x = c.prop -> val x = <delegate>.getValue(c, <property>)
  • c.prop = x -> `.setValue(c, , x)

<< 이후로는 활용 사례들 >>

7.5.5 프로퍼티 값을 맵에 저장

class Person {
  // 추가 정보
  private val _attributes = hashMapOf<String, String>()
  fun setAttribute(attrName: String, value: String) {
    _attributes[attrName] = value
  }
  // 필수 정보
  val name: String
  get() = _attributes["name"]!! // 수동으로 맵에서 정보를 꺼냄
}
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data)
  p.setAttribute(attrName, value)
println(p.name)
// Dmitry
  • 확장 가능한 객체(expando object): 자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티 활용 가능
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 확장 함수를 제공하기 때문에 가능

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

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 // 사용자 이름은 "name" 컬럼에 존재
  var age: Int by Users.age
}
  • Users: 테이블 표현, 싱글컨 객체로 선언
  • User의 프로퍼티에 접근할 때 자동으로 엔티티 클래스에 정의된 데이터베이스 매핑으로부터 필요한 값 가져옴
  • User 객체 변경 -> dirty 상태로 변하고, 프레임워크가 적절히 반영...
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
  // DB에서 컬럼 값 가져오기
}

operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) [
  // DB의 값 변경하기
}
  • Column 객체를 위임 객체로 사용함
  • user.age += 1 -> user.ageDelegate.setValue(user.ageDelegate.getValue() + 1 비슷한 코드로 변경
  • 자세한 내용은 Exposed 프레임워크 소스코드 참고 (11장에서 프레임워크에 사용된 DSL 설계 기법을 알아봄)

7.6 요약

  • 여러 함수 정의해서 연산자 오버로드 및 관례 기능 사용
    • 산술, 비교 연산자 등
    • [], in, ..
    • 구조분해 선언(componentN)
  • 위임 프로퍼티를 통해 프로퍼티 값 초기화/수정/읽기 시 사용되는 로직 재활용 가능 / 프레임워크 제작에도 유용
반응형

댓글