본문 바로가기
iOS/SWIFT

[Swift] Chapter 05. 연산자

by 원만사 2021. 10. 8.
반응형

 

스위프트의 연산자는 특정한 문자로 표현한 함수라 할 수 있다. 따라서 특정 연산자의 역할을 프로그래머의 의도대로 변경할 수도 있다.

5.1 연산자의 종류


5.1.2 산술 연산자

NOTE _ 스위프트의 나머지 연산과 나누기 연산자 스위프트에서는 부동소수점 타입의 나머지 연산까지 지원한다. 기존의 프로그래밍 언어에서는 나머지 연산자가 정수 타입만 지원하는 경우가 많았는데 스위프트에서는 부동소수점 타입도 나머지 연산을 할 수 있다. let number: Double = 5.0 var result: Double = number.truncatingRemainder(dividingBy: 1.5) // 0.5 result = 12.truncatingRemainder(dividingBy: 2.5) // 2.0 나누기 연산은 기존의 프로그래밍 언어처럼 나머지나 소수점을 제외한 정수만을 결괏값으로 반환한다. var result: Int = 5 / 3 // 1 result = 10 / 3 // 3 또한, 스위프트는 데이터 타입에 굉장히 엄격하므로 서로 다른 자료형끼리의 연산을 엄격히 제한한다. 서로 다른 자료형끼리의 연산을 실행하려면 값을 해당 타입으로 변환한 후 연산해야 한다. 심지어 같은 정수 타입인 Int 타입과 UInt 타입끼리의 연산도 엄격히 제한된다.

 

5.1.3 비교 연산자

두 값을 비교할 때 사용한다.

 

연산자 부호 설명
참조가 같다. A === B A와 B가 참조(레퍼런스) 타입일 때 A와 B가 같은 인스턴스를 가리키는지 비교하여 불리언 값을 반환한다.
참조가 같지 않다. A ! == B A와 B가 참조(레퍼런스) 타입일 때 A와 B가 같지 않은 인스턴스를 가리키는지 비교하여 불리언 값을 반환한다.
패턴 매치 A ~= B A와 B의 패턴이 매치되는지 확인하여 불리언 값을 반환한다.

 

NOTE _ 참조 비교 연산자 스위프트의 유일한 참조(Reference) 타입인 클래스의 인스턴스에서만 참조 비교 연산자를 사용할 수 있다(다른 데이터 타입에서 === 등의 연산자를 프로그래머가 정의하면 다른 용도로 사용이 가능하다). 스위프트의 기본 데이터 타입은 모두 구조체로 구현되어 있기 때문에 값 타입이다. 그렇기 때문에 값의 비교 연산에는 ==를 사용하고 클래스의 인스턴스인 경우에만 ===를 사용한다.
let valueA: Int = 3
let valueB: Int = 5
let valueC: Int = 5

let isSameValueAB: Bool = valueA == valueB  // false
let isSameValueBC: Bool = valueB == valueC  // true

let referenceA: SomeClass = SomeClass()
let referenceB: SomeClass = SomeClass()
let referenceC: SomeClass = referenceA

let isSameReferenceAB: Bool = referenceA === referenceB // false
let isSameReferenceAC: Bool = referenceA === referenceC // true

 

5.1.4 삼항 조건 연산자

Question ? A : B

→ Question(불리언 값)이 참이면 A를, 거짓이면 B를 반환한다.

 

/*
	코드 5-1. 삼항 조건 연산자의 사용
*/

var valueA: Int = 3
var valueB: Int = 5
var biggerValue: Int = valueA > valueB ? valueA : valueB    // 5

 

5.1.5 범위 연산자

값 (수)의 범위를 나타내고자 할 때 사용한다.

 

(1) 폐쇄 범위 연산자

  • A...B → A부터 B까지의 수를 묶어 범위를 표현. A와 B를 포함한다.

 

(2) 반폐쇄 범위 연산자

  • A..<B → A부터 B 미만까지의 수를 묶어 범위를 표현. A를 포함하고 B를 포함하지 않는다.

 

(3) 단방향 범위 연산자

  • A... → A 이상의 수를 묶어 범위를 표현한다. A를 포함한다.
  • ...A → A 이하의 수를 묶어 범위를 표현한다. A를 포함한다.
  • ..<A → A 미만의 수를 묶어 범위를 표현한다. A를 포함하지 않는다.

 

5.1.9 오버플로 연산자

