Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

고급 연산자 #64

Open
simoniful opened this issue Nov 9, 2022 · 0 comments
Open

고급 연산자 #64

simoniful opened this issue Nov 9, 2022 · 0 comments

Comments

@simoniful
Copy link
Owner

simoniful commented Nov 9, 2022

Basic Operators에서 설명한 연산자에 더해,
Swift는 더 복잡하게 값을 조작하는 여러 가지 고급 연산자를 제공한다
모든 종류의 비트 (bitwise) 및 비트 이동 (bit shifting) 연산자를 포함하는데 C와 Objective-C에서부터 익숙한 개념이다

C의 산술(arithmetic) 연산자와 달리, Swift의 산술 연산자는 기본적으로 값이 오버플로우되지 않는다
오버플로우 동작은 감지되어 에러라고 보고된다
오버플로우 동작을 직접 선택하려면, 오버플로우 덧셈 연산자(&+) 같이, 기본적으로 오버플로우 가능한 Swift의 두 번째 산술 연산자 집합을 사용한다
이러한 모든 오버플로우 연산자는 앰퍼샌드(&)로 시작한다

자신만의 구조체와, 클래스, 및 열거형을 정의할 땐,
해당 커스텀 타입에 자신만의 표준 스위프트 연산자를 구현하는 게 유용할 수 있다
스위프트는 해당 연산자들의 맞춤식 구현을 쉽게 제공하도록
그리고, 생성한 각 타입마다 이들의 정확한 동작이 무엇인지도 쉽게 결정하게 해준다

이미 정의된 연산자로 제한된 것도 아니다
Swift가 제공하는 유연성을 통해, 자신만의 우선 순위 및 결합 법칙 값을 가진,
커스텀 중위(infix)와, 접두사(prefix), 접미사(postfix), 및 할당(assignment) 연산자를 정의할 수 있다
이미 정의된 연산자 같이 해당 연산자를 코드에서 사용하고 채택할 수 있으며,
심지어 기존 타입을 확장하면 직접 정의한 자신만의 연산자도 지원할 수 있도록 구성 가능하다

비트 연산자 (Bitwise Operators)

비트 연산자(bitwise operators)는 자료 구조 안의 개별 원시 데이터 비트를 조작할 수 있게 한다
그래픽 프로그래밍과 장치 드라이버 생성 같은, 저-수준 프로그래밍에 주로 사용된다
커스텀 프로토콜로 통신하는 데이터의 부호화(encoding) 및 복호화(decoding) 같이,
외부 소스의 원시 데이터와 작업할 때도 비트 연산자가 유용할 수 있다

아래 설명 처럼, 스위프트는 C 에 있는 모든 비트 연산자를 지원한다

1) 비트 부정 연산자 (Bitwise NOT Operator)

비트 부정 연산자(bitwise NOT operator, ~)는 모든 수치 값 비트를 거꾸로 만든다

비트 부정 연산자는 접두사 연산자로, 어떤 공백도 없이, 연산 값 바로 앞에 둔다

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits  
// equals 11110000

UInt8 정수는 8비트로 0과 255 사이의 어떤 값이든 저장할 수 있다
예제는 이진 값 00001111로 UInt8 정수를 초기화하여,
첫 네 비트는 0으로 설정하고, 다음 네 비트는 1로 설정한다
부호없는 10진수 값 15와 같다

그런 다음 비트 부정 연산자로 invertedBits라는 새로운 상수를 생성하는데
initialBits 와 같지만 모든 비트가 거꾸로되게 된다
0은 1이 되고, 1은 0이 되며, invertedBits 의 값은 11110000다
부호없는 10진수 값 240과 같다

2) 비트 곱 연산자 (Bitwise AND Operator)

비트 곱 연산자 (bitwise AND operator; &) 는 두 수치 값 비트를 조합한다
입력 수치 값 비트가 둘 다 1 일 때만 비트 설정이 1인 새로운 수치 값을 반환하게 된다

아래 예제의, firstSixBits 와 lastSixBits 값은 둘 다 네 중간 비트가 1이다
비트 곱 연산자로 조합하면 00111100 인데, 부호없는 10진수 값 60과 같다

let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8  = 0b00111111
let middleFourBits = firstSixBits & lastSixBits  // equals 00111100

3) 비트 합 연산자 (Bitwise OR Operator)

비트 합 연산자 (bitwise OR operator, |) 는 두 수치 값 비트를 비교한다
연산자는 어느 입력 수치 값 비트가 1이면 비트 설정이 1인 새로운 수치 값을 반환한다

아래 예제의, someBits와 moreBits 값은 서로 다른 비트에 1 이 설정되어 있다
비트 합 연산자로 조합한 수치 값은 11111110으로, 부호없는 10진수 254와 같다

