Apple 제공 Swift 프로그래밍 가이드(3.1)의 Advanced Operators 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
Swift는 복잡한 연산을 위한 몇 가지 고급 연산자들을 제공한다. 예를들면 bitwise 와 bit shifting 연산자들이다.
C의 산술 연산자와 다르게 Swift의 산술 연산자는 오버플로우가 되지 않는다. 오버플로우가 될 만한 상황이면 에러를 뱉어낸다. 산술 연산에서의 오버플로우를 굳이 사용하고 싶다면 Swift의 산술 연산자 second set을 사용하도록 하자. 기본 산술 연산자 앞에 & 기호를 붙이는 방식인데, 예를들어 덧셈에서의 오버플로우를 허용하고 싶다면 &+ 연산자 (overflow addition operator 라고 부른다) 를 쓰자.
필요 시 Swift의 Standard 연산자를 자신이 직접 구현한 것으로 대체할 수 있으며 확장을 시킬 수도 있다. 또한 자신이 직접 Infix, Prefix, Postfix, assignment 연산자를 정의할 수도 있다.
Bitwise Operators
비트 연산자Bitwise Operator는 Data structure 안에서 Individual raw data bit를 조작할 때 사용한다. Low-level 프로그래밍을 할 때 이런 비트 연산자를 사용할 일이 종종 있는데, 예를들어 그래픽 프로그래밍이나 디바이스 드라이버를 만들 때이다. 또한 외부 소스의 raw data를 가지고 작업할 때도 유용할 수 있다.
Swift는 C에서 사용하던 모든 비트 연산자를 제공한다. 지금부터 하나씩 살펴보자.
<Bitwise NOT Operator>
표기 : ~
기능 : Number의 모든 비트를 반전시킨다.
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // equals 11110000
UInt8은 8개의 비트를 가지며 0부터 255까지의 값을 저장할 수 있다. 예제에서의 initialBits는 00001111 라는 바이너리 값을 가진다. (십진수로는 15) 이 initialBits에 bitwise NOT 을 하면 0이 1로, 1이 0으로 반전되어 11110000이 된다. (십진수로는 240)
<Bitwise AND Operator>
표기 : &
기능 : 두 Number를 AND 연산으로 결합한다. (비트를 각각 보면서 두 비트가 모두 1일 때만 1, 그렇지 않으면 0을 셋팅)
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits
// middleFourBits equals 00111100
<Bitwise OR Operator>
표기 : I
기능 : 두 Number를 OR 연산으로 결합한다. (비트를 각각 보면서 두 비트 중 하나라도 1이면 1, 그렇지 않으면 0을 셋팅)
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits
// equals 11111110
표기 : ^
기능 : 두 Number를 Exclusive OR 연산으로 결합한다. (비트를 각각 보면서 두 비트가 다른 값이면 1, 같은 값이면 0을 셋팅)
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits
// equals 00010001
<Bitwise Left and Right Shift Operators>
표기 : Left shift는 << , Right shift는 >>
기능 : Number의 모든 비트를 특정 횟수만큼 옮긴다. Left shift면 왼쪽으로, Right shift면 오른쪽으로.
Bitwise left & right shift 연산자는 2의 인수로 곱하거나 나누는 것과 같다. Left shift를 1 하면 곱하기 2를 한 것과 같고, Right shift를 1 하면 나누기 2를 한 것과 같다.
1. Shifting Behavior for Unsigned Integers
부호가 없는 정수들은 logical shift의 규칙을 따른다.
비트들은 요구된 횟수만큼 왼쪽 또는 오른쪽으로 옮겨진다.
옮긴 후 정수의 저장공간을 넘어선 비트들은 버려진다.
옮긴 후 비어있는 자리에는 0이 삽입된다.
let shiftBits: UInt8 = 4 // 00000100 in binary
shiftBits << 1 // 00001000
shiftBits << 2 // 00010000
shiftBits << 5 // 10000000
shiftBits << 6 // 00000000
shiftBits >> 2 // 00000001
다른 데이터 타입으로 인코딩 또는 디코딩하기 위해 bit shifting을 이용할 수도 있다.
let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16
// redComponent is 0xCC, or 204
let greenComponent = (pink & 0x00FF00) >> 8
// greenComponent is 0x66, or 102
let blueComponent = pink & 0x0000FF
// blueComponent is 0x99, or 153
2. Shifting Behavior for Signed Integers
부호가 있는 정수들은 binary로 표현될 때 맨 첫번째 bit가 sign bit로 쓰인다. sign bit는 이 정수가 양수인지 음수인지 구분하기 위한 용도다. 0이라면 양수, 1이라면 음수다.
맨 첫번째 bit (sign bit) 를 제외한 나머지 bit 들은 value bit 들이다. 여기에 실제 값이 저장된다. 양수의 경우 value bit 들에 unsigned integer 값이 그대로 저장된다. 숫자 4 를 Int8로 표현한 예제를 살펴보자.
sign bit 는 0이고 (양수임을 나타냄) 나머지 value bits 에는 4가 저장되어 있다. (이진수 표기법) 양수의 표현은 자연스럽게 이해할 수 있다.
그러나 음수의 경우에는 value bits 의 저장방식이 양수와 다르다. value bit 의 개수가 n개라고 할 때, 2의 n승에서 표현할 값의 절대값을 뺀 값이 저장된다. 예제를 통해 살펴보자. 숫자 -4를 Int8로 표현하는 예제이다.
우선 sign bit 는 음수이기 때문에 1이다. 그러나 나머지 7개의 value bit 들에는 124가 저장되어 있다. 위에서 말했듯이 음수의 value bit에는 "value bit의 개수가 n개일 때, 2의 n승 - 표현할 값의 절대값" 이 저장되기 때문이다. 예제의 경우 2의 7승은 128이고, 128에서 4를 뺀 값이 124이다.
이러한 음수의 인코딩 방법을 2의 보수 표현법two’s complement representation이라고 한다. 굳이 이런 표현법을 사용하는 데에는 몇 가지 이유가 있다. 2의 보수 표현법이 가진 장점을 두 가지 소개한다.
첫 번째로, 음수끼리 덧셈 연산을 할 때, 단순히 모든 8개 bit를 더하기만 하면 된다. (sign bit 포함) 전부 더한 다음 8개 bit를 벗어나는 bit는 무시해버리면 된다. -1과 -4를 더하는 예제를 보자.
두 번째로, 2의 보수 표현법 또한 모든 비트를 특정 횟수만큼 옮기는 것으로 2의 진수로 곱하거나 나누는 효과를 낼 수 있다. (Bitwise shift left & right) Shift right 연산을 할 때 양수의 경우에는 비어있는 자리에 0이 삽입되었지만 음수의 경우에는 sign bit로 채워진다. 양수의 경우 sign bit는 0이기 때문에 그냥 비어있는 자리는 sign bit로 채워진다고 생각하면 된다.
이러한 동작 덕분에 음수를 shift left & right 해도 여전히 음수임이 보장되며, 양수와 음수 둘 다 shift right를 할수록 0에 가까워지게 된다. 참고로 이러한 Shift 연산을 arithmetic shift 라고 한다.
Overflow Operators
정수형 상수나 변수에 저장범위를 넘어선 값을 넣으려고 하면 Swift에서는 에러가 발생된다. Swift에서는 디폴트로 오버플로우를 허용하지 않는다는 것에 주의하자. 이런 특성은 아주 크거나 아주 작은 수를 다룰 때 안전성을 제공할 것이다.
예를 들어보자. Int16 타입은 -32768과 32767 사이의 Integer 값을 저장할 수 있다. Int16 타입으로 생성된 상수나 변수에 이 범위를 넘어선 값을 저장하려고 하면 다음과 같이 에러가 발생된다.
var potentialOverflow = Int16.max
// potentialOverflow equals 32767 (max value)
potentialOverflow += 1
// this causes an error
오버플로우 에러를 처리handling하면, 너무 크거나 너무 작은 수의 경계값 조건을 다루는 것에 있어서 좀 더 유연한 코드를 작성할 수 있을 것이다.
그러나 경우에 따라 오버플로우 발생이 필요할 수도 있을 것이다. 이럴 때 쓰이는 것이 Overflow Operator 이다. 다음 세 가지 산술 오버플로우 연산자를 사용하면 오버플로우가 발생하는 상황에서 에러가 발생하지 않는다.
Overflow addition (&+)
Overflow subtraction (&-)
Overflow multiplication (&*)
Value Overflow
숫자는 양수 방향으로도 음수 방향으로도 오버플로우가 가능하다.
예제를 보자. Unsigned Integer 타입의 변수에서 양수 방향으로 오버플로우가 발생했지만, overflow addition operator (&+) 를 통해서 오버플로우가 허용된 경우이다.
var unsignedOverflow = UInt8.max
// unsignedOverflow equals 255
// which is the maximum value a UInt8 can hold
unsignedOverflow = unsignedOverflow &+ 1
// unsignedOverflow is now equal to 0
// (에러 발생하지 않음)
최대값이 들어있던 변수에 &+ 연산자로 1이 더해질 경우, 아래 그림처럼 동작하게 된다. Unsigned Integer, 즉 부호가 없기 때문에 이 변수는 8 bit 를 전부 값 저장에 사용한다. 여기에 1이 더해지면 8 bit가 전부 0이 되고, 범위를 넘은 bit가 1이 된다. 이때 범위를 넘은 bit는 그냥 무시된다. 따라서 &+ 1 을 하면 이 변수의 값은 0가 된다. (그냥 + 1 을 하면 오버플로우 에러가 발생함에 유의하자,)
Unsigned Integer가 &- 연산자를 통해 음수 방향으로 오버플로우가 발생했을 때도 이와 비슷한 동작을 하게 된다.
var unsignedOverflow = UInt8.min
// unsignedOverflow equals 0
// which is the minimum value a UInt8 can hold
unsignedOverflow = unsignedOverflow &- 1
// unsignedOverflow is now equal to 255
// (에러 발생하지 않음)
최소값인 0이 들어있는 변수는 이진법으로 나타내면 00000000 이다. 여기서 &- 연산자를 통해 1을 빼면 아래 그림처럼 동작하게 된다. 결과는 11111111, 즉 십진수로는 255가 된다. (일반적인 - 연산자를 사용할 경우 오버플로우 에러가 나는 것에 유의하자.)
이제 Signed Integer, 즉 부호가 있는 정수에서 오버플로우 연산자를 쓰면 어떻게 되는지 살펴보자. 앞에서 설명한 것처럼 Signed 의 경우에는 맨 첫번째 bit 가 signed bit (양수, 음수 구분) 로 사용되고 나머지 bit들이 값을 저장하는 데에 쓰인다. 또한 Signed Integer에서 숫자를 더하고 뺄 때는 signed bit까지 포함이 되어서 연산이 된다. Bitwise Left & Right Shift 연산에서 설명한 부분이다.
var signedOverflow = Int8.min
// signedOverflow equals -128
// which is the minimum value an Int8 can hold
signedOverflow = signedOverflow &- 1
// signedOverflow is now equal to 127
// (부호가 바뀌었다!)
음수였던 변수가 &- 1 이 되면서 양수가 된 것을 볼 수 있다. 아래 그림을 보면 이해가 빠르다. signed bit가 1에서 0이 되었기 때문에 이런 현상이 발생한 것이다.
이처럼 Signed / Unsigned Integer 둘 다 양수 방향으로 오버플로우 되면 maximum 값에서 minimum 값이 되고, 음수 방향으로 오버플로우 되면 minimum 값에서 maximum 값이 된다. 물론 이것은 오버플로우 연산자 등을 사용하여 오버플로우가 허용될 경우에 한해서 적용되는 말이다.
우선순위와 결합성Precedence and Associativity
* 자주 사용하는 연산자들의 우선순위 규칙은 일반적으로 적용되는 규칙과 같습니다. 이 부분의 설명은 생략하고 예제로 대신하겠습니다.
2 + 3 % 4 * 5
// 2 + ((3 % 4) * 5)
// 2 + (3 * 5)
// 2 + 15
// this equals 17
Swift의 연산자 우선순위와 결합성에 대한 전체 규칙은 Expressions을 참조할 것. 또한 Swift Standard Library가 제공하는 연산자에 대한 자세한 설명은 Swift Standard Library Operators Reference 을 참조할 것.
<NOTE> Swift의 연산자 우선순위와 결합성 규칙은 C와 Objective-C보다 간단하며 예측이 쉽다. 그러나 이 말은 C-based 언어와 완전히 같은 규칙이 아니라는 말이다. 따라서 기존 C-based 코드를 Swift로 포팅할 때는 기존에 의도한 대로 연산이 되는지 확인할 필요가 있다.
Operator Methods
클래스와 구조체는 기존의 연산자(ex: +, - 등)를 오버로딩하여 자체적인 구현을 제공할 수 있다. 이 오버로딩 메서드를 오버로딩 메서드Operator Method라고 부른다.
예제를 보자. 한 커스텀 구조체에서 arithmetic addition operator (+) 를 오버로딩하여 자체적인 구현을 제공하고 있다.
<참고> + 연산자는 2개의 타겟을 대상으로 하는 연산자이기 때문에 이진 연산자binary operator이고, 2개의 타겟 가운데에 위치하기 때문에 infix 이다.
예제에는 2차원 벡터 (x, y) 를 위한 커스텀 구조체 Vector2D가 정의되어 있다. Vector2D 구조체는 자신의 인스턴스들이 + 연산자를 통해 쉽게 더해질 수 있도록 + 연산자를 오버로딩하고 있다. 코드를 보자.
struct Vector2D {
var x = 0.0, y = 0.0
}
extension Vector2D {
static func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
}
이 Operator method에 대해 아래와 같은 점들을 살펴볼 수 있다.
Vector2D의 타입 메서드로 정의되어있다.
+ 연산자 오버로딩을 위해 메서드 이름은 + 이다.
Vector2D 구조체에서 + 연산은 필수 동작이 아니기 때문에 Extension 을 통해 정의하였다.
+ 는 binary operator이기 때문에 2개의 input parameter 를 받고 1개의 output value를 리턴한다. 전부 Vector2D 타입이다.
구현을 자세히 살펴보면, left 와 right 라는 이름의 input parameter 들의 x좌표와 y좌표 값을 각각 더하여 새로운 Vector2D 인스턴스를 생성하고, 그것을 리턴하고 있다.
이렇게 만든 Operator method는 Vector2D 타입의 인스턴스 2개를 더하는 infix operator로 사용할 수 있다. 사용예제를 보자.
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector is a Vector2D instance (5.0, 5.0)
이 예제를 그림으로 나타내보면 아래와 같다. 벡터 (3.0, 1.0) 과 벡터 (2.0, 4.0) 을 더하여 벡터 (5.0, 5.0) 이 된다.
Prefix and Postfix Operators
위에서 살펴본 예제는 2개의 target에 적용되는 infix operator 에 해당하는 Operator method 였다. 이제 single target 에 적용되는 Unary operator 에 해당하는 Operator method 들도 살펴보자. Unary operator의 종류에는 target의 앞에 붙는 Prefix operator (ex: -a) 와 target의 뒤에 붙는 Postfix operator (ex: b!) 가 있다. Prefix operator 를 구현할 때는 prefix 를, Postfix operator를 구현할 때는 postfix 를 func 키워드 앞에 붙여야 한다. 예제를 보자.
extension Vector2D {
static prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
}
Vector2D 인스턴스를 위한 unary minus operator (-a) 를 구현하고 있다. 이제 Vector2D 인스턴스에 대고 이 Operator 를 사용하면 x 와 y 값 각각의 부호가 바뀌게 된다.
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative is a Vector2D instance
// with values of (-3.0, -4.0)
let alsoPositive = -negative
// alsoPositive is a Vector2D instance
// with values of (3.0, 4.0)
Compound Assignment Operators
이제 Compound assignment operator (복합 할당 연산자) 의 경우에는 어떻게 Operator method 를 구현하는지 알아보자. 일단 Compound assignment operator 라는 것은 assignment (=) 와 다른 operator 를 결합한 것인데, 예를 들면 += 같은 것이다. 이것 역시 2개의 target을 가지는데, Operator method 내부에서 Left parameter의 값이 변경되어야 하기 때문에 Left input parameter의 타입을 inout으로 만들어야 한다. 예제를 보자.
extension Vector2D {
static func += (left: inout Vector2D, right: Vector2D) {
left = left + right
}
}
Addition operator (+) 는 앞선 예제에서 이미 따로 구현했기 때문에 또 구현할 필요는 없다.
+= 연산자를 Vector2D에 대해 따로 구현했기 때문에 이제 아래와 같이 편하게 사용할 수 있다.
var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original now has values of (4.0, 6.0)
<NOTE> default assignment operator (=) 를 오버로드 하는 것은 불가능하다. 또한 ternary conditional operator (a ? b : c) 역시 오버로드 할 수 없다.
Equivalence Operators
커스텀 클래스와 구조체는 equivalence operator (==, !=) 의 기본 구현이 적용되지 않는다. "equal to"와 "not equal to"를 판단할 기준이 명백하지 않기 때문이다.
그러나 커스텀 타입의 경우에도 equivalence operator 를 따로 구현해 놓는다면 다른 타입과 마찬가지로 ==, != 를 이용해 편리하게 비교할 수 있게 된다. 예제를 보자.
extension Vector2D {
static func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
static func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}
}
Vector2D 인스턴스 2개가 서로 같은 경우는 "두 개의 x와 y값이 같을 때"가 되어야 할 것이다. 예제를 보면 그것이 "equal to"에 대한 로직으로 사용되고 있다. "not equal to"는 단순히 "equal to"의 반대를 리턴하도록 하고있다.
이제 == 와 != 를 통해 두 개의 Vector2D 인스턴스가 동일한지 비교할 수 있다.
let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
print("These two vectors are equivalent.")
}
// Prints "These two vectors are equivalent."
Custom Operators
Swift 가 제공하는 standard operator 외에도 자신만의 custom operator를 선언하고 구현할 수 있다. custom operator 로 사용할 수 있는 문자 리스트를 보려면 Operators를 참조하라.
Custom operator 선언은 전역 레벨에서 operator 키워드를 통해서 하면 된다. 또한 prefix / infix / postfix 를 표시해주어야 한다.
prefix operator +++
위와 같이 정의함으로써 이제 +++ 연산자를 사용할 수 있다. 기존에는 Swift에 없던 연산자이다. Vector2D 인스턴스에 대해 이 연산자를 사용하면 x와 y값을 2배로 만들어주는 구현을 해보자.
extension Vector2D {
static prefix func +++ (vector: inout Vector2D) -> Vector2D {
vector += vector
return vector
}
}
var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled now has values of (2.0, 8.0)
// afterDoubling also has values of (2.0, 8.0)
Addition operator (+) 는 앞선 예제에서 이미 따로 구현했기 때문에 또 구현할 필요는 없다.
Precedence for Custom Infix Operators
Custom infix operator 들을 어느 Precedence group(우선순위 그룹)에 속하게 할지 결정할 수 있다. Precedence group은 다른 infix operator들과의 우선순위를 결정할 때 관여하게 되는 것인데, 예를 들어 +와 *가 둘 다 포함된 수식이 있다면 * 연산자가 속한 Precedence group이 더 높은 우선순위를 가지기 때문에 *를 먼저 계산하게 되는 것이다.
Precedence group을 따로 명시하지 않은 custom infix operator 의 경우에는 default precedence group에 속하게 된다. (삼항 연산자 바로 위의 우선순위를 가진다)
예제를 하나 보자. custom infix operator +- 를 하나 정의하고 이것을 precedence group 중 AdditionPrecedence 에 속하게 하였다.
infix operator +-: AdditionPrecedence
extension Vector2D {
static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y - right.y)
}
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector is a Vector2D instance
// with values of (4.0, -2.0)
새롭게 정의한 +- 연산자의 역할은 두 벡터 인스턴스 사이에서 x 값은 더하고 y 값은 빼는 것이다. 따라서 결국은 가감에 관련된 연산자이기 때문에 +, - 연산자와 같은 precedence group에 속하게 하였다.
선택할 수 있는 Operator precedence group 의 모든 리스트와 연관성 설정에 대해 알아보려면 Swift Standard Library Operators Reference 를 참조하라.
Precedence group에 대한 문법이나 정의방법 등을 알아보려면 Operator Declaration 를 참조하라.
<NOTE> prefix , postfix operator 를 정의할 때는 따로 우선순위를 정하지 않지만, 만약 하나의 피연산자에 prefix와 postfix를 둘 다 적용할 경우에는 postfix operator가 우선된다.
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Access Control (2) | 2017.04.01 |
---|---|
Generics (0) | 2017.03.29 |
Protocols (3) (2) | 2017.03.25 |
Protocols (2) (0) | 2017.03.25 |
Protocols (1) (0) | 2017.03.23 |