기존 프로그래밍 언어에서는 오버플로(또는 언더플로) 가능성이 있는 연산에 대해서는 따로 오버플로에 대한 추가 알고리즘 및 로직 등을 설계하는 것이 일반적이었다. 스위프트는 기본 연산자를 통해 오버플로에 대비할 수 있도록 했다. 오버플로 연산자를 사용하면 오버플로를 자동으로 처리한다.

  • &+ → 오버플로에 대비한 덧셈 연산을 한다.
  • &- → 오버플로에 대비한 뺄셈 연산을 한다.
  • &* → 오버플로에 대비한 곱셈 연산을 한다.

 

/*
	코드 5-2. 오버플로 연산자의 사용
*/

var unsignedInteger: UInt8 = 0
//let errorUnderflowResult: UInt8 = unsignedInteger - 1  // 런타임 오류
let underflowedValue: UInt8 = unsignedInteger &- 1    // 255

unsignedInteger = UInt8.max    // 255
//let errorOverflowResult: UInt8 = unsignedInteger + 1   // 런타임 오류
let overflowedValue: UInt8 = unsignedInteger &+ 1      // 0

 

5.1.10 기타 연산자

(1) nil 병합 연산자

  • A ?? B → A가 nil이 아니면 A를 반환하고, A가 nil이면 B를 반환한다.

 

(2) 옵셔널 강제 추출 연산자

  • O! → O(옵셔널 개체)의 값을 강제로 추출한다.

 

(3) 옵셔널 연산자

  • V? → V(옵셔널 값)를 안전하게 추출하거나, V(데이터 타입)가 옵셔널임을 표현한다.

 

 

5.3 사용자 정의 연산자


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

 

NOTE _ =과 ?: 할당 연산자(=)와 삼항 연산자(?:)는 사용자 정의 역할을 부여할 수 없다.

 

사용자 정의 연산자는 아스키 문자 /, =, -, +, !, *, %, <, >, &, |, ^, ?, ~를 결합해서 사용한다.

또 마침표(.)를 사용자 정의 연산자에 사용할 수 있다. 다만 마침표를 사용자 정의 연산자에 사용할 때 주의할 점이 있다. 연산자를 표현하는 문자 중 맨 처음의 문자가 마침표일때만 연산자에 포함된 마침표가 연산자로 인식된다. 예를 들어 .+. 처럼 사용할 수 있다. 만약 마침표로 시작되지 않는 연산자에 마침표가 들어가게 되면 이를 인식할 수 없다. 예를 들어 +.+의 경우에는 +연산자와 .+ 연산자를 사용한 것으로 인식된다.

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

5.3.1 전위 연산자 정의와 구현

Int 타입의 제곱을 구하는 연산자로 **을 전위 연산자로 사용하려고 한다. 기존에 없던 전위 연산자를 만들고 싶다면 연산자 정의를 먼저 해주어야 한다. 정의한다는 뜻은 '이제 이 연산자를 사용하겠다'라고 알리는 것을 뜻한다. 정의된 연산자는 모듈 전역에서 사용된다.

 

/*
	코드 5-6. 전위 연산자 정의
*/

prefix operator **

 

[코드 5-6]처럼 연산자의 정의를 마치면, 어떤 데이터 타입에 이 연산자가 동작할 것인지 함수를 구현한다. 전위 연산자 함수를 구현할 때는 함수 func 키워드 앞에 prefix 키워드를 추가해준다.

/*
	코드 5-7. 전위 연산자 구현과 사용
*/

prefix operator **

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

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

print(sqrtMinusFive) // 25

 

스위프트 표준 라이브러리에 존재하는 전위 연산자에 기능을 추가할 때는 따로 연산자를 정의하지 않고 함수만 중복 정의하면 된다. ! 연산자를 문자열에도 사용하도록 추가하고자 한다. 문자열 앞에 사용하면 문자열이 비어있는지 확인하는 연산자로 사용하기 위해 함수를 중복 정의해준다.

/*
	코드 5-8. 전위 연산자 함수 중복 정의와 사용
*/

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

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

print(isEmptyString) // false

stringValue = ""
isEmptyString = !stringValue

print(isEmptyString) // true

 

5.3.2 후위 연산자 정의와 구현

[코드 5-10]에서 정수 타입의 값 뒤에 **를 붙이면 10을 더해주는 연산을 구현해보자. 후위 연산자의 함수 구현 앞에는 postfix라는 키워드를 붙여준다.

 

