[Kotlin in Action] part7_연산자 오버로딩과 기타 관례

지슈·2023년 2월 7일
0

Kotlin in Action

목록 보기
7/9

산술 연산자 오버로딩

자바에서는 원시 타입에 대해서만 산술 연산자를 사용하거나 String에 대해 +연산자를 사용할 수 있다. 코틀린은 이외의 클래스에서도 산술 연산자를 사용하는 것을 허용하고 있다.

어떻게 클래스에 대해 일반 산술 연산자를 정의할 수 있을까.

이항 산술 연산 오버로딩

Point 클래스에서 두 점을 더하는 연산. x 좌표와 y 좌표를 각각 더한다.

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)

연산자를 오버로딩할 때 함수 앞에 operater 키워드를 붙여야 한다.

그렇지 않으면 "operator modifier is required.." !!!

  • 연산자를 멤버 함수로 만들 수도 있지만 확장 함수로 정의할 수도 있다.

=> 코틀린에서는 언어에서 미리 정해둔 연산자만 오버로딩 할 수 있다. 정의해야 할 이름도 정해져 있다.

  • 연산자를 사용할 때 두 피연산자가 같은 타입일 필요는 없다
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
>>> val p = Point(10, 20)
>>> println(p * 1.5)
Point(x=15, y=30)
  • 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나여야 할 필요도 없다.
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
>>> println('a' * 3)
aaa

ㄴ 이 연산자는 Char를 좌항으로 받고 Int 를 우항으로 받아서 String 을 돌려준다.

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

plus와 같은 연산자를 오버로딩 하면 + 연산자 뿐만 아니라 +=도 자동으로 함께 지원한다.

+= *=와 같은 연산자를 복합 대입 연산자라고 한다.

point += Point(3, 4) == point = point + Point(3, 4)

-> 경우에 따라 +=연산이 객체에 대한 참조를 다른 참조로 바꾸기보다 원래 객체의 내부 상태를 변경하게 만들고 싶을 때가 있다.??

단항 연산자 오버로딩 (unary)

이항 연산자 오버로딩하는 것처럼 단항 연산자를 오버로딩할 수 있다.

operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
>>> val p = Point(10, 20)
>>> println(-p)
Point(x=-10, y=-20)
  • 단항 연산자를 오버로딩하기 위해 사용하는 함수는 인자를 취하지 않는다.

비교 연산자 오버로딩

동등성 연산자: equals

== 연산자는 equals 메서드 호출하여 호출한다.

!= 연산자를 사용하는 식도 equals 메서드를 호출한다.

  • 이 두 연산자는 널 검사에도 사용되므로 널이 될 수 있는 값에도 적용할 수 있다.

=> a==b를 처리할 때 a가 널이 아닌 경우에만 a.equals(b) 를 호출한다. a가 널이라면 b도 널이어야 true이다.

class Point(val x: Int, val y: Int) {
override fun equals(obj: Any?): Boolean {
if (obj === this) return true
if (obj !is Point) return false
return obj.x == x && obj.y == y
}
}

식별자 비교 연산자 === 를 사용해 equals의 파라미터가 수신 객체와 같은지 살펴본다.

순서 연산자: compareTo

코틀린도 자바와 마찬가지로 Comparable 인터페이스를 지원해서 비교하는 연산자를 사용한다.

따라서 <, >, <=, >= 와 같은 비교 연산자는 compareTo 호출로 컴파일된다.

: 사람을 비교하는 Person 예제

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

위 예제와 같이 코드를 작성하면 자바에 비해 훨씬 간결하게 compareTo를 정의할 수 있다.

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

인덱스로 원소에 접근: get와 set

코틀린에서 맵의 원소에 접근하거나 자바에서 배열 원소에 접근할 때 각괄호([])를 사용한다.

  • 코틀린에서는 인덱스 연산자도 관례를 따른다.
    ㄴ 인덱스 연산자로 원소를 읽을 때: get 연산자 메서드로 변환.
    ㄴ 인덱스 연산자로 원소를 쓸 떄: set연산자 메서드로 변환.
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
>>> val p = Point(10, 20)
>>> println(p[1])
20

-> 점의 좌표를 읽을 때 인덱스 연산을 사용. get 메서드.

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")
}
}

>>> val p = MutablePoint(10, 20)
>>> p[1] = 42
>>> println(p)
MutablePoint(x=10, y=42)

