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

sua·2021년 8월 8일
0

Kotlin In Action

목록 보기
7/9
post-thumbnail

관례 : 코틀린에서 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법

7.1 산술 연산자 오버로딩

7.1.1 이항 산술 연산 오버로딩

private data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point { // plus 연산자 함수 정의
        return Point(x + other.x, y + other.y) // 좌표를 성분별로 더한 새로운 점을 반환
    }
}

fun main(args: Array<String>) {
    val p1 = Point(10, 20)
    val p2 = Point(30, 40)
    println(p1 + p2) // Point(x=40, y=60)
    // p1 + p2 => p1.plus(p2)로 컴파일
}

연산자를 오버로딩하는 함수 앞에 operator 키워드를 붙여야 함

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

operator fun Point2.plus(other: Point2) : Point2 { // 연산자를 확장 함수로 정의
    return Point2(x + other.x, y + other.y)
}

fun main(args: Array<String>) {
    val p1 = Point2(10, 20)
    val p2 = Point2(30, 40)
    println(p1 + p2) // Point2(x=40, y=60)
}

[오버로딩 가능한 이항 산술 연산자]

함수 이름
a * btimes
a / bdiv
a % bmod(1.1부터 rem)
a + bplus
a - bminus
  • 직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 표준과 같음
operator fun Point.times(scale: Double) : Point { // 두 피연산자의 타입이 다른 연산자 정의
    return Point((x * scale).toInt(), (y * scale).toInt())
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p * 1.5) // Point(x=15, y=30)
}
  • 자동으로 교환 법칙을 지원하지는 않음 => 변수 타입 순서 바꾸면 x
operator fun Char.times(count: Int) : String { // 결과 타입이 피연산자 타입과 다른 연산자 정의
    return toString().repeat(count)
}

fun main(args: Array<String>) {
    println('a' * 3) // aaa
}



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

복합 대입 연산자 : +=, -= 등의 연산자
변경 가능한 클래스를 설계하는 경우 복합 대입 연산 제공

fun main(args: Array<String>) {
    var point = Point(1, 2)
    point += Point(3, 4)
    println(point) // Point(x=4, y=6)
}

[복합 대입 연산자 함수 종류]

  • plusAssign : +=
  • minusAssign : -=
  • timesAssign : *=
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

fun main(args: Array<String>) {
    val numbers = ArrayList<Int>()
    numbers += 42
    println(numbers[0]) // 42
}

[컬렉션에서 연산자 접근 방법]

fun main(args: Array<String>) {
    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 { // 단항 minus 함수는 파라미터가 x
    return Point(-x, -y) // 좌표에서 각 성분의 음수를 취한 새 점을 반환
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(-p) // Point(x=-10, y=-20)
}

[오버로딩할 수 있는 단항 산술 연산자]

함수 이름
+aunaryPlus
-aunaryMinus
!anot
++a, a++inc
--a, a--dec
import java.math.BigDecimal

operator fun BigDecimal.inc () = this + BigDecimal.ONE // 증가 연산자 정의

fun main(args: Array<String>) {
    var bd = BigDecimal.ZERO
    println(bd++) // 0, 후위 증가 연산자는 println이 실행된 다음에 값을 증가시킴
    println(++bd) // 2, 전위 증가 연산자는 println이 실행되기 전에 값을 증가시킴
}




7.2 비교 연산자 오버로딩

7.2.1 동등성 연산자: equals

class Point3(val x: Int, val y: Int) {
    override fun equals(obj: Any?) : Boolean { // Any에 정의된 메소드를 오버라이딩
        if (obj === this) return true // 파라미터가 "this"와 같은 객체인지 살펴봄
        if (obj !is Point3) return false // 파라미터 타입을 검사
        return obj.x == x && obj.y == y // Point로 스마트 캐스트해서 x와 y 프로퍼티에 접근
    }
}

fun main(args: Array<String>) {
    println(Point3(10, 20) == Point3(10, 20)) // true
    println(Point3(10, 20) != Point3(5, 5)) // true
    println(null == Point3(1, 2)) // false
}
  • === 를 오버로딩할 수는 없음
  • eqauls를 확장 함수로 정의할 수 x => 상속 받은 equals가 확장 함수보다 우선순위가 높기 때문
  • != 처리를 컴파일러가 자동으로 equals 반환 값을 반전시켜서 해줌



7.2.2 순서 연산자: compareTo

p1 < p2 => p1.compareTo(p2) < 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) // 인자로 받은 함수를 차례로 호출하면서 값을 비교
        }
    }

fun main(args: Array<String>) {
    val p1 = Person("Alice", "Smith")
    val p2 = Person("Bob", "Johnson")
    println(p1 < p2) // false
}
  • Comparable 인터페이스를 구현하는 모든 자바 클래스를 코틀린에서는 간결한 연산자 구문으로 비교 가능 => 확장 메소드 만들 필요 x
println("abc" < "bac") // true




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

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

인덱스 연산자 []
get 연산자 메소드 : 원소를 읽는 연산
set 연산자 메소드 : 원소를 쓰는 연산

