BottleH Blog

Kotlin in Action - 9장 연산자 오버로딩과 다른 관례

    Tags

  • Kotlin
Kotlin in Action - 9장 연산자 오버로딩과 다른 관례 thumbnail

코틀린은 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법인 관례에 의존한다.

📖 9.1 산술 연산자를 오버로드해서 임의의 클래스에 대한 연산을 더 편리하게 만들기

🔖 9.1.1 plus, times, divide 등: 이항 산술 연산 오버로딩

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() { val p1 = Point(10, 20) val p2 = Point(30, 40) println(p1 + p2) // + 기호를 쓰면 plus 함수 호출 }
  • plus 함수 앞에 operator 키워드를 붙여야 한다.
  • 연산자를 확장 함수로 정의할 수도 있다.
표현식 함수 이름
a + b plus
a - b minus
a * b times
a / b div
a % b mod
  • 연산자 우선순위는 표준 숫자 타입에 대한 연산자 우선순위와 같다.
operator fun Point.times(scale: Double): Point { return Point((x * scale).toInt(), (y * scale).toInt()) } fun main() { val p = Point(10, 20) println(p * 1.5) }
  • 연산자를 정의할 때 두 피연산자가 같은 타입일 필요는 없다.
  • 자동으로 교환법칙을 지원하지 않는다.
operator fun Char.times(count: Int): String { return toString().repeat(count) } fun main() { println('a' * 3) }
  • 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다.

🔖 9.1.2 연산을 적용한 다음에 그 결과를 바로 대입: 복합 대입 연산자 오버로딩

fun main() { var point = Point(1, 2) point += Point(3, 4) println(point) }
  • plus와 같은 연산자를 오버로딩하면 복합 대입 연산자를 자동으로 지원한다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) { this.add(element) }
  • 어떤 클래스가 plus, plusAssign을 모두 정의하고 +=을 사용하면 오류를 보고한다.
  • 코틀린 표준 라이브러리는 컬렉션에 대해 2가지 접근 방법을 제공
    • +,-는 항상 새로운 컬렉션 반환
    • +=, -= 연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화

🔖 9.1.3 피연산자가 1개뿐인 연산자: 단항 연산자 오버로딩

operator fun Point.unaryMinus(): Point { return Point(-x, -y) }
  • 단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 취하지 않는다.