->변경 가능한 점을 표현할 때 set메서드로 인덱스에 원소를 씀.

in 관례

in 은 객체가 컬렉션에 들어있는지 검사한다.

in에 대응하는 함수는 contains이다.

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
}
>>> val rect = Rectangle(Point(10, 20), Point(50, 50))
>>> println(Point(20, 30) in rect)
true
>>> println(Point(5, 5) in rect)
false

10..20 -> 10부터 20까지의 닫힌 범위

10 until 20 -> 10부터 20까지의 열린 범위

rangeTo 관례

10..20 와 같은 ..연산자는 rangeTo 함수를 간단하게 표현하는 방법이다.

=> start..end -> start.rangeTo(end)

코틀린 표준 라이브러리에는 모든 Comparable 객체에 대해 적용 가능한 rangeTo 함수가 들어있다.

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

ㄴ 범위를 반환. in을 통해 어떤 원소가 들어있는지 검사.

for 루프를 위한 iterator 관례

for 루프에도 in을 사용하지만 의미는 다르다.
iterator메서드를 확장 함수로 정의할 수도 있다.
클래스 안에 직접 iterator 메서드를 구현할 수도 있다.

구조 분해 선언과 component 함수

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

:구조 분해를 사용하는 방법

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

=> 일반 변수 선언과 비슷하지만 = 의 좌변에 여러 변수를 괄호로 묶는 것이 차이점!

구조 분해 선언은 componentN 함수 호출로 변환된다.

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

구조 분해 선언과 루프

변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용할 수 있다.

// 구조 분해 선언을 사용해 맵 이터레이션하기
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
>>> val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
>>> printEntries(map)
Oracle -> Java
JetBrains -> Kotlin

=> 위 예제는 객체를 이터레이션 하는 관례, 구조 분해 선언하는 관례를 사용하고 있다.

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

위임 프로퍼티 소개

: 위임 프로퍼티의 일반적인 문법

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

ㄴ p 프로퍼티는 접근자 로직을 다른 객체에게 위임한다.

class Delegate {
operator fun getValue(...) { ... }
operator fun setValue(..., value: Type) { ... }
}
class Foo {
var p: Type by Delegate() //by 키워드는 프로퍼티와 위임 객체를 연결한다.
}

>>> val foo = Foo()
>>> val oldValue = foo.p
>>> foo.p = newValue

=> p의 게터나 세터는 Delegate 타입의 위임 프로퍼티 객체에 있는 메서드를 호출한다.

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

지연 초기화 : 객체의 일부분은 초기화하지 않고 남겨놨다가 필요한 경우 초기화 할 때 쓰이는 패턴이다.

어떤 경우? => 초기화 과정에 자원을 많이 사용하거나, 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티 경우

class Person(val name: String) {
private var _emails: List<Email>? = null
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
>>> val p = Person("Alice")
>>> p.emails
Load emails for Alice
>>> p.emails

ㄴ 지연 초기화를 뒷받침하는 프로퍼티를 통해 구현하기

뒷받침하는 프로퍼티 (backing property)

-> _emails 프로퍼티는 값을 제공, emails는 _emails에 대한 읽기 연산 제공.

위임 프로퍼티 구현

위임 프로퍼티를 구현하는 예제.

어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶을 때

▼ 위임프로퍼티를 사용해서 프로퍼티 변경 통지 받기

class Person(
val name: String, age: Int, salary: Int
) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
var salary: Int by ObservableProperty(salary, changeSupport)
}

by 키워드를 통해 위임 객체 지정:

=> 위임 객체를 감춰진 프로퍼티에 저장

=> 주 객체의 프로퍼티를 읽거나 쓸 떄마다 위임 객체의 getValue 와 setValue를 호출

위임 프로퍼티 컴파일 규칙

컴파일러는 모든 프로퍼티 접근자 안에 getValue와 setValue 호출 코드를 생성해준다.

=> 프로퍼티 값이 저장되는 장소를 바꾸거나 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경 가능

프로퍼티 값을 맵에 저장

확장 가능한 객체 -> 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 사용하는 위임 프로퍼티

=> by 키워드 뒤에 맵을 직접 넣기

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

ㄴ 값을 맵에 저장하는 위임 프로퍼티 사용하기

profile
공댕이😎_블체

0개의 댓글