이 글은 "Kotlin in Action by Dmitry Jemerov and Svetlana Isakova, Manning(2017)" 중 7.1.2 Overloading compound assignment opertators를 참조해 작성했습니다.
연산자 오버로딩(operator overloading)에 관한 보충 설명입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 2.2 연산자 오버로딩을 참고하기 바랍니다. 오버로딩과 비슷한 용어로 오버라이딩(overriding)이 있습니다. 오버라이딩은 부모 클래스를 상속받은 자식 클래스에서 부모 클래스의 속성이나 메소드를 재정의하는 것을 말합니다. 오버로딩은 2.plus(3) 과 같이 메소드 plus()를 호출하는 대신 연산자 '+'를 사용해 2+3으로 나타내는 것을 말합니다. 그래서 오버로딩은 "연산자" 오버로딩이라 부릅니다. 연산자 오버로딩에서 무심코 지나치면 안되는 내용들을 살펴보겠습니다.
■ "+="는 plus()와 plusAssign() 중 어느 메소드를 호출할까요?
a += b는 a = a + b를 줄여서 표현한 것입니다. 아래 예에서는 클래스 Point의 멤버 함수로 plus()와 plusAssign()을 모두 정의했습니다. 이 때, 연산자 "+="는 plus()를 호출할까요 아니면 plusAssign()을 호출할까요? 정답은 "둘 다 호출한다"입니다. 코틀린 컴파일러는 "대입 연산자가 모호합니다(assignment operators ambiguity)"라는 에러 메시지를 출력합니다. 정리하면 사용자 정의 클래스에서 연산자 오버로딩을 위해 plus()와 plusAssign()을 함께 정의하면 안됩니다. 확장 함수로 정의해도 마찬가지입니다. 덧셈(+)뿐 아니라 다른 산술 연산자(-, *, /, %)도 마찬가지입니다. 예를 들면, minus()와 minusAssign()도 함께 정의할 수 없습니다.
class Point(var x: Int, var y: Int) {
operator fun plus(other: Point): Point {
return Point(this.x + other.x, this.y + other.y)
}
operator fun plusAssign(other: Point) {
this.x += other.x
this.y += other.y
}
override fun toString(): String {
return "Point(x=$x, y=$y)"
}
}
fun main() {
var p = Point(1, 2)
p += Point(5, 10) // 에러 !!! 대입 연산자가 모호합니다.
}
■ plus()이면 객체를 val로 선언해야 하지만, plusAssign()이면 객체를 val 또는 var로 선언할 수 있습니다.
두 메소드 중 하나만 정의하면 해결이 될까요. 맞습니다. 코드를 깔끔하게 정리하기 위해, 클래스 Point를 data 클래스로 변경합니다. 이렇게 data 클래스로 변경하면 toString() 메소드를 자동 생성하므로, 객체의 속성 값을 쉽게 확인할 수 있습니다(4.13 data 클래스참조). 아래 예에서는 클래스 Point의 멤버 함수로 plus()를 정의했습니다. 연산자 "+="은 plus()를 호출합니다. 여기서 주의할 점은 객체 p를 var로 선언한 것입니다. 객체 p를 val로 선언하면. 객체 p의 참조 대상을 바꿀 수 없어 "p += Point(5, 10)" 에서 에러가 발생합니다.
data class Point(var x: Int, var y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y) // this는 생략할 수 있습니다.
}
}
fun main() {
var p = Point(1, 2)
p += Point(5, 10) // 객체 p를 val로 선언하면 에러가 발생합니다.
println(p) // Point(x=6, y=12)
}
아래처럼 클래스 Point의 멤버 함수로 plus() 대신 plusAssign()을 정의하면 어떻게 될까요? 객체 p를 val로 선언해도 에러가 발생하지 않습니다. 객체 p는 속성(x, y)만 변경했을 뿐, 객체 p가 참조하는 대상이 바뀌지 않았기 때문입니다. 메소드 plusAssign()을 구현한 코드에서 이를 확인할 수 있습니다. 반면, 메소드 plus()는 새로운 Point 타입 객체를 반환합니다.
data class Point(var x: Int, var y: Int) {
operator fun plusAssign(other: Point) {
x += other.x // this는 생략할 수 있습니다.
y += other.y
}
}
fun main() {
val p = Point(1, 2) // 객체 p를 val로 선언했습니다.
p += Point(5, 10) // 에러가 발생하지 않습니다.
println(p) // Point(x=6, y=12)
}
■ 사용자 정의 클래스는 연산자 오버로딩을 위한 함수를 반드시 정의해야 하나요?
Point 클래스처럼 사용자 정의 클래스는 연산자 오버로딩을 위한 메소드를 멤버 함수 또는 확장 함수로 추가해야 합니다. 그러나 컬렉션으로 만들면, "+,-" 또는 "+=, -=" 연산자에 대한 연산자 오버로딩 메소드를 따로 정의할 필요가 없습니다.
가변 컬렉션(MutableCollection) 타입의 확장 함수로 plusAssign()과 minusAssign()을 정의했습니다. Point 타입을 원소로 갖는 가변 리스트 컬렉션 객체 p를 만듭니다. 객체 p에 연산자 "+="를 사용해 원소 Point(5, 6)을 추가합니다. "+=" 대신 add()를 호출해도 됩니다. 마찬가지 방식으로 객체 p에 연산자 "-="를 사용해 원소 Point(5, 6)을 삭제합니다. "-=" 대신 remove()를 호출해도 됩니다.
data class Point(val x: Int, val y: Int)
operator fun <T> MutableCollection<T>.plusAssign(element: T) {
this.add(element)
}
operator fun <T> MutableCollection<T>.minusAssign(element: T) {
this.remove(element)
}
fun main() {
val p: MutableList<Point> = mutableListOf(Point(1, 2), Point(3, 4))
p += Point(5, 6) // p.add(Point(5, 6))
println(p) // [Point(x=1, y=2), Point(x=3, y=4), Point(x=5, y=6)]
p -= Point(5, 6) // p.remove(Point(5, 6))
println(p) // [Point(x=1, y=2), Point(x=3, y=4)]
}
객체 p가 리스트 컬렉션이므로, 확장 함수 plusAssign()와 minusAssign()을 따로 정의하지 않고도 연산자 "+=" 및 "-="를 사용할 수 있습니다.
data class Point(val x: Int, val y: Int)
fun main() {
val p = mutableListOf(Point(1, 2), Point(3, 4))
p += Point(5, 6)
println(p) // [Point(x=1, y=2), Point(x=3, y=4), Point(x=5, y=6)]
p -= Point(5, 6)
println(p) // [Point(x=1, y=2), Point(x=3, y=4)]
}
■ 불변 컬렉션 객체와 덧셈 연산자(+, +=)와의 관계
연산자 "+"(또는 "-")를 사용하면, 새 컬렉션 객체를 생성합니다. 불변 리스트 객체 p2는 기존 객체 p에 Point 타입 원소를 추가해 새롭게 만들어진 컬렉션 객체를 참조합니다.
data class Point(val x: Int, val y: Int)
fun main() {
val p: List<Point> = listOf(Point(1, 2), Point(3, 4))
val p2: List<Point> = p + Point(5, 6)
println(p) // [Point(x=1, y=2), Point(x=3, y=4), Point(x=5, y=6)]
}
불변 컬렉션 객체를 복합 대입연산자 "+=" (또는 "-=")에 사용할 때는, 참조 대상을 새롭게 만들어진 객체로 변경해야 하므로, 객체 p를 var로 선언해야 합니다.
data class Point(val x: Int, val y: Int)
fun main() {
var p: List<Point> = listOf(Point(1, 2), Point(3, 4)) // val로 선언하면 에러 발생!!!
p += Point(5, 6)
println(p) // [Point(x=1, y=2), Point(x=3, y=4), Point(x=5, y=6)]
}
■ 가변 컬렉션 객체와 덧셈 복합 대입 연산자(+=)와의 관계
가변 컬렉션 객체 p를 덧셈 복합 대입연산자 "+=" (또는 "-=")에 사용할 때는, val 또는 var를 사용할 수 있습니다. val로 선언해도 원소를 추가할 수 있나요? 기존 객체 p에 원소만 추가될 뿐, 객체 p가 참조하는 대상이 바뀐 건 아닙니다.
data class Point(val x: Int, val y: Int)
fun main() {
val p: MutableList<Point> = mutableListOf(Point(1, 2), Point(3, 4))
p += Point(5, 6)
println(p) // [Point(x=1, y=2), Point(x=3, y=4), Point(x=5, y=6)]
}
'코틀린' 카테고리의 다른 글
코틀린: 연산자 오버로딩(3) - compareTo() (0) | 2025.01.17 |
---|---|
코틀린: 연산자 오버로딩(2) - equals() (0) | 2025.01.16 |
코틀린: 연산자 in과 범위(updated) (0) | 2025.01.06 |
코틀린: 식과 문장 (0) | 2025.01.05 |
1장 - 테스트에 도전해 보세요(정답 해설) (0) | 2024.09.14 |