let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits  
// equals 11111110

4) 배타적 비트 합 연산자 (Bitwise XOR Operator)

배타적 비트 합 연산자 (bitwise XOR operator), 또는 “배타적 논리 합 연산자(^)”는, 두 수치 값 비트를 비교한다
‘배타적 논리 합 (exclusive OR)’ 에 대한 더 자세한 내용은,
위키피디아의 Exclusive or 항목과 배타적 논리합 항목 페이지를 통해 참고할 수 있다
연산자가 반환한 새 수치 값 비트는 입력 비트들이 서로 다르면 1로 설정하고 입력 비트가 같으면 0으로 설정한다

아래 예제의, firstBits와 otherBits 값은 각각 다른 위치에 1로 설정된 비트를 가진다
배타적 비트 합 연산자는 해당 비트 자리를 비교하여 입력 비트들이 서로 다르면 모두의 출력 값을 1로 설정한다
firstBits와 otherBits 안의 다른 동일한 모든 비트들의 출력 값은 0으로 설정한다

let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits  
// equals 00010001

5) 비트 왼쪽-이동 및 오른쪽-이동 연산자 (Bitwise Left and Right Shift Operators)

비트 왼쪽 이동 연산자 (bitwise left shift operator, <<)와 비트 오른쪽 이동 연산자 (bitwise right shift operator, >>)는 모든 수치 값 비트를,
아래 정의한 규칙에 따라, 특정 수의 자리만큼 왼쪽 또는 오른쪽으로 이동한다

비트를 왼쪽과 오른쪽으로 이동하는 건 정수를 2라는 인수(factor)로 곱하거나 나누는 효과가 있다
정수 비트를 왼쪽으로 한 위치 이동하면 자신의 값이 두 배가 되는 반면,
오른쪽으로 한 위치 이동하면 자신의 값이 반이 된다

‘인수(factor)’는 수학 용어로, ‘정수(integer)나 수식(equation)’ 을 몇 개의 곱으로 나타냈을 때, 각각의 구성 요소를 말한다
ex. ‘인수 분해 (factorization)’ 에서의 인수
‘인수(factor)’ 에 대한 더 자세한 정보는, 위키피디아의 Factor (mathematics) 항목과 인수 페이지에서 확인할 수 있다
‘인수’ 뿐만 아니라 약수 (divisor) 라는 개념과 함께 사용한다

5-1) 부호없는 정수의 이동 동작 (Shifting Behavior for Unsigned Integers)

부호없는 정수의 비트-이동 동작은 다음과 같다

    1. 기존 비트를 요청한 수의 위치만큼 왼쪽이나 오른쪽으로 이동한다
    1. 어떤 비트든 정수 저장 공간 경계를 넘으면 버린다
    1. 원본 비트가 왼쪽이나 오른쪽으로 이동한 후 남은 공간엔 0 을 집어 넣는다

해당 접근법을 논리적 이동 (logical shift)이라 한다

아래의 묘사는
(11111111 을 1 위치만큼 왼쪽 이동하는) 11111111 << 1 과,
(11111111 을 1 위치만큼 오른쪽 이동하는) 11111111 >> 1 의 결과를 보여준다
파란 숫자는 이동한 것이고, 회색 숫자는 버린 것이며, 빨간색 0은 추가된 것이다

Swift 코드의 비트 이동은 이렇게 보인다

let shiftBits: UInt8 = 4   // 00000100 in binary
shiftBits << 1             // 00001000
shiftBits << 2             // 00010000
shiftBits << 5             // 10000000
shiftBits << 6             // 00000000
shiftBits >> 2             // 00000001

비트 이동을 사용하면 다른 데이터 타입 안의 값을 부호화(encoding) 및 복호화(decoding) 할 수 있다

let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16    
// redComponent is 0xCC, or 204
// 110011000000000000000000 → 11001100
let greenComponent = (pink & 0x00FF00) >> 8   
// greenComponent is 0x66, or 102
// 110011000000000 → 1100110
let blueComponent = pink & 0x0000FF           
// blueComponent is 0x99, or 153
// 10011001

예제는 pink 라는 UInt32 상수를 사용하여 분홍색의 CSS 값을 저장한다
CSS 색상 값 #CC6699은 Swift의 16진수 표현법으론 0xCC6699 라고 작성한다
그런 다음 비트 곱 연산자(&)와 비트 오른쪽 이동 연산자(>>)로 해당 색상의 R(CC), G(66), 및 B(99) 성분을 분해한다

빨간색 성분은 수치 값 0xCC6699와 0xFF0000를 비트 곱하여 구한다
0xFF0000 안의 0이 0xCC6699의 두 번째와 세 번째 바이트를 “가린(mask)” 효과로,
6699는 무시하고 그 결과 0xCC0000만 남도록 만든다

