ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코틀린 - 연산자 오버로딩과 기타 관례
    Kotiln 2019. 7. 23. 10:54

    어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 관례 라고 부른다

    이항 산술 연산 오버로딩

    연산자를 오버로딩하는 함수 앞에는 꼭 operator 키워드를 붙여야 한다.
    operator 키워드를 붙임으로써 어떤 함수가 관례를 따르는 함수임을 명확히 할 수 있다.
    코틀린에서는 직접 연산자를 만들어 사용할 수 없고 언어에서 미리 정해둔 연산자만 오버로딩할 수 있으며 관례에 따르기 위해 클래스에서 정의해야 하는 이름이 연산자별로 정해져 있다.

    함수 이름
    a * b times
    a / b div
    a % b mod (1.1 부터 rem)
    a + b plus
    a - b minus

    코틀린은 표준 숫자 타입에 대해 비트 연산자를 정의하지 않는다. 따라서 커스텀 타입에서 비트 연산자를 정의할 수도 없다. 대신 중위 연산자 표기법을 지원하는 일반 함수를 사용해 비트 연산을 수행한다. 커스텀 타입에서도 그와 비슷한 함수를 정의해 사용할 수 있다.
    * shl - 왼쪽 시프트
    * shr - 오른쪽 시프트 (부호 비트 유지)
    * ushr - 오른쪽 시프트 (0으로 부호 비트 설정)
    * and - 비트 곱
    * or - 비트 합
    * xor - 비트 베타 합
    * inv - 비트 반전

    복합 대입 연산자 오버로딩

    plus 와 같은 연산자를 오버로딩 하면 그와 관련 있는 연산자인 += 도 자동으로 지원한다.
    반환 타입이 UnitplusAssign 함수를 정의하면 코틀린은 += 연산자에 그 함수를 사용한다.

    operator fun <T> MutableCollection<T>.plusAssign(element: T){
        this.add(element)
    }

    단항 연산자 오버로딩

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

    equals 동등성 연산자

    a == b 라는 비교를 처리할 때 코틀린은 a 가 null 인지 판단해서 null이 아닐 경우에만 a.equals(b) 를 호출한다.

    compareTo 순서 연산자

    비교연산자(<, >, <=, >=) 는 compareTo 호출로 컴파일 된다.

    getset 인덱스로 원소에 접근

    인덱스 연산자 ([ ])를 사용해 원소를 읽는 연산은 get 연산자 메소드로 변환되고 원소를 쓰는 연산은 set 연산자 메소드로 변환된다.

    in 관례

    in 은 객체가 컬렉션에 들어있는지 검사(멤버십 검사) 한다. 그런 경우 in 연산자와 대응하는 함수는 contains 다.

    rangeTo 관례

    .. 연산자는 rangeTo 함수를 간략하게 표현하는 방법이다.
    코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.

    for 루프를 위한 iterator 관례

    for 루프와 같이 in 연산자를 이용하지만 여기서는 관례이기 때문에 iterator 메소드를 확장 함 수로 정의할 수 있다.
    코틀린 표준 라이브러리는 String의 상위 클래스인 CharSequence 에 대한 iterator 확장 함수를 제공한다.

    구조 분해 선언과 component 함수

    구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.

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

    내부에서 구조 분해 선언은 다시 관례를 사용한다. 구조 분해 선언의 각 변수를 초기화하기 위해 componentN이라는 함수를 호출한다. (N은 구조 분해 선언에 있는 변수 위치에 따라 붙는 번호다.)
    위의 코드 val (a, b) = pval a = p.component1() val b = p.component2() 로 컴파일 된다.

    data 클래스의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러 자동으로 componentN 함수를 만들어준다.

    class Point(val x: Int, val y: Int){
        operator fun component1() = x
        operator fun compenent2() = y
    }

    구조 분해 선언은 함수에서 여러 값을 반환할 때 유용하다.

    구조 분해 선언과 루프

    함수 본문 내의 선언문뿐 아니라 변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용할 수 있다.

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

    위의 코드는 객체를 이터레이션 하는 관례와 구조 분해 선언을 활용한다.

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

    위임 프로퍼티를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로터피를 쉽게 구현할 수 있다.

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

    위임 프로퍼티의 일반적인 문법이다. p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다. 여기서 Delegate 클래스의 인스턴스를 위임 객체로 사용한다.

    컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화한다.

    class Foo { 
        private val delegate = Delegate()
        var p: Type
        set(value: Type) = delegate.setValue(..., value)
        get() = delegate.getValue(...)
    }

    프로퍼티 위임 관례를 따르는 Delegate 클래스는 getValuesetValue 메소드를 제공해야 한다.

    Delegate 클래스를 단순화하면 다음과 같다.

    class Delegate{
        operator fun getValue(...){...}
        operator fun setValue(..., value: Type){...}
    }
    class Foo{
        var p: Type by Delegate()
    }
    
    > val foo = Foo()
    > val oldValue = foo.p
    > foo.p = newValue

    위임 프로퍼티 사용 :

    by laze()

    를 사용한 프로퍼티 초기화 지연

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

    class Email(val name:String){
        private var _emails: List<Email>> = null
        val emails: List<Email>
            get() {
                if(_emails == null){
                    _emails = loadEmails(this)
                }
                return _emails!!
            }
    }
    
    fun loadEmails(person: Person): List<Email>{
        println("${person.name}의 이메일을 가져옴")
        return listOf(/*...*/)
    }

    loadEmails 함수는 데이터베이스에서 이메일을 가져온다.
    Email 클래스는 이메일을 불러오기 전에는 null을 저장하고 불러온 다음에는 이메일 리스트를 저장하는 _emails 프로퍼티를 추가해서 지연 초기화를 구현한 클래스이다.

    뒷받침하는 프로퍼티backing property 기법을 사용했다. _emails 프로퍼티는 값을 저장하고 다른 프로퍼티인 emails_emails 라는 프로퍼티에 대한 읽기 연산을 제공한다. _emails는 null이 될 수 있는 타입인 반면 emails은 null 이 될 수 없는 타입이므로 프로퍼티를 두 개 사용해야 한다.

    위임 프로퍼티를 사용하면 더 간단해진다. 위임 프로퍼티는 데이터를 저장할 대 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다.
    위임 객체를 반환하는 표준 라이브러리 함수가 lazy 이다.

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

    lazy 함수는 getValue 메소드가 들어있는 객체를 반환하기 때문에 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.

    위임 프로퍼티 구현

    /* PropertyChangeSupport를 사용하기 위한 도우미 클래스 */
    open class PropertyChangeAware {
        protected val changeSupport = PropertyChangeSupport(this)
        fun addPropertyChangeListener(listener: PropertyChangeListener) {
            changeSupport.addPropertyChangeListener(listener)
        }
    
        fun removePropertyChangeListener(listener: PropertyChangeListener) {
            changeSupport.removePropertyChangeListener(listener)
        }
    }
    
    /* 
        프로퍼티 위임에 사용할 수 있게 작성 
        KProperty 인자를 통해 프로퍼티 이름을 전달 받는다.
    */
    
    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)
    }
    
    fun main(args: Array<String>){
        val p = Person("Dmitry", 35, 2000)
        p.addPropertyChangeListener(
            PropertyChangeListener { event ->
                println("Property ${event.propertyName} changed from ${event.oldValue} to ${event.newValue}")
            }
        )
    
        p.age = 34
        p.salary = 10000
    }
    
    > Property age changed from 35 to 34
    > Property salary changed from 2000 to 10000

    by 키워드 오른쪽에 오는 객체를 위임 객체delegate 라고 부르며 코틀린은 위임 객체를 감춰진 프로퍼티에 저장하고 주 객체의 프로퍼티를 읽거나 쓸 때마다 위임 객체getValuesetValue 를 호출해준다.

    코틀린 표준 라이브러리에 있는 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)
    }

    프로퍼티 값을 맵에 저장

    자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티를 활용하는 경우가 있다. 그런 객체를 확장 가능한 객체expando object 라고 한다.

    class Person{
        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 = Person()
        val data = mapOf("name" to "Dmitry", "company" to "JeTBrains")
        for((attrName, value) in data)
            p.setAttribute(attrName, value)
        println(p.name)
    }
    
    > Dmitry

    참고 도서 : Kotlin in Action(2017)

    반응형

    댓글

Designed by Tistory.