7장 연산자 오버로딩과 기타 관례
KOTLIN IN ACTION 7장을 요약한 내용입니다.
코틀린에서 관례를 사용하는 가장 단순한 예는 산술 연산자다. 자바에서는 원시 타입에 대해서만 산술 연산자를 사용할 수 있고, 추가로 String에 대해 + 연산자를 사용할 수 있다.
Point에서 지원하고픈 첫 번째 연산은 두 점을 더하는 연산이다.
// 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)
}
// 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)
}
operator 변경자를 추가해 plus 함수를 선언하고 나면 + 기호로 두 Point 객체를 더할 수 있다.
Expression | Function name |
a * b | times |
a / b | div |
a % b | mod |
a + b | plus |
a - b | minus |
연산자를 정의할 때 두 피연산자가(연산자 함수의 두 파라미터)가 같은 타입일 필요는 없다. 또는 연산자 함수의 반환 타입이 꼭 두 피연산자 중 하나와 일치해야만 하는 것도 아니다.
data class Point(val x: Int, val y: Int)
// case 1. 두 피연산자가 다른 연산자 정의
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
// case 2. 반환 타입이 피연산자와 다른 연산자 정의
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
+=, -= 등의 연산자는 복합 대입(compound assignment)연산자라 불린다.
>>> var point = Point(1, 2)
>>> point += Point(3, 4)
>>> println(point)
Point(x=4, y=6)
코틀린 표준 라이브러리는 변경 가능한 컬렉션에 대해plusAssign을 정의하며, 앞의 예제는 그 plusAssign을 사용한다.
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
이론적으로코드에 있는 +=를 plus와 plusAssign 양쪽으로 컴파일할 수 있다. 어떤 클래스가 이 두 함수를 모두 정의하고 둘 다 +=에 사용 가능한 경우 컴파일러는 오류를 보고한다.
단항 연산자 오버로딩하는 절차도 이항 연산자와 마찬가지다.
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus(): Point {
return Point(-x, -y)
}
fun main(args: Array<String>) {
val p = Point(10, 20)
println(-p)
}
Expression | Function name |
+a | unaryPlus |
-a | unaryMinus |
!a | not |
++a, a++ | inc |
--a, a-- | dec |
코틀린이 == 연산자 호출을 equals 메소드 호출로 컴파일한다는 사실을 배웠다. ≠ 연산자를 사용하는 식도 equals 호출로 컴파일된다.
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
}
}
fun main(args: Array<String>) {
println(Point(10, 20) == Point(10, 20))
println(Point(10, 20) != Point(5, 5))
println(null == Point(1, 2))
}
자바에서 정렬이나 최댓값, 최솟값 등 값을 비교해야 하는 알고리즘에 사용할 클래스는 Comparable 인터페이스를 구현해야 한다. Comparable에 들어있는 compareTo 메소드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내준다. 하지만 자바에는 이 메소드를 짧게 호출할 수 있는 방법이 없다.
코틀린도 똑같은 Comparable 인터페이스를 지원한다.
class Person(
val firstName: String, val lastName: String
) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other,
Person::lastName, Person::firstName)
}
}
이 코드는 코틀린 표준 라이브러리의 compareValuesBy 함수를 사용해 compareTo를 쉽고 간결하게 정의할 수 있다.
fun main(args: Array<String>) {
println("abc" < "bac")
}
컬렉션이 지원하는 다른 연산자로는 in이 있다. In은 객체가 컬렉션에 들어있는지 검사한다. 그런 경우 in 연산자와 대응하는 함수는 contains다.
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
}
fun main(args: Array<String>) {
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
println(Point(5, 5) in rect)
}
범위를 만들려면 .. 구문을 사용해야 한다. 예를 들어 1..10은 1부터 10까지 모든 수가 들어있는 범위를 가리킨다.