그런 다음 이 수를 16자리만큼 오른쪽 이동(>> 16) 한다
16진수 안의 각 문자 쌍은 8비트를 사용해서, 오른쪽 16자리 이동은 0xCC0000 을 0x0000CC로 변환하게 된다
이는 0xCC와 똑같은 것으로, 10진수 값은 204다

녹색 성분은 수치 값 0xCC6699 와 0x00FF00를 비트 곱하여 구하며, 결과 값은 0x006600다
그런 다음 결과 값을 8자리만큼 오른쪽 이동하면, 0x66이라는 값을 주는데, 10진수 값은 102다

최종적으로, 파란색 성분은 수치 값 0xCC6699와 0x0000FF를 비트 곱하여 구하며, 결과 값은 0x000099다
0x000099는 이미 0x99와 같은, 10진수 값 153를 갖기 때문에, 이 값은 오른쪽 이동 없이 사용한다

5-2) 부호있는 정수의 이동 동작 (Shifting Behavior for Signed Integers)

부호있는 정수의 이동 동작은, 부호있는 정수를 2진수로 나타내는 방식 때문에, 부호없는 정수보다 더 복잡하다
아래 예제는 단순함을 위해 8비트 부호있는 정수로 하고 있지만, 어떤 크기의 부호있는 정수든 작용 원리는 동일하다

부호있는 정수는 부호 비트(sign bit)로 첫 번째 비트를 사용하여
정수가 양수인지 음수인지 지시한다
부호 비트가 0이면 양수를 의미하고, 부호 비트가 1이면 음수다

남은 값 비트(value bits)는 실제 값을 저장한다
양수의 저장 방식은 부호없는 정수와 정확히 똑같아서, 0부터 위로 센다
Int8 안에 수치 값 4가 있으면 비트는 이렇게 보인다

부호 비트는 0이며 “양수(positive)”를 의미하며, 일곱 개의 값 비트는 그냥 수치 값 4를, 2진 표기법으로 작성한 부분이다

하지만, 음수(negative)일 경우 다르게 저장한다. 2의 n 제곱에서 자신의 절대 값을 뺀 걸로 저장하는데,
여기서 n은 값 비트 개수가 된다
8비트 수엔 값 비트가 일곱 개 있으므로, 2의 7제곱 또는 128을 의미한다

Int8 안에 수치 값 -4가 있으면 비트가 이렇게 보인다

이번엔, 부호 비트가 1이며 “음수 (negative)”를 의미하며, 일곱 개의 값 비트엔 2진 값 124(128 - 4)가 있다

음수를 이렇게 부호화(encoding)하는 걸 "2의 보수(two’s complement) 표현법"이라 한다
해당 방식은 음수를 특이하게 나타내는 것 같지만, 여러가지 장점이 있다

2 의 n 제곱에서 자신의 절대 값을 뺀 걸 ‘2의 보수 (two’s complement)’ 라고 한다
2의 보수를 사용하면, 0의 표현 방식을 한 가지로 통일할 수 있으며, 사칙 연산도 자연스러워진다
2의 보수에 대한 더 자세한 정보는, 위키피디아의 Two’s complement 항목과 2의 보수 항목 페이지를 통해 알 수 있다

첫 번째는, -1 과 -4 를 더하는 걸,
단순히 모든 부호 비트를 포함한 여덟 비트의 표준 이진 덧셈을 하고나서
여덟 비트에 들지 않는 어떤 것이든 버림으로써 수행할 수 있다

두 번째로, 2의 보수 표현법은 음수 비트의 왼쪽 오른쪽 이동을 양수처럼 하게 해주며,
여전히 왼쪽 이동마다 두 배가 되고, 오른쪽 이동마다 절반이 된다
이를 달성하기 위해, 부호있는 정수의 오른쪽 이동 땐 부가 규칙을 사용하는데:
부호있는 정수를 오른쪽으로 이동할 땐, 부호없는 정수와 동일한 규칙을 적용하되,
왼쪽의 빈 자리를, 0보단, 부호 비트(sign bit)로 채운다

이런 행동은 부호있는 정수의 오른쪽 이동 후에도 부호가 동일하도록 보장하여, 산술 이동 (arithmetic shfit)이라고 한다

특수한 방식으로 양수와 음수를 저장하기 때문에, 오른쪽으로 이동하면 어느 것이든 더 0에 가까워지게 된다
이런 이동 중에 부호 비트를 동일하게 유지한다는 건, 자신의 값이 0에 가까워져도 음수는 음으로 남는다는 의미가 된다

오버플로우 연산자