표현식 함수 이름
+a unaryPlus
-a unaryMinus
!a not
a++, ++a inc
a-- , --a dec
fun main() { var bd = BigDecimal.ZERO println(bd++) // 후위 증가 연산자는 println이 실행된 다음에 값 증가 println(bd) println(++bd) // 전위 증가 연산자는 println이 실행되기 전에 값 증가 }

📖 9.2 비교 연산자를 오버로딩해서 객체들 사이의 관계를 쉽게 검사

🔖 9.2.1 동등성 연산자: equals

  • != 연산자를 사용하는 식도 equals 호출로 컴파일 된다.
  • null 검사도 한다.
class Point(val x: Int, val y: Int) { override fun equals(other: Any?): Boolean { if (other === this) return true if (other !is Point) return false return other.x == x && other.y == y } }
  • 동등성 비교 연산자(===)는 자바 == 연산자와 같다.
    • 자신의 두 피연산자가 서로 같은 객체ㅡㄹ 가리키는지 비교한다.
  • ===를 오버로딩할 수는 없다.

🔖 9.2.2 순서 연산자: compareTo (<, >, <=, >=)

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() { val p1 = Person("Alice", "Smith") val p2 = Person("Bob", "Johnson") println(p1 < p2) }
  • 비교 연산자를 제공한다.
  • compareValuesBy는 두 객체와 여러 비교 함수를 인자로 받는다.
    • 첫 번째 비교 함수에 두 객체를 넘겨 두 객체가 같지 않다는 결과가 나오면 그 결괏값을 즉시 반환
  • 비교 연산자를 자바 클래스에 대해 사용하기 위해 특별히 확장 메서드를 만들거나 할 필요는 없다.

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

🔖 9.3.1 인덱스로 원소 접근: get과 set

operator fun Point.get(index: Int): Int { return when (index) { 0 -> x 1 -> y else -> throw IndexOutOfBoundsException("Invalid coordinate $index") } } fun main() { val p = Point(10, 20) println(p[1]) }
  • get 메서드의 파라미터로 Int가 아닌 타입도 사용할 수 있다.
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() { val p = MutablePoint(10, 20) p[1] = 42 println(p) }
  • set도 관례로 표현할 수 있다.

🔖 9.3.2 어떤 객체가 컬렉션에 들어있는지 검사: in 관레

data class Rectangle(val upperLeft: Point, val lowerRight: Point) operator fun Rectangle.contains(p: Point): Boolean { return p.x in upperLeft.x..<lowerRight.x && p.y in upperLeft.y..<lowerRight.y // 열린 범위 } fun main() { val rect = Rectangle(Point(10, 20), Point(50, 50)) println(Point(20, 30) in rect) println(Point(5, 5) in rect) }
  • ..< 연산자를 쓰면 열린 범위를 만들 수 있다.
  • in 함수는 contains 메서드의 수신 객체가 되고 in의 왼쪽에 있는 객체는 contains 메서드에 인자로 전달된다.

🔖 9.3.3 객체로부터 범위 만들기: rangeTo와 rangeUntil 관레

operator fun <T: Comparable<T>> T.rangeTo(other: T): ClosedRange<T>
  • 코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.
fun main() { val now = LocalDate.now() val vacation = now..now.plusDays(10) println(now.plusWeeks(1) in vacation) }
  • now..now.plusDays(10) 식은 컴파일러에 의해 now.rangeTo(now.plusDays(10))으로 변환된다.
  • rangeTo 연산자는 다른 산술 연산자보다 우선순위가 낮다.
    • 혼동을 피하기 위해 괄호를 쓰자!
fun main() { val n = 9 (0..n).forEach { println(it) } }
  • 범위의 메서드를 호출하려면 범위를 괄호로 둘러싸야 한다.
  • rangeUntil 연산자(..<)>=는 열린 범위를 만든다.

🔖 9.3.4 자신의 타입에 대해 루프 수행: iterator 관례

operator fun CharSequence.iterator(): CharIterator
  • 이 라이브러리 함수는 문자열을 이터레이션할 수 있게 해준다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> { var current = start override fun hasNext(): Boolean = current <= endInclusive override fun next(): LocalDate { val thisDate = current current = current.plusDays(1) return thisDate } } fun main() { val newYear = LocalDate.ofYearDay(2042, 1) val daysOff = newYear.minusDays(1)..newYear for (dayOff in daysOff) { println(dayOff) } }
  • ClosedRange<LocalDate>에 대한 확장 함수 iterator를 정의했기 때문에 LocalDate의 범위 객체를 for 루프에 사용할 수 있다.

📖 9.4 component 함수를 사용해 구조 분해 선언 제공

fun main() { val p = Point(10, 20) val (x, y) = p println(x) println(y) }
  • 구조 분해 선언은 일반 변수 선언과 비슷해 보인다.
  • 내부에서 구조 분해 선언의 각 변수를 초기화하고자 componentN이라는 함수를 호출한다.
    • N은 구조 분해 선언에 있는 변수 위치에 따라 붙는 번호
  • data class의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다.
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() { val (name, ext) = splitFileName("example.kt") println(name) println(ext) }
  • 구조 분해 선언을 사용해 여러 값 반환이 가능하다.
data class NameComponents(val name: String, val extension: String) fun splitFileName(fullName: String): NameComponents { val (name, extension) = fullName.split('.', limit = 2) return NameComponents(name, extension) }
  • 컬렉션에 대해 구조 분해 선언 사용이 가능하다.

🔖 9.4.1 구조 분해 선언과 루프

fun printEntries(map: Map<String, String>) { for ((key, value) in map) { println("$key -> $value") } } fun main() { val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin") printEntries(map) }
  • 맵에 대한 확장 함수로 iterator가 들어있다.
map.forEach { (key, value) -> println("$key -> $value") }
  • 람다가 구조 분해 선언을 쓸 수 있다.

🔖 9.4.2 _ 문자를 사용해 구조 분해 값 무시

data class Person(val firstName: String, val lastName: String, val age: Int, val city: String) fun introducePerson(p: Person) { val (firstName, lastName, age, city) = p println("This is $firstName, aged $age.") }
  • 전체 객체를 구조 분해해야만 하는 것은 아니기 때문에 구조 분해 선언에서 뒤쪽의 구조 분해 선언을 제거할 수는 잇다.
val (firstName, lastName, age) = p
  • lastName을 그냥 없앨 수는 없다.
fun introducePerson(p: Person) { val (firstName, _, age) = p println("This is $firstName, aged $age.") }
  • _ 를 쓰면 컴포넌트를 무시하고 구조 분해 선언이 가능하다.

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

🔖 9.5.1 위임 프로퍼티의 기본 문법과 내부 동작

class Foo { var p = Type by Delegate() }
  • p 프로퍼티는 접근자 로직을 다른 객체에 위임한다.
class Foo { private val delegate = Delegate() var p: Type set(value: Type) = delegate.setValue(/**/, value) get() = delegate.getValue(/**/) }
  • 프로퍼티 위임 관례에 따라 Delegate 클래스는 get, set을 제공해야한다.
  • by 키워드는 프로퍼티와 위임객체를 연결한다.

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

  • 지연 초기화는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴
class Person(val name: String) { private var _emails: List<String>? = null val emails: List<String> get() { if (_emails == null) { _emails = loadEamils(this) } return _emails!! } } fun main() { val p = Person("Alice") p.emails }
  • 뒷받침하는 프로퍼티 기법 사용
  • 클래스 같은 개념을 표헌하는 프로퍼티가 2개 있을 때 비공개 프로퍼티 앞에 밑줄을 붙이며, 공개 프로퍼티에는 아무것도 붙이지 않는다.
class Person(val name: String) { val emails by lazy { loadEmails(this) } }
  • 위임 프로퍼티를 사용하면 훨씬 간단해진다.
  • lazy 함수는 기본적으로 스레드 안전하다.

🔖 9.5.3 위임 프로퍼티 구현

fun interface Observer { fun onChange(name: String, oldValue: Any?, newValue: Any?) } open class Observable { val observers = mutableListOf<Observer>() fun notifyObservers(propName: String, oldValue: Any?, newValue: Any?) { for (obs in observers) { obs.onChange(propName, oldValue, newValue) } } } class Person(val name: String, age: Int, salary: Int) : Observable() { var age: Int = age set(newValue) { val oldValue = field field = newValue notifyObservers("age", oldValue, newValue) } var salary: Int = salary set(newValue) { val oldValue = field field = newValue notifyObservers("salary", oldValue, newValue) } }
  • field 키워드를 사용해 age와 salary 프로퍼티를 뒷받침하는 필드에 접근하는 방법을 보여준다.
class ObservableProperty(val propName: String, var propValue: Int, val observable: Observable) { fun getValue(): Int = propValue fun setValue(newValue: Int) { val oldValue = propValue propValue = newValue observable.notifyObservers(propName, oldValue, newValue) } } class Person(val name: String, age: Int, salary: Int) : Observable() { val _age = ObservableProperty("age", age, this) var age: Int get() = _age.getValue() set(value) { _age.setValue(value) } val _salary = ObservableProperty("salary", salary, this) var salary: Int get() = _salary.getValue() set(value) { _salary.setValue(value) } }
  • 프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스를 만들었다.
  • 위임 프로퍼티를 사용하면 더 간단해진다.
class ObservableProperty(var propValue: Int, val observable: Observable) { operator fun getValue(thisRef: Any?, prop: KProperty<*>): Int = propValue operator fun setValue(thisRef: Any?, prop: KProperty<*>, newValue: Int) { val oldValue = propValue propValue = newValue observable.notifyObservers(prop.name, oldValue, newValue) } }
  • operator 변경자가 붙는다.
  • KProperty 타입의 객체를 사용해 프로퍼티 표현
class Person(val name: String, age: Int, salary: Int) : Observable() { val age by ObservableProperty(age, this) var salary by ObservableProperty(salary, this) }
  • 여러 작업을 컴파일러가 자동으로 처리해준다.
  • by의 오른쪽에 있는 식이 꼭 새 인스턴스를 만들 필요는 없다.

🔖 9.5.4 위임 프로퍼티는 커스텀 접근자가 있는 감춰진 프로퍼티로 변환된다

class C { private val <delegate> = MyDelegate() var prop: Type get() = <delegate>.getValue(this, <property>) set(value: Type) = <delegate>.setValue(this, <property>, value) }
  • 컴파일러는 모든 프로퍼티 접근자 안에 getValue, setValue 호출 코드를 생성해준다.
  • 프로퍼티 값이 저장될 장소를 바꿀 수도 있고 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경할 수도 있다.

🔖 9.5.5 맵에 위임해서 동적으로 애트리뷰트 접근

class Person { private val _attributes = mutableMapOf<String, String>() fun setAttribute(attrName: String, value: String) { _attributes[attrName] = value } var name: String get() = _attributes["name"]!! set(value) { _attributes["name"] = value } }
  • 위임 프로퍼티를 활용하도록 변경하는 것은 아주 쉽다.
class Person { private val _attributes = mutableMapOf<String, String>() fun setAttribute(attrName: String, value: String) { _attributes[attrName] = value } var name: String by _attributes }
  • Map에 대해 getValue, setValue 확장 함수를 제공하기에 동작함.
  • getValue에서 맵에 프로퍼티 값을 저장할 때는 자동으로 프로퍼티 이름을 키로 활용

🔖 9.5.6 실전 프레임워크가 위임 프로퍼티를 활용하는 방법

  • 위임 프로퍼티는 프로퍼티의 접근 로직을 재사용 가능하게 만들어, 코드의 중복을 줄이고 유지보수성을 향상시킨다.
  • 프레임워크나 라이브러리에서 공통된 프로퍼티 동작을 캡슐화하여, 사용자 코드의 간결함과 일관성을 유지할 수 있다.
Written by@BottleH
Back-End Developer

GitHub