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

보람·2022년 5월 12일
1

Kotlin-In-Action

목록 보기
8/12

관례(convention)

  • 여러 연산을 지원하기 위해 특별한 이름이 붙은 메서드
  • 자바
    • 언어 기능을 타입에 의존
  • 코틀린
    • 언어 기능을 관례에 의존
    • 확장 함수를 사용 -> 자바 코드를 바꾸지 않고도 새로운 기능 부여 가능

산술 연산자 오버로딩

  • 코틀린 관례의 가장 단순한 예
  • 자바
    • 원시 타입(int, double, float 등등) 만 산술 연산자 가능
    • String 에서만 +가능

그러나 그 외의 클래스에 대해서도 산술 연산이 가능한 편이 좋은데 코틀린에서는 가능!

이항 산술 연산 오버로딩

  • 이항 연산자 : 연산자 함수 이름
    • a*b : times
    • a/b : div
    • a%b : mod (1.1부터 rem)
    • a+b : plus
    • a-b : minus
/**
 * x, y 한 좌표가 정의 된 Point data class
 */
data class Point(val x: Int, val y: Int) {

    /**
     * "plus (+와 대응)라는 이름의 연산자 함수를 정의
     * operator(연산자) 함수라는 것을 표시
     */
    operator fun plus(other: Point): Point { 
    //반환타입이 Point고 파라미터도 Point로 현재는 타입이 동일하지만
    //반환타입과 파라미터타입이 불일치 해도 상관 없다
        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) // + 로 계산하면 "plus" 함수가 호출 // Point(x=40, y=60)
}

복합 대입 연산자 오버로딩

  • +=, -=
  • 참조를 다른 참조로 바꿔치기
var point = Point(1,2)
point += Point(3,4) // point = point + Point(3,4) 
//-> 변수가 변경 가능한 경우에만(var) += 가능
println(point) //>> Point(x=4, y=6)
  • 변경 가능한 컬렉션에 원소 추가 (컬렉션은 val로 선언해도 값을 add할 수 있음)
val numbers = ArrayList<Int>()
numbers += 42 //numbers.add(42)
println(numbers[0]) //get
point += Point(3,4) // point = point + Point(3,4) 
//-> 변수가 변경 가능한 경우에만(var) += 가능
println(point) //>> Point(x=4, y=6)

위 예제는 Unit타입인 plusAssign 연산자 함수 사용

operator fun <T> MutableCollection<T>.plusAssign(element:T) {
	this.add(element)
}
  • 자매품 : minusAssign(-=), timesAssign(*=)
  • plus, plusAssign 연산을 동시에 한 클래스에 정의하지 말것 (why? 이론적으로 +=은 plus, plusAssign 양쪽으로 컴파일 가능하여 컴파일러는 오류를 보고함)

단항 연산자 오버로딩

  • +a : unaryPlus
  • -a : unaryMinus
    • Int num=1, println(-num) -> -1 출력
  • !a : not
  • ++a, a++ : inc
  • --a, a-- : dec
  • 이항 연산자와 마찬가지로 미리 정해진 이름의 함수를 operator로 선언하면 된다.
operator fun Point.unaryMinus(): Point { //단항 minus(음수) 함수는 파라미터가 없다.
//본인안에 있는 값에서 -만 하면 되니까 파라미터가 없쥬
    return Point(-x, -y) //좌표에서 각 성분의 음수를 취한 새 점을 반환
}

val p = Point(10, 20)
println(-p) //>> Point(-10, -20)
  • ++a | a++ 이 헷갈린다면?
var num :Int = 0
println(num++) //>> 0 //출력 후 계산
println(++num) //>> 2 //선출력 후 계산 

비교 연산자 오버로딩

동등 연산자 : equals

class Point(val x: Int, val y: Int) {
    override fun equals(obj: Any?): Boolean { //Any에 정의된 메서드를 오버라이딩 한다.
        //equals는 Any에 이미 operator로 정의되어져 있어서 오버라이드 시에는 명시하지 않아도 된다. 
        if (obj === this) return true //파라미터가 수신객체와 같은지를 비교할때 ===을 사용
        if (obj !is Point) return false //파라미터 타입 검사
        return obj.x == x && obj.y == y //Point로 스마트 캐스트하여(위 if에서 is 사용) x,y 프로퍼티 접근
    }
}
  • equals를 operator로 선언하지 않는 이유
    • 상위 클래스인 Any에서 이미 operator로 선언됨
      • 연산자 함수를 정의하기 위해서는 operator 를 써야 하지만 이미 작성된 클래스 상속시 상위클래스에 해당 연산자 함수가 이미 정의돼있다면 operator 명시 필요 X