수를 집어 넣으려는데 정수 상수나 변수가 그 값을 가질 수 없다면,
Swift는 기본적으로 무효한 값을 생성하기 보단 에러를 보고한다
해당 안전 장치는 너무 크거나 작은 수를 다룰 때 부가적인 안전성을 준다

예를 들어, Int16 정수 타입은 -32768 과 32767 사이의 어떤 부호있는 정수든 가질 수 있다
Int16 상수 또는 변수에 이 범위 밖의 수를 설정하려 하면 에러가 발생한다

var potentialOverflow = Int16.max
// potentialOverflow equals 32767, which is the maximum value an Int16 can hold
potentialOverflow += 1
// this causes an error

값이 너무 커지거나 작아질 때 에러 처리를 제공하면 경계 값 조건의 코딩 때 훨씬 더 많은 유연함을 준다

하지만, 특히 사용할 수치 비트만 오버플로우 조건으로 잘라내고 싶을 때,
에러 발동 보단 해당 동작을 직접 선택할 수 있다
Swift가 제공하는 세 개의 산술 오버플로우 연산자 (overflow operators)로
정수 계산에 대한 오버플로우 경우의 동작을 직접 선택할 수 있다
해당 연산자는 모두 앰퍼샌드(&)로 시작한다

  • 오버플로우 덧셈 (overflow addition, &+)
  • 오버플로우 뺄셈 (overflow subtraction, &-)
  • 오버플로우 곱셈 (overflow multiplication, &*)

1) 값 오버플로우 (Value Overflow)

수치 값은 양의 방향과 음의 방향 양쪽으로 넘칠 수 있다
부호 없는 정수가 오버플로우 덧셈 연산자 (&+) 로, 양의 방향 값 오버플로우를 허용할 때 발생하는 일에 대한 예제는 아래와 같다

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

unsignedOverflow 변수를 UInt8 가 가질 수 있는 최대 값으로 255, 또는 2진수 11111111 로 초기화한다
그런 다음 오버플로우 덧셈 연산자 (&+) 로 1 만큼 증가시킨다
이는 자신의 2진수 값을 UInt8이 가질 수 있는 크기 위로 밀어내서,
아래 도표에 보는 것처럼, 경계 너머로 오버플로우 되게한다
오버플로우 덧셈 후 UInt8 경계 안에 남는 값은 00000000, 또는 0이게 된다

부호 없는 정수가 음의 방향 오버플로우를 허용할 때도 이와 비슷한 뭔가가 발생한다
오버플로우 뺄셈 연산자(&-)를 사용한 예제는 아래와 같다

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

UInt8 이 가질 수 있는 최소 값은 0, 또는 2진수 00000000이다
오버플로우 뺄셈 연산자(&-)로 00000000에서 1을 빼면,
수치 값이 넘쳐서 11111111, 또는 10-진수 255 로 넘어가게(wrap around) 된다

컴퓨터 용어로 ‘wrap around’ 는 0, 1, 2 ... 9, 0, 1 ... 9, 0, ... 처럼
최대 값을 넘어선 수들이 다시 처음부터 되풀이되는 걸 말한다
‘wrap around’ 에 대한 더 자세한 정보는, 위키피디아의 Integer overflow 항목을 페이지를 통해 알 수 있다

부호 있는 정수도 오버플로우가 일어난다
모든 부호있는 정수의 덧셈과 뺄셈은,
비트 왼쪽-이동 및 오른쪽-이동 연산자(Bitwise Left and Right Shift Operators)에서 설명한 것처럼,
부호 비트도 덧셈 또는 뺄셈하는 수의 일부로 포함되는 비트 방식으로 수행된다

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

Int8 이 가질 수 있는 최소 값은 -128, 또는 2진수 10000000다
해당 2진수에 오버플로우 연산자로 1을 빼면
01111111 이라는 2진수 값을 주는데,
이는 부호 비트를 반전하고, Int8이 가질 수 있는 최대 양수 값인 127이 된다

부호 있는 정수 및 부호 없는 정수 둘 다,
양의 방향 값 넘침은 최대 유효 정수 값에서 최소 값으로 넘어가며,
음의 방향 값 넘침은 최소 값에서 최대 값으로 넘어가게 된다

우선권과 결합성 (Precedence and Associativity)

연산자 우선권(precedence)은 일부 연산자에 다른 것보다 더 높은 우선 순위를 주며 해당 연산자를 먼저 적용한다

연산자 결합성(associativity)은 동일 우선권의 연산자를 서로-왼쪽부터 그룹짓거나, 오른쪽부터 그룹지어-묶는 방법을 정의한다
이는 “자신의 왼쪽 표현식과 결합한다”, 거나 “자신의 오른쪽 표현식과 결합한다” 는 의미로 생각하면 된다

