[Kotlin In Action] 7장. 연산자 오버로딩과 기타 관례

[Kotlin In Action] 7장. 연산자 오버로딩과 기타 관례

산술 연산자 오버로드

관례란?

어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 말한다.

ex) 산술 연산자 - 어떤 클래스에 plus라는 메서드를 정의할 때 그 클래스의 인스턴스에 대해 + 연산자를 사용할 수 있다.

이항 산술 연산자

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) Point(x=40, y=60)

operator 키워드와 지정한 함수 이름을 사용해서 연산자를 오버로딩

기본적으로 연산자 오버로딩을 사용할 때는 operator 키워드를 명시해야 함 이는 이 함수가 어떤 관례를 따르고 있는지 명시하기 위함

operator fun Point.plus(other:Point):Point { return Point(x + other.x, y + other.y) } operator fun Point.times(scale: Double): Point { ... }

확장함수로 만드는 것도 가능

여러 타입에 대한 연산자 오버로딩도 가능

Expression Function name a*b times a/b div a%b mod(1.1부터 rem) a+b plus a-b minus

복합 대입 연산자 오버로딩

plus와 minus와 같은 연산자를 오버로딩하면 코틀린은 관련있는 +=, ‐=의 복합 대입 연산자도 자동으로 지원

var point = Point(1, 2) point += Point(3, 4) // point = point + Point(3, 4)

변경 가능한 컬렉션에 원소를 추가하는 경우

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

반환 타입이 Unit인 plusAssign 함수를 정의할 수 있음

해당 컬렉션이 읽기 전용일 경우 변경을 적용한 복사본을 반환

+=에 대해 plus와 plusAssign 둘 다 정의할 시 컴파일 오류가 발생한다.

변수를 val로 한다면 plusAssign을 사용하자.

단항 연산자 오버로딩

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

operator fun BigDecimal.inc() = this + BigDecimal.ONE var bd = BigDecimal.ZERO println(bd++)

비교 연산자 오버로딩

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

자바와 달리 equals, compareTo를 호출하지 않고도 == 비교 연산자를 직접 사용할 수 있다.

동등성 연산자: "equals"

== 연산자 호출은 equals 메서드 호출로 컴파일한다.

Any의 equals에 이미 operator가 있으므로 붙일 필요가 없다.

식별자 비교 연산자인 ===을 사용해서 두 객체간의 참조가 같은지 비교할 수 있다.

순서 연산자: "compareTo"

Comparable 인터페이스의 compareTo 메서드 호출 관례를 지원해줌

<, <=, >, >= 연산자를 Comparable#compareTo 호출로 컴파일

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

compareTo는 Int 값을 반환

class Person(val firstName: String, val lastName: String) : Comparable { override fun compareTo(other: Person): Int { return compareValuesBy(this, other, Person::firstName, Person::lastName) } } val p1 = Person("Alice", "Smith") val p2 = Person("Bob", "Johnson") println(p1 < p2)

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

x[a, b] : x.get(a, b)

: x.get(a, b) x[a, b] = c : x.set(a, b, c)

: x.set(a, b, c) a in c : c.contains(a)

in 관례

객체가 컬렉션에 들어있는지 검사

in 연산자와 대응하는 함수는 contains()

rangeTo 관례

.. 구문으로 범위를 생성 val range = 1 .. 10

rangeTo 함수 호출로 컴파일 됨 start..end : start.rangeTo(end)

rangeTo의 리턴 값은 ClosedRange

operator fun > T.rangeTo(that: T): ClosedRange

for 루프를 위한 iterator 관례

kotlin.collections.Iterator를 리턴하는 iterator() 함수는 for 루프에서 사용 가능 for(x in list) { ... } 같은 경우는 list.iterator()를 호출해 iterator를 얻음 이 iterator에 대해 hasNext와 next 호출을 반복하는 식으로 변환됨

operator fun ClosedRange.iterator() : Iterator = object : Iterator { var current = start override fun hasNext(): Boolean = current <= endInclusive override fun next(): LocalDate = current.apply { current = plusDays(1) } } fun main(args: Array) { val newYear = LocalDate.ofYearDay(2018, 1) val daysOff = newYear.minusDays(1)..newYear for (dayOff in daysOff) { println(dayOff) } }

구조 분해 선언과 component 함수

구조 분해 선언과 루프

destructuring, declaration

복합적인 값을 분해해서 여러 변수를 한꺼번에 초기화할 수 있음

val p = Point(10,20) // Point는 data 클래스 val (a, b) = p /* 아래와 같이 컴파일 됨 * val a = p.component1() * val b = p.component2() */ for ((key, value) in map) { // Map.Entry }

data 클래스는 컴파일러가 각 프로퍼티에 대해 component 함수 자동 생성

콜렉션의 경우 맨 앞 5개 원소에 대한 component 확장 함수 제공

data 클래스가 아닌 일반 클래스일 경우 아래와 같이 구현할 수 있음

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

구조 분해 선언과 루프 관례를 동시에 사용한 예시는 다음과 같다.

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

자바와는 다르게 코틀린에서는 Map에 iterator를 사용할 수 있다.

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

위임 프로퍼티 소개

프로퍼티 접근을 다른 객체에게 위임하는 기능

위임을 사용해 자신의 값을 필드가 아닌 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있음

class Foo { var p: Type by Delegate() // p 프로퍼티에 대한 접근을 Delegate 객체에 위임 // p: 위임할 프로퍼티 // by 오른쪽 : 위임 객체 } // 아래와 같이 컴파일 됨 class Foo { private val delegate = Delegate() var p: Type set(value: Type) = delegate.setValue(..., value) get() = delegate.getValue(...) }

위임한 프로퍼티를 읽고 쓸 때마다 위임 객체의 getValue/setValue 를 호출

위임 객체의 함수 규칙

operator getValue(obj: 프로퍼티_포함_타입, prop: KProperty<*>): 프로퍼티_타입

operator setValue(obj: 프로퍼티_포함_타입, prop: KProperty<*>, newValue:프로퍼티_타입)

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

lazy initialization은 객체의 일부분을 초기화하지 않고 남겨두었다가 해당 값이 필요한 시점에 초기화 할 경우 사용

lazy 함수는 기본적으로 스레드 안전

// 프로퍼티 초기화 지연 class Person(val name: String) { val emails by lazy { loadEmails(this) } } public inline operator fun Lazy.getValue( thisRef: Any?, property: KProperty<*>): T = value

➕ 더 읽어보면 좋을 글

프로퍼티 값을 맵에 저장

자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 사용할 수 있음

class Person { private val _attributes = hashMapOf() fun setAttribute(attrName: String, value: String) { _attributes[attrName] = value } val name: String by _attributes }

위 코드가 가능한 이유는 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장 함수를 제공하기 때문

from http://newwisdom.tistory.com/113 by ccl(A) rewrite - 2021-12-10 13:01:41