[CH5.3] 연산자

사용자 정의 연산자

스위프트에서는 사용자 (프로그래머)의 입맛에 맞게 연산자 역할을 부여할 수 있다.
또, 기존에 존재하지 않던 연산자 기호를 만들어 추가할 수도 있다.

우선 기존 연산자의 역할을 변경하거나 새로운 역할을 추가하기 위해서는 기존의 연산자가 전위 연산자인지, 중위 연산자인지, 후위 연산자인지 알아야한다.

전위 연산자는 연산자가 피연산자 앞에 위치하는 연산자를 뜻한다.
대표적인 예로 Bool 부정 논리연산(NOT) 연산자(!) 가 있다.

  • !A

중위 연산자는 피연산자 사이에 위치하는 연산자를 뜻한다.
많은 수의 연산자가 이에 속한다.

  • A + B

후위 연산자는 피연산자 뒤에 위치하는 연산자를 뜻한다.
대표적인 예로 옵셔널 강제 추출 연산자 등이 있다.

  • A!

전위 연산자를 뜻하는 키워드는 prefix , 중위 연산자를 뜻하는 키워드는 infix , 후위 연산자를 뜻하는 키워드는 postfix 를 사용한다.

operator 라는 키워드는 연산자임을 뜻하고, associativity 는 연산자 결합방향을 뜻한다.
precedence 라는 키워드는 우선순위를 뜻한다.

사용자 정의 연산자는 아스키 문자 를 결합해서 사용한다.

또, 마침표(.)를 사용자 정의 연산자에 사용할 수 있다. 다만 마침표를 사용자 정의 연산자에 사용할 때 주의할 점이 있다. 연산자를 표현하는 문자 중 맨 처음의 문자가 마침표일 때만 연산자에 포함된 마침표가 연산자로 인식된다.

예를 들어 .+. 처럼 사용할 수 없다. 만약 마침표로 시작되지 않는 연산자에 마침표가 들어가게 되면, 이를 인식할 수 없다.

예를 들어 +.+ 의 경우에는 + 연산자와 .+ 연산자를 사용한 것으로 인식된다.

물음표(?)도 사용자 정의 연산자에 포함시킬 수 있지만, 물음표 자체만으로는 사용자 정의 연산자를 정의할 수 없다. 더불어 사용자 정의 연산자에 느낌표(!) 도 같은 조건으로 포함시킬 수 있다. 단, 전위 연산자는 물음표나 느낌표로 시작하는 사용자 정의 연산자를 정의할 수 없다.

토큰으로 사용되는 =,->,//,/*,*/,. 과 전위 연산자 <, &, ? 중위 연산자 ? , 후위 연산자 >, !, ? 등은 이미 스위프트에서 예약한 상태이기 때문에 재정의 할 수 없으며, 사용자 정의 연산자로 사용될 수도 없다.

5.3.1 전위 연산자 정의와 구현

Int 타입의 제곱을 구하는 연산자로 을 전위 연산자로 사용하려고 한다. 기존에 없던 전위 연산자를 만들고 싶다면, 연산자 정의**를 먼저 해주어야 한다.

정의한다는 뜻은 '이제 이 연산자를 사용하겠다' 라고 알리는 것을 뜻한다. 정의된 연산자는 모듈 전역에서 사용된다.

prefix operator **

위 코드처럼 연산자의 정의를 마치면, 어떤 데이터 타입에 이 연산자가 동작할 것인지 함수를 구현한다.

전위 연산자 함수를 구현할 때는 함수 func 키워드 앞에 prefix 키워드를 추가해준다.

prefix operator **

prefix func ** (value: Int) -> Int {
		return value * value
}

let minusFive: Int = -5
let sqrtMinusFive: Int = **minusFive

print(sqrtMinusFive) // 25

스위프트 표준 라이브러리에 존재하는 전위 연산자에 기능을 추가할 떄는 따로 연산자를 정의하지 않고 함수만 중복 정의하면 된다.

prefix func ! (value: String) -> Bool {
		return value.isEmpty
}

var stringValue: String = "iby"
var isEmptyString: Bool = !stringValue

print(isEmptyString) // false

stringValue = ""
isEmptyString = !stringValue

print(isEmptyString) // true