Swift의 ‘결합성 (associativity)’은 수학 분야에 있는 ‘결합 법칙 (associative law)’과 관련이 있다
결합 법칙에 대한 더 자세한 내용은, 위키피디아의 Associative property 항목과 결합법칙 항목 페이지에서 알 수 있다

각 연산자의 우선권과 결합성을 고려하는 건 복합 표현식의 계산 순서를 알아낼 때 중요하다
예를 들어, 연산자 우선권은 왜 다음 표현식이 17 인지 설명할 수 있다

2 + 3 % 4 * 5
// this equals 17

왼쪽에서 오른쪽으로 곧이곧대로 읽으면, 표현식 계산이 다음과 같다고 예상할 수도 있다

  • 2 더하기 3은 5
  • 5를 4로 나눈 나머지는 1
  • 1 곱하기 5는 5

하지만, 실제 답은 17이지 5가 아니다
더 높은-우선권인 연산자를 더 낮은-우선권인 것보다 먼저 평가한다
Swift에선, C처럼, 나머지 연산자(%) 와 곱하기 연산자(*) 의 우선 순위가 덧셈 연산자(+) 보다 더 높다
그 결과, 덧셈 전에 이 둘을 평가한다

하지만, 나머지와 곱셈의 우선권은 서로 동일하다
정확한 평가 순서를 알아 내려면, 그들의 결합성도 고려할 필요가 있다
나머지와 곱셈 둘 다 자신의 왼쪽 표현식과 결합한다
이는 표현식 주위에 왼쪽에서 시작하는 암시적 괄호를 추가한다고 생각하면 된다

2 + ((3 % 4) * 5)

(3 % 4)는 3이므로, 다음과 동일하다

2 + (3 * 5)

(3 * 5)는 15이므로, 다음과 동일하다

2 + 15

해당 계산이 내는 최종 답은 17이게 된다

Swift 표준 라이브러리가 제공한 연산자에 대한, 연산자 우선권 그룹과 결합성 설정의 완전한 목록을 포함한 정보는,
Operator Declaration(연산자 선언) 항목 페이지에서 더 알 수 있다

Swift 연산자 우선권과 결합성 규칙은 C 및 Objective-C 의 것보다 단순하여 더 예측하기 쉽다
하지만, 이는 C기반 언어의 것과 정확히 똑같지는 않다
기존 코드를 Swift로 바꿀 땐 연산자가 여전히 의도대로 상호 작용하도록 보장하는데 주의할 필요가 있다

연산자 메서드

클래스와 구조체는 기존 연산자에 자신만의 구현을 제공할 수 있다
이를 기존 연산자의 중복 정의(overloading)라고 한다

아래 예제는 자신의 구조체에 산술 덧셈 연산자(+)를 구현하는 방법을 보여준다
산술 덧셈 연산자는 연산 대상이 두 개이기 때문에 이항 연산자 (binary operator)이며
두 대상 사이에 있기 때문에 중위(infix) 연산자다

예제는 2차원 위치 벡터 (x, y)를 위한 Vector2D 구조체를 정의하고,
뒤이어 Vector2D 구조체의 인스턴스를 서로 더하는 연산자 메서드(operator method)를 정의한다

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

연산자 메소드는 Vector2D 에 대한 타입 메서드로 정의하며, 메서드 이름은 중복 정의한 연산자(+)와 일치한다
덧셈은 벡터의 핵심 동작이 아니기 때문에, 타입 메서드를 Vector2D 구조체의 주 선언부 보다는 Vector2D 의 익스텐션에서 정의한다
산술 덧셈 연산자가 이항 연산자이기 때문에, 해당 연산자 메서드는 Vector2D 타입의 입력 매개 변수는 두 개 취하고,
역시 Vector2D 타입인 단일 출력 값을 반환한다

해당 구현 안의 입력 매개 변수엔 left와 right라는 이름을 붙여서
+ 연산자의 왼쪽과 오른쪽에 있을 Vector2D 인스턴스를 나타낸다
메서드가 반환한 새로운 Vector2D 인스턴스의, x 와 y 프로퍼티는
두 Vector2D 인스턴스에 있는 x 와 y 프로퍼티를 서로 더한 합계로 초기화된다

타입 메서드를 기존 Vector2D 인스턴스 사이의 중위 연산자인 것처럼 사용할 수 있다

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 with values of (5.0, 5.0)

예제는 아래 묘사 처럼, 벡터 (3.0, 1.0)과 (2.0, 4.0)을 서로 더하여 벡터 (5.0, 5.0)을 만든다

1) 접두사 및 접미사 연산자 (Prefix and Postfix Operators)

