ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [코틀린 Kotlin] 연산자 오버로딩과 기타 관례
    카테고리 없음 2021. 10. 10. 20:03
    728x90

     

    관례(Convention)

    예를 들어 어떤 클래스 안에 plus 라는 이름의 특별한 메소드를 정의하면 + 연산자를 사용할 수 있다.

    ex) person + person

     

    기존 자바 클래스를 코틀린 언어에 적용하기 위함이다.

    기존 자바 클래스가 구현하는 인터페이스가 이미 고정되어 있고, 코틀린 쪽에서 자바 클래스가 새로운 인터페이스를 구현하게 만들 수는 없다. 

    반면 확장 함수를 사용하면, 기존 자바 클래스에 대해 관례에 따라 이름을 붙임으로써 기존 자바 코드를 수정하지 않고 새로운 기능을 쉽게 구현할 수 있다.

     

     

    1. 산술 연산자 오버로딩

    코틀린에서 관례(Convention) 를 사용하는 가장 단순한 예는 산술 연산자이다.

    자바에서는 원시 타입에 대해서만 산술 연산자를 사용할 수 있고, 추가로 String 에 대해서 + 연산자를 사용할 수 있다.

     

    1) 이항 산술 연산 오버로딩

    연산자를 오버로딩하는 함수 앞에는 operator 키워드가 있어야 한다.

    // case 1. 연산자를 자체 함수로 정의하기
    data class Point(val x: Int, val y: Int) {
        operator fun plus(other: 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)	
        // 결과 : Point(x=40, y=40)
        // p1.plus(b2) 로 컴파일 된다.
    }
    
    // case 2. 연산자를 확장 함수로 정의하기
    data class Point(val x: Int, val y: Int)
    
    operator fun Point.plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
    Expression Function name
    a * b times
    a / b div
    a % b mod
    a + b plus
    a - b minus

     

    연산자를 정의할 때 두 피연산자가(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없다. 또는 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다.

     

    2) 복합 대입 연산자 오버로딩

    +=, -= 와 같은 복합 대입 (Compound assignemt) 연산자도 자동으로 지원한다.

    >>> var point = Point(1, 2)
    >>> point += Point(3, 4)
    >>> println(point)
    Point(x=4, y=6)

    단 plus와 plusAssign 두개를 동시 구현하면 컴파일 오류가 발생한다. (+ 에 대한 동작을 어떤걸 해야할지 모르기 때문이죠.)

    코틀린은 컬렉션에도 해당 연산자를 아래 규칙에 따라 제공한다.

    • +, - 는 항상 새로운 collection을 반환한다
    • mutable collection에서 +=, -=는 collection을 원소를 변경한다.(새로운 collection을 생성하지 않음)
    • immutable collection에서 +=, -=는 새로운 collection을 반환한다. 따라서 이를 받는 변수는 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]

     

    3) 단항 연산자 오버로딩

    Expression Function name
    +a unaryPlus
    -a unaryMinus
    !a not
    ++a, a++ inc
    --a, a-- dec

     

     

    2. 비교 연산자 오버로딩

    코틀린에서는 Primitive 타입 뿐만 아니라 모든 객체에 대한 비교 연산을 수행할 수 있다.

    eqauls나 compareTo를 호출해야하는 자바와 달리 코틀린에서는 == 비교 연산자를 직접 사용할 수 있어 훨씬 코드를 이해하기 쉽다

     

    1) 동등성 연산자 : equals

    코틀린은 ==, != 연산자 호출을 equals 메소드 호출로 컴파일 한다.

    a == b → a?.equals(b) ?: (b==null)

    Data 클래스는 컴파일러가 equals 를 자동으로 생성해주며, 직접 equals 를 구현할 수도 있다. (equals 함수는 Any에 정의된 메소드이므로 override를 해야한다)

     

    2) 순서 연산자 : compareTo

    자바에서는 정렬이나 최대,최소값을 비교할 때 Comparable 인터페이스를 구현해야했다. 코틀린도 Comparable 인터페이스를 지원한다.

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

    a >= b -> a.compareTo(b) >= 0

    compareValueBy(객체1, 객체2, 비교조건1, 비교조건2) 함수는 두개의 조건을 우선순위 비교조건에 따라 처리하는 함수이다.

    1. 두객체 비교 equals -> 0

    2. 비교조건1 사용 -> 0 이 안나올때까지 비교

    3. 만약 비교조건1이 모두 0이라면 비교조건2 사용 -> 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 (비교조건1인 lastName을 비교했을때 알파벳 우선순위로 p1의 S가 P2의 J보다 우선)
    }

     

     

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

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

    코틀린에서는 특이하게 a[b] 처럼 배열 형태로 collection을 읽을수 있으며 이 또한 convention으로 처리한다.

    배열은 array[index]형태로 접근하며, collection에서 같은 방법을 제공하기 위해 index 연산자로 get 과 set을 제공한다.

    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
            else ->
                throw IndexOutOfBoundsException("Invalid coordinate $index")
        }
    }
    
    fun main(args: Array) {
        val p = MutablePoint(10, 20) 
        p[1] = 42 
        println(p)  // MutablePoint(x=10, y=42) 
    }

     

    2) in 관례

    in은 객체가 컬렉션에 들어있는지 검사한다. 이 경우 in 연산자와 대응하는 함수는 contains다.

    a in c -> c.contains(a)
    data class Point(val x: Int, val y: Int)
    
    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
    }
    // upperLeft.x until lowerRight.x -> 범위를 만든다
    // p.x in uper... -> .contains(p.x) -> 범위안에 p.x가 있는지 검사한다
    
    fun main(args: Array<String>) {
        val rect = Rectangle(Point(10, 20), Point(50, 50))
        println(Point(20, 30) in rect)  // true
        println(Point(5, 5) in rect)  // false
    }

    until을 통해 열린 범위를 만들고 그 범위 안에 해당 좌표가 포함되는지를 검사한다.

    (열린범위 : 끝 값을 포함하지 않는 범위 / 닫힌범위 : 끝 값을 포함하는 범위)

     

    3) rangeTo 관례

    start..end -> start.rangeTo(end)
    fun main(args: Array<String>) {
        val n = 9
        println(0..(n + 1))				// 0..10
        (0..n).forEach { print(it) }	// 0123456789
    }

     

    4) for 루프를 위한 iterator 관례

    for(x in list) { ... } 의 경우 list.iterator()를 호출해서 이터레이터를 얻은 다음, 자바와 마찬가지로 그 이터레이터에 대해 hasNext와 next 호출을 반복하는 식으로 변환된다.

    코틀린에서는 이 또한 관례이므로 iterator 메소드를 확장 함수로 정의할 수 있다. 이러한 성질로 인해 일반 자바 문자열에 대한 for 루프가 가능하다.

    operator fun CharSequence.iterator(): CharIterator
    for (c in "abc") {}

     

     

    4. 구조 분해 선언 (destructuring declaration)과 component 함수

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

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

    data class는 property 순서대로 componentN을 자동 생성해준다. 일반 클래스라도 componentN()을 operator fun으로 선언하면 사용이 가능하다.

    val(a, b) = 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]) // 함수에서 데이터 클래스의 인스턴스를 반환한다. 
    }
    
    fun main(args: Array<String>) {
        val (name, ext) = splitFilename("example.kt") // 구조 분해 선언 구문을 사용해 데이터 클래스프를 푼다. 
        println(name)  // example
        println(ext)   // kt
    }

    * 표준 라이브러리의 Pair나 Triple 클래스를 사용하면 함수에서 여러 값을 더 간단하게 반환할 수 있다.

     

    구조 분해 선언과 루프

    for((key, value) in map) {
        println("$key -> $value")
    }

    이 간단한 예제는 두 가지 코틀린 관례를 활용한다. 하나는 객체를 이터페이션하는 관례고, 다른 하나는 구조 분해 선언이다. 또한 코틀린 라이브러리는 Map.Entry에 대한 확장 함수로 component1과 component2를 제공한다.

     

     

    5. 프로퍼티 접근자 로직 재활용 : 위임 프로퍼티 (Property delegation)

    위임 프로퍼티 (delegated property)는 코틀린이 제공하는 관례에 의존하는 특성 중 독특하면서 강력한 기능을 갖고 있다.

    property의 위임이란 property에 대한 get(), set() 동작을 특정 객체가 처리하도록 위임하는걸 말한다.

    class Delegate {
        operator fun getValue(...) { ... }
        operator fun setValue(..., value: Type) { ... }
    }
    
    class Foo {
        var p: Type by Delegate()
    }

    위임 프로퍼티의 일반적인 문법은 위와 같다. p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다. 여기서는 Delegate 클래스의 인스턴스를 위임 객체로 사용한다. by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻는다.

     

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

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

    초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다.

     

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

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

    이런 코드를 만드는 일은 약간 성가시다. 지연 초기화해야 하는 프로퍼티가 많아지면 코드가 어떻게 될까? 게다가 이 구현은 스레드 안전하지 않아서 언제나 제대로 작동한다고 말할 수도 없다. 위임 프로퍼티를 사용하면 훨씬 더 간편해지고 스레드 안전하다.

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

    lazy 함수는 코틀린 관례에 맞는 시그니처 getValue 메소드가 들어있는 객체를 반환한다.

     

     

    참고

    https://saeyeong.tistory.com/5

    https://incheol-jung.gitbook.io/docs/study/kotlin-in-action/7

    https://tourspace.tistory.com/118

     

     

     

    728x90
Designed by Tistory.