위 코드는 !를 사용하여 해당 문자열이 비어있는지를 확인할 수 있는 연산자로 사용하기 위해 함수를 중복 정의해준다.

또, 앞서 만들어주었던 ** 연산자를 String 타입에서도 동작할 수 있도록 중복 정의해줄 수도 있다.

prefix operator **

prefix func ** (value: String) -> String {
		return value + " " + value
} 

let resultString: String = **"iby"

print(resultString) // iby iby

5.3.2 후위 연산자 정의와 구현

이번에는 후위 연산자를 사용자 정의하는 방법을 알아본다.

사용자 정의 전위 연산자를 구현하는 것과 크게 다르지 않는다.

postfix operator **

postfix func ** (value: Int) -> Int {
		return value + 10
}

let five: Int = 5
let fivePlusTen: Int = five**

print(fivePlusTen) // 15

하나의 피연산자에 전위 연산과 후위 연산을 한줄에 사용하게 되면 후위 연산을 먼저 수행하게 된다.

아래 코드를 살펴보자.

prefix operator ** // 전위 연산자
postfix operator ** // 후위 연산자

prefix func ** (value: Int) -> Int {
    return value * value
}

postfix func ** (value: Int) -> Int {
    return value + 10
}

let five: Int = 5
let sqrtFivePlusTen: Int = **five**

print(sqrtFivePlusTen) // (10 + 5) * (10 + 5) == 225

이렇게 보면 같이 적혀있음에도 불구하고, 후위 연산을 먼저 수행하고 전위를 수행하는 모습이 보인다.

5.3.3 중위 연산자 정의와 구현

중위 연산자 정의도 전위 연산자나 후위 연산자 정의와 크게 다르지 않다.
다만, 중위 연산자는 우선순위 그룹을 명시해줄 수 있다.

우선 연산자 우선순위 그룹을 정의하는 방법에 대해 알아보자. 연산자 우선순위 그룹은 precedencegroup 뒤에 그룹 이름을 써주어 정의할 수 있다.

precedencegroup 우선순위그룹이름 {
    higherThan: 더 낮은 우선순위 그룹 이름
    lowerThan: 더 높은 우선순이 그룹 이름
    associativity: 결합방향(left, right, none)
    assignment: 할당방향 사용 (true, false)
}

연산자 우선순위 그룹은 중위 연산자에서만 사용된다. 전위 연산자 및 후위 연산자는 결합방향 및 우선순위를 지정하지 않는다. 대신, 앞서 설명했듯 하나의 피연산자에 전위 연산과 후위 연산을 한 줄에 사용하게 되면 후위 연산을 먼저 수행한다.

더 낮은 우선순위 그룹 이름을 넣을 수 있는 higherThan 과 더 높은 우선순위 그룹 이름을 넣을 수 있는 lowerThan 에 들어갈 수 있는 그룹 이름을 통해 기존의 우선순위 그룹과 새로 만들어줄 우선순위 그룹과의 상하관계를 설정해 줄 수 있다. lowerThan 속성에는 현재 모듈 밖에 정의된 우선순위 그룹만 명시할 수 있다.

결합방향을 명시해줄 수 있는 associativity 에는 left, right, none 을 지정해줄 수 있다.
만약 associativity 를 빼놓고 연산자 우선순위 그룹을 정의하면 기본적으로 none이 설정된다.

결합방향이 없는 연산자는 여러 번 연달아 사용할 수 있다. 결합방향이 있는 더하기(+), 빼기(-) 등의 연산자는 1 + 2 + 3 과 같이 연산해줄 수 있고, 3 - 2 - 1, 과 같이 연산해줄 수 있다.

결합방향이 있는 연산자는 섞어서 1 + 2 - 3 처럼도 사용할 수 있다.

그러나, 결합방향이 없는 부등호 연산자는 연달아 사용할 수 없다.

1 < 2 < 3 과 같은 모양으로 사용할 수 없다라는 뜻이다.

연산자 우선순위 그룹의 assignment 는 옵셔널 체이닝과 관련된 사항이다. 연산자가 옵셔널 체이닝을 포함한 연산에 포함되어 있을 경우 연산자의 우선순위를 지정한다.