앞선 예제는 자신만의 중위 이항 연산자를 실제로 구현해보았다
클래스와 구조체는 표준 단항 연산자 (unary operators)도 구현할 수 있다
단항 연산자는 단일 대상을 연산하는데 자신의 대상 앞에 -a 처럼 있으면 접두사(prefix) 연산자이고
자신의 대상 뒤에 b! 처럼 있으면 접미사(postfix) 연산자이다

단항 접두사나 단항 접미사 연산자를 구현할 때는
연산자 메서드 선언의 func 키워드 앞에 prefix 나 postfix 수정자를 작성해야한다

extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}

위 예제는 Vector2D 인스턴스의 단항 음수 연산자(-a)를 구현한다
단항 음수 연산자는 접두사 연산자라서, 해당 메서드를 prefix 수정자로 규명(qualified)해야 한다

‘규명(qualifed) 해야 한다’ 는 건 자신의 소속을 알려야 한다는 의미다
'규명하다'에 대한 더 자세한 내용은, 중첩 타입(Nested Types) 챕터의 중첩 타입 참조하기(Referring to Nested Types) 부분을 참고하자

단순 수치 값에선 단항 음수 연산자가 양수는 같은 절대 값의 음수로 변환하고 그 반대도 마찬가지다
Vector2D 인스턴스에 해당하는 구현은 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)

2) 복합 할당 연산자 (Compound Assignment Operators)

복합 할당 연산자 (compound assignment operators)는 할당 (=)을 다른 연산과 조합한다
예를 들어, 덧셈 할당 연산자 (+=) 는 덧셈과 할당을 단일 연산으로 조합한다
복합 할당 연산자의 왼쪽 입력 매개 변수 타입은 inout 으로 표시하는데,
매개 변수 값을 연산자 메서드 안에서 직접 수정하기 때문이다

아래 예제는 Vector2D 인스턴스의 덧셈 할당 연산자 메서드를 구현한다

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right
    }
}

덧셈 연산자는 앞서 정의했기 때문에, 덧셈 과정을 여기서 다시 구현할 필요는 없다
대신, 덧셈 할당 연산자 메서드는 기존 덧셈 연산자 메서드를 사용하여, 왼쪽 값과 오른쪽 값을 더한 걸 왼쪽 값으로 설정한다

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)

기본 할당 연산자(=) 를 중복 정의(overload)하는 건 불가능하다
복합 할당 연산자만 중복 정의할 수 있다
이와 비슷하게, 삼항 조건 연산자 (a ? : b : c) 도 중복 정의할 수 없다

3) 동등 비교 연산자 (Equivalence Operators)

기본적으로 커스텀 클래스와 구조체는 항등 연산자(==)와 부등 연산자(!=)라는, 동등 비교 연산자(euivalence operators)를 구현하지 않는다
대체로 == 연산자는 구현하고, != 연산자는 표준 라이브러리의 기본 구현을 써서 == 연산자의 결과를 반대로 뒤집는다
== 연산자 구현에는 두 가지 방법이 있는데
스스로 구현할 수도, 또는 많은 타입들에서, Swift에 통합 구현을 요청할 수도 있다
두 경우 모두, 표준 라이브러리의 Equatable 프로토콜을 준수하도록 추가하면 된다

== 연산자의 구현 방식은 다른 중위 연산자의 구현과 똑같다

extension Vector2D: Equatable {
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }
}

위 예제는 == 연산자를 구현하여 두 Vector2D 인스턴스가 가진 값이 같은 지를 검사한다
Vector2D 상황에선, “같음 (equal)”의 의미가 “인스턴스 둘 다 동일한 x 값과 y 값을 가진다” 고 고려하는 게 말이 되므로,
해당 논리를 연산자 구현에서도 사용한다

이제 이 연산자를 사용하여 두 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."

Adopting a Protocol Using a Synthesized Implementation에서 설명한 것처럼,
수많은 단순한 경우에, Swift 동등 비교 연산자의 통합 구현을 제공하도록 요청할 수 있다

커스텀 연산자

Swift가 제공하는 표준 연산자에 더해 자신만의 커스텀 연산자를 선언하고 구현할 수 있다
자신만의 연산자 정의에 사용할 수 있는 문자 목록은, Operators 페이지를 참고하면 된다

새로운 연산자는 전역 수준에서 별도로 operator 키워드로 선언하며, prefix나, infix 또는 postfix 수정자를 표시한다

prefix operator +++