/*
	코드 5-10. 사용자 정의 후위 연산자 정의와 함수 구현
*/

postfix operator **

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

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

print(fivePlusTen) // 15

 

하나의 피연산자에 전위 연산과 후위 연산을 한 줄에 사용하게 되면 후위 연산을 먼저 수행한다. [코드 5-11]을 통해 전위 연산자와 후위 연산자를 한 번에 사용하게 되면 후위 연산을 먼저 실행한다는 것을 확인할 수 있다.

 

/*
	코드 5-11. 전위 연산자와 후위 연산자 동시 사용
*/

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)
}

 

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

lowerThan 속성에는 현재 모듈 밖에 정의된 우선순위 그룹만 명시할 수 있다.

 

결합방향을 명시해줄 수 있는 associativity에는 left, right, none을 지정해줄 수 있다. 만약 생략하면 기본적으로 none이 설정된다. 결합방향이 없는 연산자는 여러 번 연달아 사용할 수 없다. (ex. 1 < 2 < 3과 같은 모양으로 사용 불가)

 

연산자 우선순위 그룹의 assignment는 옵셔널 체이닝과 관련된 사항이다. 연산자가 옵셔널 체이닝을 포함한 연산에 포함되어 있을 경우 연산자의 우선순위를 지정한다. true로 설정해주면 해당 우선순위 그룹에 해당하는 연산자는 옵셔널 체이닝을 할 때 표준 라이브러리의 할당 연산자와 동일한 결합방향 규칙을 사용한다. 즉, 스위프트의 할당 연산자는 오른쪽 결합을 사용하므로 assignment를 true로 설정하면 연산자를 사용하여 옵셔널 체이닝을 할 때 오른쪽부터 체이닝이 시작된다는 뜻이다. 그렇지 않고 false를 설정하거나 assignment를 따로 명시해주지 않으면 해당 우선순위 그룹에 해당하는 연산자는 할당을 하지 않는 연산자와 같은 옵셔널 체이닝 규칙을 따른다. 즉, 연산자에 옵셔널 체이닝 기능이 포함되어 있다면 왼쪽부터 옵셔널 체이닝을 하게 된다.

 

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

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

[코드 5-12]에서 **를 중위 연산자로 사용하기 위해 정의해보자. 연산자의 이름은 **이고, MultiplicationPrecendence 연산자 우선순위 그룹에 속하게 된다.

 

/*
	코드 5-12. 중위 연산자의 정의
*/

infix operator **: MultiplicationPrecedence

 

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

 

/*
	코드 5-13. 중위 연산자의 구현과 사용
*/

// String 타입의 contains(_:) 메서드를 사용하기 위해 Foundation 프레임워크를 임포트한다.
import Foundation

infix operator **: MultiplicationPrecedence

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

let helloYagom: String = "Hello yagom"
let yagom: String = "yagom"
let isContainsYagom: Bool = helloYagom ** yagom   // true

 

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

 

/*
	코드 5-14. 클래스 및 구조체의 비교 연산자 구현
*/

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

 

[코드 5-14]의 사용자 정의 연산자는 전역함수로 구현했다. 그러나 특정 타입에 국한된 연산자 함수라면 그 타입 내부에 구현되는 것이 읽고 이해하기에 더욱 쉬울 것이다. 그래서 타입 내부에 타입 메서드로 구현할 수도 있다. [코드 5-15]에서 타입 메서드로 구현된 사용자 정의 연산자 함수를 확인해보자.

 

/*
	코드 5-15. 타입 메서드로 구현된 사용자 정의 비교 연산자
*/

class Car {
    var modelYear: Int?  // 연식
    var modelName: String? // 모델 이름
    
    // Car 클래스의 인스턴스끼리 == 연산했을 때 modelName이 같다면 true를 반환
    static func == (lhs: Car, rhs: Car) -> Bool {
        return lhs.modelName == rhs.modelName
    }
}

struct SmartPhone {
    var company: String? // 제조사
    var model: String? // 모델
    
    // SmartPhone 구조체의 인스턴스끼리 == 연산했을 때 model이 같다면 true를 반환
    static 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

 

 

스위프트 프로그래밍: Swift 5 - 교보문고

객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에! | 스위프트를 제대로 이해하고 싶은 개발자를 위한 책스위프트는 iOS와 macOS용 애플리케이션 개발에 주로 사용하는 프로그래밍 언어입니

www.kyobobook.co.kr

 

 

반응형

댓글