true 로 설정해주면 해당 우선순위 그룹에 해당하는 연산자는 옵셔널 체이닝을 할 때 표준 라이브러리의 할당 연산자와 동일한 결합방향 규칙을 사용한다. 즉, 스위프트의 할당 연산자는 오른쪽 결합을 사용하므로 assignment 를 true로 설정하면 연산자를 사용하여 옵셔널 체이닝을 할 때 오른쪽부터 체이닝이 시작된다는 뜻이다.

그렇지 않고 false를 설정하거나 assignment 를 따로 명시해주지 않으면 해당 우선순위 그룹에 해당하는 연산자는 할당을 하지 않은 연산자와 같은 옵셔널 체이닝 규칙을 따른다.

만약, 중위 연산자를 정의할 때 우선순위 그룹을 명시해주지 않는다면, 우선순위가 가장 높은 DefaultPrecendence 그룹을 우선순위 그룹으로 갖게 된다.

연산자 우선순위 그룹을 정의하는 방법을 알아보았으니, 본격적으로 중위 연산자를 정의하는 방법을 알아보자.

중위 연산자의 정의에는 infix 라는 키워드를 사용한다.

infix operator ** : MultiplicationPrecedence

를 중위 연산자로 사용하기 위해 정의했다.
연산자의 이름은
이고, MultiplicationPrecedence 연산자 우선순위 그룹에 속하게 된다.

만약 MultiplicationPrecedence 라고 명시해주지 않는다면 DefaultPrecedence 그룹으로 자동 지정된다.

import Foundation

infix operator ** : MultiplicationPrecedence

func ** (lhs: String, rhs: String) -> Bool {
    return lhs.contains(rhs)
}

let helloIby: String = "iby"
let iby: String = "iby"
let isContainsIby: Bool = helloIby ** iby // true

위에서 연산자 정의를 했으니, 이제 구현할 차례이다.

문자열과 문자열 사이에 ** 연산자를 사용하면 뒤에 오는 문자열이 앞의 문자열 안에 속해 있는지 확인하는 연산을 실행하도록 구현했다. 중위 연산자 구현 함수에는 따로 키워드를 추가하지 않는다.

import Foundation

class Car {
    var modelYear: Int?
    var modelName: String?
}

struct SmartPhone {
    var company: String?
    var model: String?
}

// Car 클래스의 인스턴스끼리 == 연산했을 때 modelName이 같다면 true를 반환
func == (lhs: Car, rhs: Car) -> Bool {
    return lhs.modelName == rhs.modelName
}

// SmartPhone 구조체의 인스턴스끼리 == 연산했을 때 model이 같다면 true를 반환
func == (lhs: SmartPhone, rhs: SmartPhone) -> Bool {
    return lhs.model == rhs.model
}

let myCar = Car()
myCar.modelName = "S"

let yourCar = Car()
yourCar.modelName = "S"

var myPhone = SmartPhone()
myPhone.model = "SE"

var yourPhone = SmartPhone()
yourPhone.model = "6"

print(myCar == yourCar)     // true
print(myPhone == yourPhone) // false

위 코드를 보면, 우리가 정의한 데이터 타입(클래스, 구조체) 에서 유용하게 사용할 수 있는 연산자도 새로 정의하거나 중복 정의할 수 있음을 알 수 있다.

위 코드의 사용자 정의 연산자는 다른 코드들과 마찬가지로 전역함수로 구현했다. 그러나 특정 타입에 국한된 연산자 함수라면 그 타입 내부에 구현되는 것이 읽고 이해하기에는 더욱 쉬울 것이다.

그래서 타입 내부에 타입 메서드로 구현할 수도 있다.

class Car {
    var modelYear: Int?
    var modelName: String?
    
    static func == (lhs: Car, rhs: Car) -> Bool {
        return lhs.modelName == rhs.modelName
    }
}

struct SmartPhone {
    var company: String?
    var model: String?
    
    static func == (lhs: SmartPhone, rhs: SmartPhone) -> Bool {
        return lhs.model == rhs.model
    }
}

코드에서 타입 메서드로 구현한 사용자 정의 연산자는 각 타입의 익스텐션으로 구현해도 된다.

좋은 웹페이지 즐겨찾기