위 예제는 +++ 라는 새로운 접두사 연산자를 정의한다
해당 연산자는 기존의 Swift에선 제공하지 않는 것이라서,
Vector2D 인스턴스와 작업하는 특정 상황 하에서만 자신만의 의미를 가지게 된다
예제 용으론, +++ 를 새로 “두 배로 만드는 접두사(prefix doubling)” 연산자로 정의해보자
이는, 앞서 정의한 덧셈 할당(+=) 연산자로 벡터에 자신을 더하여, Vector2D 인스턴스의 x 와 y 값을 두 배로 만든다
+++ 연산자를 구현하려면, 다음 처럼 Vector2D 에 +++ 라는 타입 메서드를 추가해야한다

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)

1) 커스텀 중위 연산자의 우선권 (Precedence for Custom Infix Operators)

커스텀 중위 연산자 각각은 우선권 그룹에 속하게 된다
우선권 그룹은 다른 중위 연산자와 상대적인 연산자의 우선권 뿐만 아니라 연산자의 결합성도 지정한다
Precedence and Associativity을 보면 이러한 성질이 중위 연산자와 다른 중위 연산자의 상호 작용에 영향을 주는 방법을 설명한다

커스텀 중위 연산자에 우선권 그룹을 명시하지 않으면, 기본 우선권 그룹으로 삼항 조건 연산자 바로 위의 우선권을 가진다

다음 예제는 +- 라는 새로운 커스텀 중위 연산자를 정의하는데,
이는 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 값은 첫 번째에서 두 번째 벡터 걸 뺀다
이는 본질적으로 “덧셈류 (additive)” 연산자이기 때문에, + 와 - 같은 덧셈류 중위 연산자와 동일한 우선권 그룹을 설정한다
Swift 표준 라이브러리가 제공하는 연산자 우선권 그룹 및 결합성 설정에 대한 완전한 목록을 포함하는 연산자 정보는,
Operator Declarations 항목 페이지에서 확인할 수 있다
우선권 그룹에 대한 더 많은 정보와 자신만의 연산자 및 우선권 그룹 정의 구문을 보려면,
Operator Declaration 항목 페이지에서 확인할 수 있다

접두사나 접미사 연산자를 정의할 땐 우선권을 지정하지 않는다
하지만, 동일한 피연산자에 접두사와 접미사 연산자를 둘 다 적용한다면 접미사 연산자가 먼저 적용된다

Result Builders

Result Builders는 자연스러운 선언형 방식으로,
리스트나 트리 같은 중첩 데이터 생성 구문을 추가하는 사용자가 정의하는 타입이다
Result Builders를 사용한 코드는 if와 for 같은 평범한 스위프트 구문을 포함해서,
조건이나 데이터 조각의 반복을 처리할 수 있다

아래 코드는 한 줄 위에 별과 글로 그림을 그리는 몇 가지 타입을 정의한다

protocol Drawable {
    func draw() -> String
}

struct Line: Drawable {
    var elements: [Drawable]
    func draw() -> String {
        return elements.map { $0.draw() }.joined(separator: "")
    }
}

struct Text: Drawable {
    var content: String
    init(_ content: String) { self.content = content }
    func draw() -> String { return content }
}

struct Space: Drawable {
    func draw() -> String { return " " }
}

struct Stars: Drawable {
    var length: Int
    func draw() -> String { return String(repeating: "*", count: length) }
}

struct AllCaps: Drawable {
    var content: Drawable
    func draw() -> String { return content.draw().uppercased() }
}

Drawable 프로토콜은 선이나 도형 같이 그릴 것에 대한 요구 사항을 정의하는데
프로토콜을 준수하는 타입은 반드시 draw() 함수를 구현해야 한다
Line 구조체는 단 한 줄짜리 그림을 나타내며, 대부분의 그림에서 최-상단 컨테이너 역할을 한다

여기서의 ‘컨테이너(container)’는 자료 구조 타입을 의미하는데
예제에 있는 List 구조체도 그리기 가능한 원소들을 [Drawable] 처럼 배열로 담고 있다
컨테이너에 대한 더 자세한 정보는, 위키피디아의 Container (abstract data type) 항목 페이지에서 알 수 있다

Line을 그리고자, 구조체는 각 줄(line) 성분의 draw() 를 호출한 다음, 결과 문자열을 단일 문자열로 이어붙인다
Text 구조체는 문자열을 포장하여 그림으로 만든다
AllCaps 구조체는 또 다른 그림을 포장 및 수정하는데,
그림 안의 어떤 문장이든 대문자로 변환한다

해당 타입들의 이니셜라이저를 호출하면 그림을 만드는게 가능하다

let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
    Stars(length: 3),
    Text("Hello"),
    Space(),
    AllCaps(content: Text((name ?? "World") + "!")),
    Stars(length: 2),
    ])
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"

