[Kotlin Tutorial] Operator overload 와 convention #1 - Chap7. Operator overloading and other conventions |
참조 : Kotlin in action
-
Java 에는 특정 class 에 결속되어 있는 언어적 기능이 있다.
예를 들어 Iterable 를 구현하면 for loop 에서 쓸 수 있고, AutoCloseable 을 구현하면 try-with-resources 에서 사용할 수 있다.
Kotlin 도 비슷한 것들이 있다.
그러나 specific type 에 결속된 것이 아니라 specific name 에 결속되는 기능들이 있다.
예를 들어 plus 라는 이름으로 class 에 function 을 추가하면, + operator 를 해당 class 간에 사용할 수 있다.
이것을 Kotlin 에서는 “convention” 이라고 부른다.
-
Kotlin 이 convention 을 사용하는 이유는 이미 존재하는 Java class 들도 Kotlin language feature 에 적용하기 위함이다.
Java 코드를 변화시킬 수 없는 경우에는 Kotlin 만의 특성을 적용시키기 어렵기 때문이다. ( 추가 interface 를 구현시킬 수 없다. )
그러나 extension function 을 이용해 기능을 추가할 수는 있다.
extension method 를 통해 convention method 들을 추가하면 Java 코드 수정 없이 Kotlin 의 convention 기능을 사용할 수 있다.
7.1. Overloading arithmetic operators
-
Java 에서 arithmetic operation 은 primitive type 과 String 에만 사용할 수 있었다.
그러나 BigInteger 등에서도 + 가 쓰이면 편리할 것이다.
Collection 에서 += operator 들이 쓰이면 또 편리할 것이다.
Kotlin 은 “convention” 을 사용해 이를 수행할 수 있다.
7.1.1. Overloading binary arithmetic operations
-
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)
val sumPoint = p1 + p2 // 40, 60 값을 갖는 Point, p1.plus(p2) 코드가 수행된다고 보면 된다
operator keyword 를 사용해 plus function 을 정의하면 + 를 쓸 수 있다.
-
extension function 으로도 정의 가능하다.
external library class 에 convention 을 추가할 때는 이렇게 extension function 으로 정의한다.
operator fun Point.plus(other: Point): Point{
return Point(x + other.x, y+other.y)
}
-
직접 operator 를 정의할 수는 없고, 정해진 몇 개의 set 에 대해서만 overload 가능하다.
Expression / Function name
a * b / times
a / b / div
a % b / mod
a + b / plus
a - b / minus
-
operator 를 overload 해도 기본 numeric type 우선순위는 보장된다.
즉 a + b * c 가 있으면 b * c 가 먼저 수행되고, 그 다음 a 가 더해진다.
-
Java 에서 Kotlin 의 operator 는 그냥 function call 을 하면 된다.
Java 는 operator overload 가 없기 때문에 그냥 convention 함수들이 정의되어 있으면 operator 들을 쓸 수 있다.
-
operator 를 정의할 때 operand 를 꼭 같은 type 으로 지정할 필요는 없다.
operator fun Point.times(scale: Double): Point{
return Point(x * scale).toInt(), (y * scale).toInt())
}
-
Kotlin 의 operator 는 commutativity ( left, right 를 operator 를 사이에 두고 swap 해도 결과가 같음 ) 를 지원하지 않는다.
예를 들어 위의 times 의 경우 1.5 * p 는 p * 1.5 와 다르다.
1.5 * p 를 사용하려면 Double.times(p : Point) : Point 가 정의되어 있어야 한다.
-
operator 의 return type 도 type 이 달라도 된다.
operator fun Char.times(count: Int): String{
return toString().repeat(count)
}
-
operator function 도 일반 function 처럼 자유롭게 overload 할 수도 있다.
-
Kotlin 은 number type 에 대해 bitwise operator 를 정의하지 않는다.
따라서 해당 operator overload 도 불가능하다.
대신 infix call syntax 를 이용해서 일반적인 function 으로 대체하였다.
shl - Signed shift left
shr - Signed shift right
ushr - Unsigned shift right
and - Bitwie and
or - Bitwise or
xor - Bitwise xor
inv - Bitwise inversion
println(0x0F and 0xF0) // 0
println(0x0F or 0xF0) // 255
println(0x1 shl 4) // 16
7.1.2. Overloading compound assignment operators
-
plus operator 를 정의하면 + 뿐만 아니라 자동으로 += 도 지원된다.
-
+= 를 새롭게 정의하고 싶다면 plusAssign 를 정의해주면 된다. ( 기존 operator 함수에 Assign 을 postfix 로 )
이 때 return type 을 Unit 으로 준다면 새롭게 Object 를 만드는 것이 아니라, 기존 object 를 modify 한다고 보면 된다.
( 이 경우는 array 에 대한 operation 을 생각하면 쉽다. array1 += array2 )
-
plus 와 plusAssign 이 모두 정의가 된 상황에서 += 가 사용되면 plus 와 plusAssign 모두가 불릴 수 있다.
이 경우에는 compiler 가 error 를 내뿜는다.
a += b
a = a.plus(b)
a.plusAssign(b)
이를 해결하려면 operator 가 아닌 function call 을 해야 한다.
혹은 val 로 선언해서 compiler 가 plusAssign 이 호출되도록 유도할 수 있다.
가장 좋은 것은 plus 와 plusAssign 을 둘 다 정의하지 않는 것이다.
class 가 immutable 이면 plus 만 정의하고,
mutable 인 경우에는 plusAssign 만 제공하는 것이 좋다.
-
Kotlin 의 collection 에서는 다음과 같이 작동한다.
+, - operator 는 항상 새로운 collection 을 return 한다.
+= 와 -= operator 는 mutable collection 에서는 object 를 modify 한다.
+=, -= 가 read-only 에서는 modified copy 를 return 한다. ( collection 참조가 var 이어야만 한다 )
그리고 operands 로는 collection 이 될 수도 있고, 개별 element 가 될 수도 있다.
val list = arrayListOf(1, 2)
list += 3 // [1, 2, 3]
val newList = list + listOf(4,5) // [1, 2, 3, 4, 5]
7.1.3. Overloading unary operators
-
operator fun Point.unaryMinus(): Point{
return Point(-x, -y)
}
Unary operator 는 param 이 없다.
-
Expression / Function name
+a / unaryPlus
-a / unaryMinus
!a / not
++a, a++ / inc
—a, a— / dec
inc 와 dec 는 기존 동작과 동일하게 작동한다. ( 순서적으로 )
operator fun BigDecimal.inc() = this + BigDecimal.ONE
var bd = BigDecimal.ZERO
println(bd++) // 0
println(++bd) // 2
7.2. Overloading comparison operators
7.2.1. Equality operators: “equals”
-
Kotlin 에서 == 는 equals method 를 호출해 동일함을 비교한다.
!= 도 마찬가지로 equals 를 이용한다.
다른 operator 들과는 다르게 == 와 != 는 nullable operands 와도 사용될 수 있다.
a == b 를 사용하면 a 가 null 이 아닌지 보고, null 이 아니면 a.equals(b) 를 호출한다. a 가 null 이면 b 도 null 일 때만 true 이다.
a == b // a?.equals(b) ?: (b== null)
-
data class 로 define 된 class 는 property 들을 이용해서 자동으로 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
}
}
-
=== 는 overload 될 수 없다.
equals 는 extension 으로 정의될 수 없다. Any class 를 상속한 경우 extension 보다 Any class 의 것이 항상 우선되기 때문이다.
7.2.2. Ordering operators: compareTo
-
Java 와 동일하게 Kotlin 에서도 Comparable interface 를 사용해야 object 간 비교가 가능하다.
그러나 Java 와는 다르게 모든 Comparable interface 를 구현한 클래스들에 대해 <, >, <=, >= 를 사용할 수 있다.
Kotlin 에서 저 operator 들이 호출되면 Comparable interface 의 compareTo 가 자동으로 불린다.
a >= b // a.compareTo(b) >= 0
-
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 는 Kotlin 기본 lib 에 들어 있는 것으로 compareTo 를 쉽고 간단하게 구현할 수 있게 도와준다.
param 으로 비교할 object 들과 callback 들을 받는데, callback 들은 비교대상을 이야기한다.
callback 은 lambda 일 수도 있고, property reference 일 수도 있다.
물론 빠르기로 따진다면 직접 구현한 것이 더 빠르다.
그래서 compareValuesBy 를 사용할 때는 conciseness 와 performance 를 고려해봐야 한다.
-
Java 에서 Comparable interface 를 구현한 것은 Kotlin 에서 operator 를 사용해 비교될 수 있다.
7.3. Conventions used for collections and ranges
7.3.1. Accessing elements by index: “get” and “set”
-
Kotlin 에서 index operator 는 또 하나의 convention 이다.
val value = map[key]
mutableMap[key] = newValue
read 는 get 으로 치환되고, write 는 set 으로 치환된다.
operator fun Point.get(index: Int): Int{
return when(index){
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException(“Invalid coordinate $index”)
}
}
-
x[a, b] // x.get( a, b )
위와 같이 get 으로 치환되는 형태이기 때문에 parameter 는 Int 가 아닌 다른 것이 되어도 된다.
예를 들어 map 의 경우는 이미 key 가 object (Any) 형태이다
여러 버전의 get 함수를 overload 해도 된다.
two dimensional array 의 경우는 row, column 으로 해당 element 에 접근하는 신박한 녀석을 만들 수도 있다.
set 의 경우도 비슷하다.
x[a, b] = c // x.set(a, b, c)
7.3.2. The “in” convention
-
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
}
a in c // c.contains(a)
7.3.3. The rangeTo convention
-
.. 는 rangeTo function 에 mapping 된다.
rangeTo 는 Range class 를 return 한다.
start..end // start.rangeTo(end)
-
만약 class 가 Comparable interface 를 구현했다면 Kotlin standard lib 을 사용해서 range 를 create 할 수 있다.
이 떄 생성되는 Range 는 in 을 check 하기 위한 용도이지 모든 element 가 다 들어간 것은 아니다.
operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>
val now = LocalDate.now()
val vacation = now..now.plusDays(10)
println(now.plusWeeks(1) in vacation) // true
LocalDate 는 rangeTo 를 구현하지 않았지만, Comparable interface 를 구현했기 때문에 extension function 에 의해 .. 을 사용할 수 있다.
-
.. 은 arithmetic operator 보다 우선순위가 낮지만, 가독성을 위해 그래도 괄호를 쓰는 것이 좋다.
0..n+1 보다 0..(n+1) 이 선호된다.
7.3.4. The “iterator” convention for the “for” loop
-
for 문 안의 in 은 iterator() 함수를 호출한다.
iterator 는 hasNext 와 next 가 있고, 이 녀석들이 호출되면서 for 문을 도는 것이다.
Kotlin 에서는 iterator method 도 convention 이다.
-
Kotlin 에서는 CharSequence 에 extension 으로 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) }
}
댓글