operator fun Point.get(index: Int) : Int { // "get" 연산자 함수를 정의
    return when(index) { // 주어진 인덱스에 해당하는 좌표를 찾음
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array<String>) {
    val p = Point(10, 20)
    println(p[1]) // 20
}

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

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

operator fun MutablePoint.set(index: Int, value: Int) { // "set"이라는 연산자 함수를 정의
    when(index) { // 주어진 인덱스에 해당하는 좌표를 변경
        0 -> x = value
        1 -> y = value
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array<String>) {
    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 관례

in 연산자와 대응하는 함수 -> contains

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 && // 범위를 만들고 "x"좌표가 그 범위 안에 있는지 검사
            p.y in upperLeft.y until lowerRight.y // "until" 함수를 사용해 열린 범위를 만듦
}

fun main(args: Array<String>) {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    // in의 우항에 있는 객체는 contains 메소드의 수신 객체가 됨
    // in의 좌항에 있는 객체는 contains 메소드에 인자로 전달됨
    println(Point(20, 30) in rect) // true
    println(Point( 5, 5) in rect) // false
}

a in c => c.contains(a)



7.3.3 rangeTo 관례

.. 구문 : 범위를 만들 때 사용
start..end => start.rangeTo(end)

  • 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있음
operator fun <T: Comparable<T>> T.rangeTo(that: T) : ClosedRange<T>
fun main(args: Array<String>) {
    val now = LocalDate.now()
    val vacation = now..now.plusDays(10) // now(오늘)부터 시작해 10일짜리 범위를 만듦
    // => 컴파일러에 의해 now.rangeTo(now.plusDays(10))으로 변환됨 [Comparable에 대한 확장 함수]
    println(now.plusWeeks(1) in vacation) // true

    val n = 9
    println(0..(n + 1)) // 0..10 , 괄호를 쳐서 뜻이 명확하게 해줌
    (0..n).forEach { print(it) } // 012345679, 범위의 ㅔ소드를 호출하려면 범위를 괄호로 둘러싸야 함
}



7.3.4 for 루프를 위한 iterator 관례

//operator fun CharSequence.iterator() : CharIterator
// => 이 라이브러리 함수는 문자열을 이터레이션할 수 있게 해줌

fun main(args: Array<String>) {
    for (c in "abc") {
        println(c)
    }
}
operator fun ClosedRange<LocalDate>.iterator() : Iterator<LocalDate> =
    object : Iterator<LocalDate> { // 이 객체는 LocalDate 원소에 대한 Iterator를 구현
        var current = start

        override fun hasNext() = current <= endInclusive // compareTo 관례를 사용해 날짜 비교
        override fun next() = current.apply { // 현재 날짜를 저장한 다음에 날짜를 변경하고 저장해둔 날짜를 반환
            current = plusDays(1) // 현재 날짜를 1일 뒤로 변경
        }
    }

fun main(args: Array<String>)  {
    val newYear = LocalDate.ofYearDay(2017, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) { println(dayOff) } // daysOff에 대응하는 iterator함수가 있으면 daysOff에 대해 이터레이션함
    // 2016-12-31
    // 2017-01-01
}




7.4 구조 분해 선언과 component 함수

val (a, b) = p => val a = p.component1(), val b = p.component2()

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

fun splitFilename2(fullName: String) : NameComponents2 {
    val (name, extension) = fullName.split('.', limit = 2)
    return NameComponents2(name, extension)
}

fun main(args: Array<String>) {
    val (name, ext) = splitFilename2("example.kt") // 구조 분해 선언 구문을 사용해 데이터 클래스를 풂
    println(name) // example
    println(ext) // kt
}

7.4.1 구조 분해 선언과 루프

변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언 사용 가능

fun printEntries(map: Map<String, String>) {
    for ((key, value) in map) { // 루프 변수에 구조 분해 선언을 사용
        println("$key -> $value")
    }
}

fun main(args: Array<String>) {
    val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
    printEntries(map)
    // Oracle -> Java
    // JetBrains -> Kotlin
}




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

7.5.1 위임 프로퍼티 소개

class Delegate {
	operator fun getValue(...) { ... }
    operator fun setValue(..., value: Type) {...}
}

class Foo {
	var p : Type by Delegate() // "by" 키워드는 프로퍼티와 위임 객체를 연결
}

fun main(args: Array<String>) {
	val foo = Foo()
    	val oldValue = foo.p // foo.p라는 프로퍼티 호출은 내부에서 delegate.getVAlue()을 호출
    	foo.p = newValue // 프로퍼티 값을 변경하는 문장은 내부에서 delegate.setValue()을 호출
}



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

지연 초기화 : 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴

class Email(val text: String)

class Person2(val name: String) {
    private var _emails: List<Email>? = null // 데이터를 저장하고 emails의 위임 객체 역할을 하는 _emails 프로퍼티
    val emails: List<Email>
        get() {
            if (_emails == null) {
                _emails = loadEmails(this) // 최초 접근 시 이메일을 가져온다.
            }
            return _emails!! // 저장해 둔 데이터가 있으면 그 데이터를 반환
        }
}

class Person3(val name: String) { // 지연 초기화를 위임 프로퍼티를 통해 구현하기
    val emails by lazy { loadEmails2(this) }
}

fun loadEmails(person: Person2) : List<Email> {
    println("${person.name}의 이메일을 가져옴")
    return listOf(Email(person.name))
}

fun loadEmails2(person: Person3) : List<Email> {
    println("${person.name}의 이메일을 가져옴")
    return listOf(Email(person.name))
}

fun main(args: Array<String>) {
    val p = Person2("Alice")
    println(p.emails)
    println(p.emails)

    val p2 = Person3("Bob")
    println(p2.emails)
    println(p2.emails)
}



7.5.3 위임 프로퍼티 구현

import java.beans.PropertyChangeListener
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

class Person6 (
    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(args: Array<String>) {
    val p = Person6("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
}



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

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



7.5.5 프로퍼티 값을 맵에 저장

확장 가능한 객체 : 자신의 프로퍼티를 동적으로 정의할 수 있는 객체 -> 이를 만들 때 위임 프로퍼티를 활용

// 값을 맵에 저장하는 위임 프로퍼티 사용
class Person7 {
    private val _attributes = hashMapOf<String, String>()

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

    val name: String by _attributes // 위임 프로퍼티로 맵을 사용
}

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



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

구현 소스코드

profile
가보자고

0개의 댓글

관련 채용 정보