코드는 작동하지만 조금 어색하다
AllCaps 뒤에 깊게 중첩된 괄호는 이해하기가 힘들다
name이 nil 일 땐 “World” 를 사용하라는 대체 논리는 ?? 연산자를 써서 인라인으로 작성해야 하는데, 어떤 더 복잡한 방법을 쓰든 어려워진다
switch 나 for 반복문을 포함하여 그림을 제작할 경우가 있어도 그렇게 할 수 있는 방법이 없다
Result Builders는 이와 같은 코드를 재작성하여 보통의 Swift 코드 같이 보이게 해준다

ResultBuilder를 정의하려면, 타입 선언에 @resultBuilder 특성을 작성한다
예를 들어, 다음 코드는 DrawingBuilder라는 ResultBuilder를 정의하여, 선언형 구문으로 그림을 설명하게 해준다

@resultBuilder
struct DrawingBuilder {
    static func buildBlock(_ components: Drawable...) -> Drawable {
        return Line(elements: components)
    }

    static func buildEither(first: Drawable) -> Drawable {
        return first
    }

    static func buildEither(second: Drawable) -> Drawable {
        return second
    }
}

DrawingBuilder 구조체는 resultBuilder 구문의 각 부분들을 구현하는 세 메서드를 정의한다
buildBlock(_:) 메서드는 코드 블럭에 연속된 줄의 작성을 지원한다
해당 메서드는 그 블럭 안의 성분들을 하나의 Line으로 조합한다
buildEither(first:)와 buildEither(second:) 메소드는 if-else 문을 지원한다

함수 매개 변수에 @DrawingBuilder 특성을 적용하면,
함수에 전달한 클로저를 해당 클로저를 써서 결과 제작자가 생성한 값으로 바꿀 수 있다
예를 들면 다음과 같다

func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return content()
}
func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return AllCaps(content: content())
}

func makeGreeting(for name: String? = nil) -> Drawable {
    let greeting = draw {
        Stars(length: 3)
        Text("Hello")
        Space()
        caps {
            if let name = name {
                Text(name + "!")
            } else {
                Text("World!")
            }
        }
        Stars(length: 2)
    }
    return greeting
}

let genericGreeting = makeGreeting()
print(genericGreeting.draw())
// Prints "***Hello WORLD!**"

let personalGreeting = makeGreeting(for: "Ravi Patel")
print(personalGreeting.draw())
// Prints "***Hello RAVI PATEL!**"

makeGreeting(for:) 함수는 name 매개 변수를 취하여 이를 사용하여 개인별 인사말을 그린다
draw(_:) 와 caps(_:) 함수는 둘 다 자신의 매개 변수로 단일 클로저를 취하며,
이를 @DrawingBuilder 특성으로 표시한다
해당 함수들을 호출할 땐, DrawingBuilder가 정의한 특수 구문을 사용한다

makeGreeting 함수 안에서 draw { ... } 부분과 caps { ... } 부분이 이 함수들을 호출하는 부분이며,
이 때 DrawingBuilder 가 정의한 특수 구문을 사용하게 된다

Swift는 선언형 그림 설명을 DrawingBuilder에 있는 메서드의 연속 호출로 변형하여 함수 인자로 전달한 값을 제작한다.
예를 들어, 위 예제 안의 makeGreeting 함수 안에서 caps(_:) 호출 코드를 다음 같이 변형한다

let capsDrawing = caps {
    let partialDrawing: Drawable
    if let name = name {
        let text = Text(name + "!")
        partialDrawing = DrawingBuilder.buildEither(first: text)
    } else {
        let text = Text("World!")
        partialDrawing = DrawingBuilder.buildEither(second: text)
    }
    return partialDrawing
}

Swift는 if-else 블럭을 buildEither(first:)와 buildEither(second:) 메서드 호출로 변형한다
자신의 코드에서 해당 메서드들을 호출하지 않긴 하지만,
변형 결과를 보는 건 DrawingBuilder 구문을 사용할 때의 Swift 코드 변형 방법을 더 알아보기 쉽게 한다

특수 그림 구문이 for 반복문 작성을 지원하게 하려면, buildArray(_:) 메소드를 추가하면 된다

extension DrawingBuilder {
    static func buildArray(_ components: [Drawable]) -> Drawable {
        return Line(elements: components)
    }
}

let manyStars = draw {
    Text("Stars:")
    for length in 1...3 {
        Space()
        Stars(length: length)
    }
}
// Prints "Stars: * ** ***"

위 코드에서, for 반복문은 그림 배열을 생성하며, buildArray(_:) 메소드가 그 배열을 Line 으로 바꾼다

builder 구문을 builder 타입의 메서드 호출로 Swift가 변형하는 방법에 대한 완전한 목록은, resultBuilder 페이지에서 확인할 수 있다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant