코틀린
[Kotlin In Action] 7장. 연산자 오버로딩과 기타 관례
newwisdom
2021. 12. 10. 11:31
반응형
산술 연산자 오버로드
관례란?
어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법을 말한다.
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 <T> MutableCollection<T>.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<Person> {
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[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<T: Comparable>
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
for 루프를 위한 iterator 관례
- kotlin.collections.Iterator를 리턴하는 iterator() 함수는 for 루프에서 사용 가능
- for(x in list) { ... } 같은 경우는 list.iterator()를 호출해 iterator를 얻음
- 이 iterator에 대해 hasNext와 next 호출을 반복하는 식으로 변환됨
operator fun ClosedRange<LocalDate>.iterator() : Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext(): Boolean = current <= endInclusive
override fun next(): LocalDate = current.apply {
current = plusDays(1)
}
}
fun main(args: Array<String>) {
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 <T> Lazy<T>.getValue(
thisRef: Any?, property: KProperty<*>): T = value
➕ 더 읽어보면 좋을 글
프로퍼티 값을 맵에 저장
- 자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 사용할 수 있음
class Person {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
- 위 코드가 가능한 이유는 표준 라이브러리가 Map과 MutableMap 인터페이스에 대해 getValue와 setValue 확장 함수를 제공하기 때문
반응형