순서 연산자 : compareTo

  • a >= ba.compareTo(b) >= 0과 동일
  • a < ba.compareTo(b) < 0과 동일

컬레션과 범위에 대한 관례

  • 컬렉션에서 자주 쓰이는 연산 :
    • return a[0] : get
    • a[0] = 1 : set
    • in : contains와 대응 : a 1 until 10 : (1<=a && a<=9)
      • 컬렉션의 in은 찾고자 하는 값이 있니? 에 대한 결과 도출
    • a 1 .. 10(1<=a && a<=10) : rangeTo
  • for 루프를 위한 iterator 관례
    • 위에 있는 in과 달리 for의 in은 hasNext, next 호출을 반복
      • for의 in은 끝날때까지 반복

구조 분해 선언

val map = mapOf("Oracle" to "Java", "Jetbrains" to "Kotlin") //key to value 형태 
for ((key, value) in map) { //루프 변수에 구조 분해 선언 사용
	println("$key -> $value") //Oracle -> Java ...
}
  • 함수에서 여러 값 반환시 유용

위임 프로퍼티

  • 프로퍼티 접근자 로직 재활용
    • 그렇다. 반복되는 코드를 미리 만들어서 by로 위임 후 재활용할 수 있다.
  • 위임 : 객체 본인이 작업을 수행하지 않고 다른 도우미 객체에게 맡기는 디자인 패턴
  • 위임 프로퍼티를 통해 프로퍼티 값을 저장하거나 초기화하거나 읽거나 변경할때 사용하는 로직을 재활용할 수 있다.
  • 위임 프로퍼티는 프레임워크를 만들 때 아주 유용

(어렵다..한국말이 참 어렵다..이해해보자..)
프로퍼티 본인이 본인이 해야 하는 작업을 직접 수행하는 것이 아닌 by해서 위임한 객체에게 일을 맡기는 고런 것?0?
위임 객체를 위임한 프로퍼티가 위임 프로퍼티..
뭔가 프로퍼티들의 중복된 행위들을 직접 작성하지 않고 이미 구현된 도우미 클래스에게 이것좀 해줘! 라고 맡기는 느낌
중복코드를 방지할 수 있을 듯 하다.

lazy : 지연 초기화 프로퍼티 구현

class Email { /*...*/ }
fun loadEmails(person: Person): List<Email> {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}
/**
 * lazy 를 사용하지 않는다면 아래와 같이 긴 코드를 작성해야 한다.
 * private 변경가능 변수와 변경 불가능 변수를 이용해서 null일때 초기화하는 코드를 작성해야함 
 * -> 매우 긴거 보이시쥬?
 */
/*class Person(val name: String) {
    private var _emails: List<ch07.LazyEmails.Email>? = null
    //_emails 보이시쥬?

    val emails: List<ch07.LazyEmails.Email>
        get() {
            if (_emails == null) { //getter에서 해당 값이 없는 경우에만 loadEmails 함수 호출
                _emails = ch07.LazyEmails.loadEmails(this)
            }
            return _emails!!
        }
}*/
class Person(val name: String) {
    val emails by lazy { loadEmails(this) } //굉장히 짧은 코드로 초기화 가능🌝
}

fun main(args: Array<String>) {
    val p = Person("Alice")
    //아래 코드 실행시에는 emails가 초기화 전 상태이기 때문에 lazy 함수를 탄다.
    p.emails //Load emails for Alice
    p.emails 
}

(혼자이해해보기..) 변수 초기화시에 private var(getter&setter) & 실제 변수 val 두개를 사용해서 값이 초기화 됐는지 여부 판단후 초기화되지 않았다면 값을 초기화해야하는데 ..
그걸 직접 작성하지 않고 by lazy를 하면 초기화 되지 않았을 때 lazy 내부 람다식을 실행 가능하다.
두개의 변수를 사용하지 않아도 되고 코드 또한 한줄로 줄어든다..
아주 좋군..

Delegates.observable : 프로퍼티 변경 관찰

Delegates 관련 내용이 책에서 넘넘 어려운데 마지막 예제를 보면 좀 더 이해하기 쉬운 것 같다!
너무너무 어려워서 예제에 다 설명을 달아뒀다.

/**
 * 프로퍼티 변경 리스터를 추적하는 도우미 클래스 PropertyChangeAware
 */
