깨알 개념/Kotlin

[Kotlin] 연산자 오버로딩 (Operator overloading)

interfacer_han 2024. 1. 29. 11:47

#1 메소드 오버로딩 (Method overloading)

class Calculator {
    // 정수형 두 개를 더하는 메소드
    fun add(x: Int, y: Int): Int {
        return x + y
    }
 
    // 실수형 두 개를 더하는 메소드
    fun add(x: Double, y: Double): Double {
        return x + y
    }
}

fun main() {
    val calculator = Calculator()
    
    // 정수형 덧셈
    val result1 = calculator.add(3, 4)
    println("Result of integer addition: $result1") // 출력 결과: 7
    
    // 실수형 덧셈
    val result2 = calculator.add(2.5, 3.5)
    println("Result of double addition: $result2") // 출력 결과: 6.0
}

메소드의 선언부에 명시되는 매개변수의 리스트를 메소드 시그니처(Method signature)라고 한다. 메소드 오버로딩이름이 같지만 메소드 시그너처가 다른 메소드들을 각각 서로 다른 메소드로 취급하는 프로그래밍 기법이다.
 

#2 연산자 오버로딩 (Operator overloading)

#2-1 개념

메소드 오버로딩이 '메소드를 중복해서 정의'이라면, 연산자(Operator) 오버로딩은 '연산자를 중복해서 정의'다. 연산자란 예를 들면 -1, 2 + 2, 4* 7 등의 수식에 있는 연산자 기호들을 말한다.
 
메소드 오버로딩에서 메소드를 구분하는 핵심은 '메소드 시그니처'였다. 연산자 오버로딩에도 이른바 '연산자 시그니처'의 역할을 수행하는 무언가가 있을까? 그건 바로 피연산자(Operation)다. 예를 들어, 71 - 16에서 연산자는 마이너스 기호고 피연산자는 Int다. "구구" + "콘"에서 연산자는 플러스 기호고, 피연산자는 String이다.
 

#2-2 문법

// report card: 성적표
class ReportCard(val name: String, val score: Int) {
    
    operator fun plus(increment: Int): ReportCard {
        return ReportCard(name, score + increment)
    }

    // '연산자 오버로딩'을 '메소드 오버로딩'함
    operator fun plus(reportCard: ReportCard): ReportCard {
        return ReportCard(name + "와(과) " + reportCard.name, score + reportCard.score)
    }

    operator fun minus(decrement: Int): ReportCard {
        return ReportCard(name, score - decrement)
    }

    // Kotlin 내 모든 클래스의 최상위 클래스인 Any 클래스의 메소드 equals()를 override
    override operator fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as ReportCard

        if (name != other.name) return false
        if (score != other.score) return false

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + score
        return result
    }
}

fun main() {
    var s1 = ReportCard("철수", 100)
    s1 = s1 - 50 + ReportCard("영희", 200)

    if (s1 == ReportCard("철수와(과) 영희", 250)) {
        println("${s1.name}의 점수는 ${s1.score}점") // 출력 결과: 철수와(과) 영희의 점수는 250점
    }
}

인터페이스나 클래스에서, 함수 키워드인 fun 앞에 operator 키워드를 달아주면 된다. 단, operator는 몇몇 지정된 이름의 함수에만 붙일 수 있다. 말 그대로 연산자를 오버로딩하는 것이기 때문에, 프로그래머들 사이에서 명시적으로 합의된 연산자의 이름들을 정해두었다. 그 정해진 이름을 가진 앞에 앞에만 operator 키워드를 붙일 수 있다. 여담으로, Any.equal()을 override할 때는 Any.hashCode()도 같이 override해주어야 한다고 한다.
 
다음은 operator 키워드를 붙일 수 있는 함수의 이름과 그 역할이다.
 

#3 operator 키워드용 함수

#3-1 일반적인 함수

 

Operator overloading | Kotlin

 

kotlinlang.org

위 링크에서 #2-2의 코드와 같은 일반적이고 기본적인 operator용 함수 이름을 찾을 수 있다.
 

#3-2 invoke()

interface Calculator {
    operator fun invoke(x: Int, y: Int): Int
    operator fun invoke(x: Double, y: Double): Double
}

class AddCalculator : Calculator {
    override operator fun invoke(x: Int, y: Int): Int {
        return x + y
    }

    override operator fun invoke(x: Double, y: Double): Double {
        return x + y
    }
}

fun main() {
    val calculator: Calculator = AddCalculator()

    val resultInt = calculator(3, 5)
    println("답은 $resultInt") // 답은 8

    val resultDouble = calculator(3.5, 5.5)
    println("답은 $resultDouble") // 답은 9.0
}

이와 같이 () 연산자의 동작도 override할 수 있다. 해당 동작에 해당하는 함수 이름은 영어로 '호출하다'라는 의미를 지닌 invoke다.
 

#3-3 get(), set()

class DataContainer {
    private val data = mutableMapOf<String, Any>()

    operator fun get(key: String): Any? {
        return data[key]
    }

    operator fun set(key: String, value: Any) {
        data[key] = value
    }
}

fun main() {
    val container = DataContainer()

    // set() 연산자 오버로딩 사용
    container["name"] = "철수"
    container["age"] = 25

    // get() 연산자 오버로딩 사용
    val name = container["name"]
    val age = container["age"]

    println("이름은 $name(이)고 나이는 $age") // 출력 결과: 이름은 철수(이)고 나이는 25
}

Array의 어떤 원소에 접근할 때 쓰던 대괄호 기호도 연산자다. 그렇기에 연산자 오버로딩이 가능하다.
 

#4 요약

연산자 오버로딩으로 사용자 정의 클래스를 유연하고 우아하게 사용한다.