Swift - 오퍼레이터 오버로드(Operator Overloads)

공식 레퍼런스 문서나 책에서는 항목으로 분리되지 않아서 중요치 않게 여겨질지도 모르겠지만, 개인적으로 스위프트(Swift)의 오퍼레이터 오버로드 기능은 중요도가 높다고 생각되어서 별도의 글로 정리를 해 본다.

글을 쓰면서 생각해봤는데 내가 제대로 이해하고 있는지도 약간 의문이다. 이상한 내용이 있을지도 모르겠다. ;-)

오퍼레이터 오버로드가 뭐냐고 하면... 그냥 특정 연산자의 기능을 추가하는 것이다. 예를 들자면 아래와 같이 특정 구조체를 선언했다고 보자.
struct MyType {
    var a = 0
    var b = 0
}
MyType이라는 이름의 구조체로 멤버가 정수형(Int) 2개의 포로퍼티만 존재하는 간단한 타입이다. 이 글의 예제는 모두 이 타입을 이용한다.

이제 실제로 인스턴스를 생성해서 비교를 해 보자.
var a = MyType(a: 1, b: 1)
var b = MyType(a: 2, b: 2)

a == b  
// ERROR: cound not find ad overload '==' that accepts the supplied arguments ...
같은지를 비교하는 연산자(==)를 당연히 쓸 수 없을거라 생각했고 역시나 오류가 발생한다. 이 두 인스턴스를 비교할 기준이 없기 때문이다.

여기서 만들어 볼 수 있는 기능이 바로 이 '같은지를 비교하는 연산자(==)'의 기능을 추가하는 것이다. 아래와 같은 함수를 추가한다. (메소드가 아니다!)
infix func == (left: MyType, right: MyType) -> Bool {
    if (left.a == right.a && left.b == right.b) {
        return true
    }
    return false
}
문법은 나중에 보고 결과를 보자.
var a = MyType(a: 1, b: 1)
var b = MyType(a: 2, b: 2)
a == b // false


var c = MyType(a: 10, b: 10)
var d = MyType(a: 10, b: 10)
c == d  // true
이제 == 오퍼레이터는 MyType에 한해서는 a와 b 프로퍼티의 값이 동일할때만 true가 된다. 비교를 하는 기능이 추가된 것이다.

이것이 바로 오퍼레이터(Operator, 연산자) 오버로드(Overload) 이다. 참고로 오버로드란 오버라이드(override)와는 다르게 '동일한 이름이지만 매개변수가 다른 함수나 메소드를 추가로 정의'하는 것을 의미한다.

오버로딩 방법

앞서 본 예 처럼 오퍼레이터 오버로딩은 특정 연산자 이름으로 함수를 만들면 되는데 이 함수 이름 앞에 특수 키워드가 붙는다. 여기에 올 수 있는건 prefix, postfix, infix 등이 있다. 이 특수키워드는 연산자의 위치나 행동 방식 등을 결정한다. (참고로 ​Beta 5 에서 각종 키워드 앞의 골뱅이 @가 빠졌고, assignment는 아예 통채로 사라졌다)

리턴값은 있을 수도 있고 없을 수도 있고 상황에 따라 다르다. 만약 연산식이 다른 값으로 대체되어야 한다거나 혹은 다른 변수에 대입하기 위해서 사용된다면 리턴 타입을 만들어 주고 값을 반환해 주어야 한다.

각 특수키워드 별로 하나하나 살펴보자.

prefix 와 postfix

이 두 키워드는 각각 '인스턴스 왼쪽에 오는 연산자' 와 '인스턴스 오른쪽에 오는 연산자' 정도로 해석하면 된다. 아래 예는 prefix 의 예제이다.
prefix func - (data: MyType) -> MyType {
    return MyType(a: -data.a, b: -data.b)
}

var e = MyType(a: 10, b: 10)
-e  // { a -10, b -10 }
이 코드는 - 연산자를 오버로드시켰다. 실행시키는 곳을 잘 보면 e 라는 변수를 만들고 여기다 MyType 인스턴스를 생성한다. 그리고 -e 를 호출해서 플레이그라운드를 통해 값을 확인한다. 여기서 '-e' 라는 구문에 집중하자. 마이너스(-)가 e 왼쪽에 붙어있다. 앞서 이야기한 대로 prefix는 '인스턴스 왼쪽' 이라고 칭했다. 이제 이해가 되어야 하는데... -_-;;

prefix 가 이해가 되었다면 postfix 도 이해가 될 것이다.

@assignment (주의: Beta 5 부터 키워드 제거)

assignment는 자신의 값이 변해야 하는 경우에 사용된다. 그래서 제한 조건으로 첫 번째 매개변수는 반드시 inout 으로 선언되어야 한다.
func += (inout left: MyType, right: Int) {
    left.a += right
    left.b += right
}

var f = MyType(a: 2, b: 5)
f += 1
// f = { a 3, b 6 }
'+=' 이라는 오퍼레이터를 오버로드했다. 마지막 실행시키는 곳을 보면 f += 1 이라는 구문이 있는데 left += right 형식이라고 이해가 가능하다.

앞서 이야기 한 대로 첫 번째 매개변수 left는 inout으로 선언되어있다. 이 말의 의미는 이 연산자의 결과로 left 인스턴스의 값이 변경된다는 뜻이다. 실제로 실행 결과는 f += 1 을 실행하니 f 자체의 값이 바뀌었다. assignment(대입)의 의미와 일치한다.

inout에 대해 잘 모르면 함수의 레퍼런스 매개변수 내용을 보자.

Xcode 6 Beta 5에서 assignment가 사라졌다. 이제는 그냥 inout으로 적당히 요리해 주면 된다.​

infix (주의: Beta 5 부터 키워드 제거)

infix는 연산자를 비교 형식으로 오버로드 할 때 필요하다. 앞서 '=='의 예제에서도 infix 를 사용했는데, 이번에는 크기를 비교하는 '>' 연산자를 오버로드 해 보자. infix 키워드는 Beta5부터 불필요해졌다. 기존 코드에서 infix를 제거하기만 하면 된다.
func > (left: MyType, right: MyType) -> Bool {
    if (left.a > right.a) {
        return true
    }
    else if (left.b > right.b) {
        return true
    }
    return false
}

MyType(a: 10, b: 11) > MyType(a: 8, b: 12)  // true
MyType(a:  7, b: 11) > MyType(a: 8, b: 12)  // false
MyType(a:  7, b: 11) > MyType(a: 7, b: 10)  // true
위 코드는 뭔가 좀 부족하긴 하지만, 값의 크기를 비교하기 위한 연산자 '>' 를 오버로드했다. 즉 left > right의 결과를 비교하기 위한 함수이며 리턴값으로 논리값인 Bool 타입을 리턴해서 참인지 거짓인지를 알려준다.

결합

앞서 살펴본 특수키워드들은 결합이 가능하다.
@prefix func ++ (inout data: MyType) -> MyType {
    data.a++
    data.b++
    return data
}

var g = MyType(a: 9, b: 10)
++g            // { a 10, b 11 }
var h = ++g    // { a 11, b 12 }
prefix, 즉 인스턴스 좌측에 오퍼레이터가 오는 형태이면서 assignment 의 기능(자신의 데이터가 바뀜)을 부여하는 형태로 ++ 오퍼레이터의 기능을 추가(오버로드)했다.

결국 ++g 라는 문법을 만들기 위해 prefix + assignment 라는 결합형이 사용되었다. 만약 g++ 이라는 문법을 지원하려면 postfix + assignment 를 사용하면 될 것이다.

결합형은 좀 복잡해 보이지만 오히려 범용 오퍼레이터를 제공하려면 아마도 결합형을 주로 사용하게 될 것이다.

위 예제는 assignment 키워드가 사라지는 바람에 결합을 설명하는데 부족해져서 삭제.

원하는 오퍼레이터를 생성하기

스위프트에서는 존재하지 않는 오퍼레이터(연산자)를 만들어서 특수한 기능을 만들어 줄 수도 있다. (참고로 Beta 5 에서 operator 키워드의 사용 순서가 변경됨)​
postfix operator ** {}
'operator' 라는 명령어와 함께 앞서 본 특수 키워드들을 활용하는데 이번에는 골뱅이(@)가 없는 형식이다. 그리고 오퍼레이터 이름을 '**'으로 만들었다. 중괄호({}) 내부가 비어있는데 여기에는 좀 더 추가로 설정값을 넣을 수 있다. 예를 들어 우선순위 같은것이 있는데 자세한 것은 레퍼런스 문서를 참고하자.

이제 오퍼레이터의 기능을 만들어보자.
postfix func ** (inout data: MyType) -> MyType {
    data.a = data.a * data.a
    data.b = data.b * data.b
    return data
}

var q = MyType(a: 10, b: 10)
q**    // { a 100, b 100 }
나는 그냥 이 ** 연산자의 기능을 '제곱'하는 기능으로 만들었다. postfix로 ** 연산자는 인스턴스 오른쪽에 위치 할 때만 동작한다. 그리고 데이터를 리턴하면서 자기자신의 값도 바뀐다.

오퍼레이터를 만드는 건 분명 대단한 내용이긴 한데 잘못 쓰면 독이 될 수도 있다. 다른 사람이 내가 만들어 놓은 이상한 오퍼레이터를 단박에 이해 할 수 있다면 좋겠지만 그렇지 않을 수도 있다. 물론 문서화를 통해 설명이 보충된다면 해결은 되겠지만 이상한 오퍼레이터는 가급적 만들지 않는 것이 좋다는 생각이다.

[관련글] Swift - 함수(Function)

[돌아가기] 스위프트(Swift) 가이드

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()