open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)
    //PropertyChangeSupport 는 프로퍼티 변경 지원 클래스

    //프로퍼티 변경 리스너 PropertyChangeSupport클래스의 함수를 오버라이드 하는 형식
    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

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

/**
 * 위에서 추가한 PropertyChangeAware(프로퍼티 변경 추적 도우미 클래스)를 상속받는 Person 클래스
 * Person 클래스에 존재하는 프로퍼티(name, age, salary) 변경 여부를 판단 가능하다
 */
class Person(
    val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    private val observer = { //observer 는 어떤 값의 변경을 관찰하는 관찰자
        //값 변경시마다 해당 람다가 실행됨
        prop: KProperty<*>, oldValue: Int, newValue: Int ->
        println("observer exist")
        //firePropertyChange 함수에서 new PropertyChangeEvent(this.source, propertyName, oldValue, newValue)
        //하면서 변경된 값에 따른 새로운 객체를 생성해준다.
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
        //changeSupport 는 상속받은 PropertyChangeAware 클래스에 존재하는 값임
    }

    /**
     * by 보이시쥬?
     * 옵저버 구현 객체를 위임하는 것이죵
     * 그러면 age 와 salary 는 값 변경시에 대한 행위를 직접 하지 않고 위임한 놈에게 해당 행위를 맡기는 것이쥬!
     */
    //by 로 위임객체를 지정해주지 않으면 아래와 같이 길고 긴 코드를 작성해야 합니다!
    //한개라면 괜찮겠지만 두개 이상의 프로퍼티를 사용하는 클래스라면 중복이 굉장하겠쥬?
    /*var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                "salary", 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 = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        //PropertyChangeListener 는 속성이 변경될때마다 읽어드리는 리스너임
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35 //값 변경
//    observer exist
//    Property age changed from 34 to 35
    p.salary = 2100 //값 변경
//    observer exist
//    Property salary changed from 2000 to 2100
    p.salary = 21002 //값 변경 
//    observer exist
//    Property salary changed from 2100 to 21002

}

책에서는 by옆에 있는 우항에 있는 식(Delegates.observable(age,observer))을 계산한 결과인 객체는 컴파일러가 호출할 수 있는 올바른 타입의 getValue와 setValue를 반드시 제공해야 한다 라고 명시돼있다.
머선 말이냐 하묜..(아래이미지 참고)

  • IDE를 이용해 Delegates 를 타고 들어가보면.. 해당 오브젝트에서 필요한 두 함수를 오버라이드해뒀다.

이미 힘든데 마지막 하나가 더 남았다..🥺

맵 위임 프로퍼티

  • 자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티를 활용하는 경우가 있는데 그런 객체를 확장 가능한 객체라고 부르기로 했어요.
  • 다양한 속성을 제공하는 객체를 유연하게 사용 가능
class Person {
    private val _attributes = hashMapOf<String, String>()
    //Map, MutableMap 자체에서 setValue와 getValue 확장 함수를 제공하기 때문에 by가 먹히는 것
    //(by를 하기 위한 위임 객체에서는 위 두 메서드가 정의돼있어야 함)

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

    //위임 프로퍼티로 맵을 사용한다. 
    val name: String by _attributes //hashMapOf 위임 객체
}

fun main(args: Array<String>) {
    val p = Person() //hashMapOf 위임객체인 _attributes 를 사용하는 프로퍼티 2개를 갖고있는 Person 클래스
    val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
    for ((attrName, value) in data)
       p.setAttribute(attrName, value) 
    println(p.name) //Dmitry
    //p.name을 호출하면 _attributes.getValue(p, prop)을 대신 호출하고 , 
    // _attributes.getValue(p, prop) 가 _attributes[prop.name] 을 통해 구현이 된다.
    // 맵에 값을 넣고 꺼내오는 과정을 name 프로퍼티가 직접하지 않고 이를 위임객체인 _attributes 에게 맡긴다.
}

val name: String by _attributes

무수히 많은 키가 존재하는 맵 데이터가 있고
어떤 키에 해당하는 값을 간편하게 갖고 오고자 할때 맵 위임 프로퍼티를 사용하면 p.name 과 같은 식으로 호출할 때 해당 맵에 있는 name 키에 해당 하는 값을 간편하게 얻어올 수 있다!

(뭔가 코드 자체가 되게 길어져서 이게 진짜 유용한가 라는 생각이 들긴하지만 해당 클래스에 대한 인스턴스의 어떤 키가 자주 사용된다면 유용하겠지? 라고 꾸역꾸역 이해해본다.😭)

profile
백엔드 개발자

0개의 댓글