https://livebook.manning.com/book/kotlin-in-action/chapter-7/65
fun main(args: Array<String>) {
val n = 9
println(0..(n + 1))
(0..n).forEach { print(it) }
}
코틀린에서는 iterator 메소드를 확장 함수로 정의할 수 있다. 이런 성질로 인해 일반 자바 문자열에 대한 for 루프가 가능하다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext() =
current <= endInclusive
override fun next() = current.apply {
current = plusDays(1)
}
}
fun main(args: Array<String>) {
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) { println(dayOff) }
}
구조 분해를 사용하면 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.
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 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)
println(ext)
}
표준 라이브러리의 Pair나 Triple 클래스를 사용하면 함수에서 여러 값을 더 간단하게 반환할 수 있다. Pair나 Triple은 그 안에 담겨있는 원소의 의미를 말해주지 않으므로 경우에 따라 가독성이 떨어질 수 있는 반면, 직접 클래스를 작성할 필요가 없으므로 코드는 더 단순해진다.
함수 본문 내의 선언문뿐 아니라변수 선언이 들어갈 수 있는 장소라면 어디든 구조 분해 선언을 사용할 수 있다.
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
fun main(args: Array<String>) {
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
}
이 간단한 예제는 두 가지 코틀린 관례를 활용한다. 하나는 객체를 이터페이션하는 관례고, 다른 하나는 구조 분해 선언이다. 또한 코틀린 라이브러리는 Map.Entry에 대한 확장 함수로 component1과 component2를 제공한다.
for (entry in map.entries) {
val key = entry.component1()
val value = entry.component2()
}
위임 프로퍼티(delegated property)를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다. 또한 그 과정에서 접근자 로직을 매번 재구현할 필요도 없다.
위임은 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴을 말한다. 이때 작업을 처리하는 도우미 객체를 위임 객체라고 부른다.
지연 초기화(lazy initialization)는 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴이다. 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있다.
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 메소드가 들어있는 객체를 반환한다. 따라서 lazy를 by 키워드와 함께 사용해 위임 프로퍼티를 만들 수 있다.
- 코틀린에서는 정해진 이름의 함수를 오버로딩함으로써 표준 수학 연산자를 오버로딩할 수 있다. 하지만 직접 새로운 연산자를 만들 수는 없다.
- 비교 연산자는 equals와 compareTo 메소드로 변환된다.
- 클래스에 get, set, contains라는 함수를 정의하면 그 클래스의 인스턴스에 대해 []와 in 연산을 사용할 수 있고, 그 객체를 코틀린 컬렉션 객체와 비슷하게 다룰 수 있다.
- 미리 정해진 관례를 따라 tangeTo, iterator 함수를 정의하면 범위를 만들거나 컬렉션과 배열의 원소를 이터레이션할 수 있다.
- 구조 분해 선언을 통해 한 객체의 상태를 분해해서 여러 변수에 대입할 수 있다. 함수가 여러 값을 한꺼번에 반환해야 하는 경우 구조 분해가 유용하다. 데이터 클래스에 대한 구조 분해는 거저 사용할 수 있지만, 커스텀 클래스의 인스턴스에서 구조 분해를 사용하려면 componentN 함수를 정의해야 한다.
- 위임 프로퍼티를 통해 프로퍼티 값을 저장하거나 초기화하거나 읽거나 변경할때 사용하는 로직을 재활용할 수 있다. 위임 프로퍼티는 프레임워크를 만들 때 아주 유용하다.
- 표준 라이브러리 함수인 lazy를 통해 지연 초기화 프로퍼티를 쉽게 구현할 수 있다.
- Delegates.observable 함수를 사용하면 프로퍼티 변경을 관찰할 수 있는 관찰자를 쉽게 추가할 수 있다.
- 맵을 위임 객체로 사용하는 위임 프로퍼티를 통해 다양한 속성을 제공하는 객체를 유연하게 다룰 수 있다.
Last modified 2yr ago