기초 연산자

기초 연산자

할당, 산술, 비교 같은 연산을 수행하는 방법을 다뤄요.

*연산자(operator)*는 값을 확인하거나, 바꾸거나, 결합할 때 쓰는 특별한 기호나 표현이에요. 예를 들어 덧셈 연산자(+)는 두 숫자를 더하죠. let i = 1 + 2처럼요. 그리고 논리 AND 연산자(&&)는 두 불리언 값을 결합해요. if enteredDoorCode && passedRetinaScan처럼요.

Swift는 C 같은 언어에서 이미 익숙한 연산자들을 지원하면서도, 몇 가지 기능을 개선해서 흔한 코딩 오류를 없애 줘요. 할당 연산자(=)는 값을 반환하지 않아요. 그래서 ==(같다)를 써야 할 자리에 =를 실수로 쓰는 걸 막아 주죠. 산술 연산자(+, -, *, /, % 등)는 값 오버플로(overflow)를 감지해서 허용하지 않아요. 저장하는 타입의 허용 범위보다 숫자가 커지거나 작아질 때 뜻밖의 결과가 나오는 걸 피하려는 거예요. 오버플로 동작이 필요하면 Swift의 오버플로 연산자를 사용해서 직접 선택할 수 있어요. doc:AdvancedOperators#Overflow-Operators에서 설명하는 내용이에요.

Swift에는 C에 없는 범위 연산자도 있어요. a..<ba...b처럼 값의 범위를 간단히 표현해 주죠.

이 챕터에서는 Swift의 흔한 연산자들을 다룰게요. 더 고급 연산자와 커스텀 타입에 연산자를 직접 정의하거나 표준 연산자를 구현하는 방법은 doc:AdvancedOperators에서 설명해요.

출처: The Swift Programming Language

본문

용어

연산자는 단항(unary), 이항(binary), 삼항(ternary)으로 나뉘어요.

  • 단항 연산자는 하나의 대상에 작용해요(-a처럼). 단항 전위(prefix) 연산자는 대상 바로 앞에 붙고(!b처럼), 단항 후위(postfix) 연산자는 대상 바로 뒤에 붙어요(c!처럼).
  • 이항 연산자는 두 대상을 다루고(2 + 3처럼), 두 대상 사이에 놓이기 때문에 중위(infix) 연산자라고 불러요.
  • 삼항 연산자는 세 대상을 다뤄요. C와 마찬가지로 Swift에도 삼항 연산자는 하나뿐이에요. 바로 삼항 조건 연산자(a ? b : c)예요.

연산자가 작용하는 값들을 *피연산자(operand)*라고 해요. 1 + 2라는 식에서 +는 중위 연산자이고, 그 두 피연산자는 값 12예요.

할당 연산자 (Assignment Operator)

할당 연산자(a = b)는 a의 값을 b의 값으로 초기화하거나 업데이트해요.

let b = 10
var a = 5
a = b
// a is now equal to 10

할당의 오른쪽이 값이 여러 개인 튜플이라면, 그 요소들을 한 번에 여러 상수나 변수로 분해할 수도 있어요.

let (x, y) = (1, 2)
// x is equal to 1, and y is equal to 2

Swift의 할당 연산자는 C나 Objective-C와 달리 그 자체로 값을 반환하지 않아요. 그래서 다음 문장은 유효하지 않아요.

if x = y {
    // This isn't valid, because x = y doesn't return a value.
}

이 특징 덕분에 실제로는 ==(같다)를 써야 할 자리에 =(할당)가 실수로 쓰이는 걸 막아 줘요. if x = y를 유효하지 않게 만들어서, Swift가 이런 종류의 실수를 코드에서 피하도록 도와주는 거예요.

산술 연산자 (Arithmetic Operators)

Swift는 모든 숫자 타입에 대해 네 가지 표준 산술 연산자를 지원해요.

  • 덧셈(Addition, +)
  • 뺄셈(Subtraction, -)
  • 곱셈(Multiplication, *)
  • 나눗셈(Division, /)
1 + 2       // equals 3
5 - 3       // equals 2
2 * 3       // equals 6
10.0 / 2.5  // equals 4.0

C나 Objective-C의 산술 연산자와 달리, Swift의 산술 연산자는 기본적으로 값이 오버플로되는 걸 허용하지 않아요. 오버플로 동작을 쓰고 싶다면 Swift의 오버플로 연산자(a &+ b 같은)를 사용하면 돼요. doc:AdvancedOperators#Overflow-Operators를 참고하세요.

덧셈 연산자는 String 연결에도 쓰여요.

"hello, " + "world"  // equals "hello, world"

나머지 연산자 (Remainder Operator)

나머지 연산자(a % b)는 ba 안에 몇 배 들어가는지를 계산한 다음, 남는 값을(즉 나머지) 반환해요.

Note: 나머지 연산자(%)는 다른 언어에서는 모듈로(modulo) 연산자라고도 불러요. 하지만 Swift에서 음수에 대한 동작을 보면, 엄밀히 말하면 모듈로 연산이 아니라 나머지 연산이에요.

나머지 연산자가 어떻게 동작하는지 볼게요. 9 % 4를 계산하려면 먼저 9 안에 4가 몇 번 들어가는지 세어요. 9 안에는 4가 두 번 들어가고, 나머지는 1이 돼요. Swift에서는 이렇게 적어요.

9 % 4    // equals 1

a % b의 답을 구하려면 % 연산자는 다음 식을 계산해서 remainder를 출력으로 반환해요.

a = (b x some multiplier) + remainder

여기서 some multiplierba 안에 들어갈 수 있는 가장 큰 배수예요. 94를 이 식에 대입하면 9 = (4 x 2) + 1이 돼요.

a가 음수일 때 나머지를 구할 때도 같은 방법을 사용해요.

-9 % 4   // equals -1

-94를 식에 대입하면 -9 = (4 x -2) + -1이 되어 나머지가 -1이 돼요.

b가 음수일 때는 b의 부호는 무시돼요. 즉 a % ba % -b는 항상 같은 답이 나온다는 뜻이에요.

단항 마이너스 연산자 (Unary Minus Operator)

숫자 값의 부호는 앞에 -를 붙여서 뒤집을 수 있어요. 이것을 단항 마이너스 연산자라고 불러요.

let three = 3
let minusThree = -three       // minusThree equals -3
let plusThree = -minusThree   // plusThree equals 3, or "minus minus three"

단항 마이너스 연산자(-)는 작용하는 값 바로 앞에 공백 없이 붙어요.

단항 플러스 연산자 (Unary Plus Operator)

단항 플러스 연산자(+)는 작용하는 값을 아무런 변화 없이 그대로 반환해요.

let minusSix = -6
let alsoMinusSix = +minusSix  // alsoMinusSix equals -6

단항 플러스 연산자는 실제로 아무것도 하지 않지만, 음수에 단항 마이너스 연산자를 쓸 때 양수 쪽에도 대칭을 맞춰 주려고 사용할 수 있어요.

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

C와 마찬가지로 Swift에도 할당(=)과 다른 연산을 결합한 복합 할당 연산자가 있어요. 한 예로 덧셈 할당 연산자(+=)가 있어요.

var a = 1
a += 2
// a is now equal to 3

a += 2라는 표현은 a = a + 2를 줄여 쓴 거예요. 사실상 덧셈과 할당이 하나의 연산자로 합쳐져서 두 작업을 동시에 수행하는 셈이에요.

Note: 복합 할당 연산자는 값을 반환하지 않아요. 예를 들어 let b = a += 2라고 쓸 수 없어요.

Swift 표준 라이브러리가 제공하는 연산자에 대한 정보는 Operator Declarations를 참고하세요.

비교 연산자 (Comparison Operators)

Swift는 다음과 같은 비교 연산자를 지원해요.

  • 같다(Equal to, a == b)
  • 같지 않다(Not equal to, a != b)
  • 크다(Greater than, a > b)
  • 작다(Less than, a < b)
  • 크거나 같다(Greater than or equal to, a >= b)
  • 작거나 같다(Less than or equal to, a <= b)

Note: Swift는 동일성 연산자(identity operator) 두 개(===!==)도 제공해요. 두 객체 참조가 같은 객체 인스턴스를 가리키는지 확인할 때 사용해요. 자세한 내용은 doc:ClassesAndStructures#Identity-Operators를 참고하세요.

비교 연산자는 각각 문장이 참인지 여부를 알려 주는 Bool 값을 반환해요.

1 == 1   // true because 1 is equal to 1
2 != 1   // true because 2 isn't equal to 1
2 > 1    // true because 2 is greater than 1
1 < 2    // true because 1 is less than 2
1 >= 1   // true because 1 is greater than or equal to 1
2 <= 1   // false because 2 isn't less than or equal to 1

비교 연산자는 if 문 같은 조건문에서 자주 쓰여요.

let name = "world"
if name == "world" {
    print("hello, world")
} else {
    print("I'm sorry \(name), but I don't recognize you")
}
// Prints "hello, world", because name is indeed equal to "world".

if 문에 대한 자세한 내용은 doc:ControlFlow에서 다뤄요.

두 튜플이 같은 타입이고 같은 개수의 값을 가지면 비교할 수 있어요. 튜플은 왼쪽에서 오른쪽으로 값을 하나씩 비교하다가, 서로 같지 않은 두 값을 만나면 그때 비교를 멈춰요. 그 두 값을 비교한 결과가 튜플 비교의 전체 결과를 결정해요. 모든 요소가 같다면 두 튜플 자체도 같은 거예요. 예를 들어 보죠.

(1, "zebra") < (2, "apple")   // true because 1 is less than 2; "zebra" and "apple" aren't compared
(3, "apple") < (3, "bird")    // true because 3 is equal to 3, and "apple" is less than "bird"
(4, "dog") == (4, "dog")      // true because 4 is equal to 4, and "dog" is equal to "dog"

위 예시에서 첫 줄에서 왼쪽에서 오른쪽으로 비교하는 동작을 볼 수 있어요. 12보다 작으므로 (1, "zebra")(2, "apple")보다 작다고 봐요. 튜플 안의 다른 값이 뭐든 상관없어요. "zebra""apple"보다 작지 않다는 건 문제가 안 돼요. 튜플의 첫 번째 요소에 의해 이미 비교가 결정됐기 때문이에요. 하지만 튜플의 첫 번째 요소가 같으면 두 번째 요소를 비교하게 돼요. 두 번째와 세 번째 줄에서 벌어지는 일이 바로 그거예요.

튜플은 주어진 연산자가 각 튜플의 값마다 적용될 수 있을 때만 그 연산자로 비교할 수 있어요. 예를 들어 아래 코드에서 보여 주듯, (String, Int) 타입의 두 튜플은 비교할 수 있어요. StringInt 값 모두 < 연산자로 비교할 수 있기 때문이에요. 반면 (String, Bool) 타입의 두 튜플은 < 연산자로 비교할 수 없어요. < 연산자를 Bool 값에는 적용할 수 없거든요.

("blue", -1) < ("purple", 1)        // OK: Evaluates to true.
("blue", false) < ("purple", true)  // Error: Can't use < to compare Boolean values.

Note: Swift 표준 라이브러리에는 요소가 7개보다 적은 튜플용 비교 연산자가 포함돼 있어요. 요소가 7개 이상인 튜플을 비교하려면 비교 연산자를 직접 구현해야 해요.

삼항 조건 연산자 (Ternary Conditional Operator)

삼항 조건 연산자는 세 부분으로 이루어진 특별한 연산자로, question ? answer1 : answer2 형태를 띠어요. question이 참인지 거짓인지에 따라 두 표현 중 하나를 평가하는 약칭이에요. question이 참이면 answer1을 평가해서 그 값을 반환하고, 그렇지 않으면 answer2를 평가해서 그 값을 반환해요.

삼항 조건 연산자는 아래 코드의 약칭이에요.

if question {
    answer1
} else {
    answer2
}

테이블 행의 높이를 계산하는 예를 들어 볼게요. 행에 헤더가 있으면 행 높이가 콘텐츠 높이보다 50포인트 커야 하고, 헤더가 없으면 20포인트 커야 한다고 해볼게요.

let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight is equal to 90

위 예시는 아래 코드를 줄여 쓴 거예요.

let contentHeight = 40
let hasHeader = true
let rowHeight: Int
if hasHeader {
    rowHeight = contentHeight + 50
} else {
    rowHeight = contentHeight + 20
}
// rowHeight is equal to 90

첫 번째 예시에서 삼항 조건 연산자를 쓴 덕분에 rowHeight를 한 줄의 코드로 올바른 값에 설정할 수 있어요. 두 번째 예시의 코드보다 훨씬 간결하죠.

삼항 조건 연산자는 두 표현 중 어느 것을 쓸지 결정하는 효율적인 약칭을 제공해요. 다만 주의해서 사용해야 해요. 너무 많이 쓰면 간결함이 오히려 읽기 어려운 코드를 만들 수 있거든요. 삼항 조건 연산자 여러 개를 하나의 복합문으로 조합하는 건 피하는 게 좋아요.

Nil-합병 연산자 (Nil-Coalescing Operator)

nil-합병 연산자(a ?? b)는 옵셔널 a가 값을 담고 있으면 그것을 언랩하고, anil이면 기본값 b를 반환해요. 표현식 a는 항상 옵셔널 타입이에요. 표현식 ba 안에 저장된 타입과 일치해야 해요.

nil-합병 연산자는 아래 코드의 약칭이에요.

a != nil ? a! : b

위 코드는 삼항 조건 연산자와 강제 언랩(a!)을 사용해서, anil이 아닐 때는 a에 감싸여 있는 값에 접근하고 그렇지 않으면 b를 반환해요. nil-합병 연산자는 이런 조건 확인과 언랩을 간결하고 읽기 쉬운 형태로 감싸 주는 더 우아한 방법을 제공해요.

Note: a의 값이 nil이 아니면 b의 값은 평가되지 않아요. 이것을 *단락 평가(short-circuit evaluation)*라고 불러요.

아래 예시는 기본 색상 이름과 옵셔널한 사용자 정의 색상 이름 중 하나를 고르는 데 nil-합병 연산자를 사용해요.

let defaultColorName = "red"
var userDefinedColorName: String?   // defaults to nil

var colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName is nil, so colorNameToUse is set to the default of "red"

userDefinedColorName 변수는 기본값이 nil인 옵셔널 String으로 정의돼요. 옵셔널 타입이기 때문에 nil-합병 연산자로 그 값을 고려할 수 있는 거예요. 위 예시에서는 colorNameToUse라는 String 변수의 초기값을 정하는 데 이 연산자를 사용했어요. userDefinedColorNamenil이므로 userDefinedColorName ?? defaultColorName이라는 표현은 defaultColorName의 값, 즉 "red"를 반환해요.

userDefinedColorNamenil이 아닌 값을 할당하고 nil-합병 연산자로 다시 확인하면, 기본값 대신 userDefinedColorName 안에 감싸여 있는 값을 사용해요.

userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
// userDefinedColorName isn't nil, so colorNameToUse is set to "green"

범위 연산자 (Range Operators)

Swift에는 값의 범위를 표현하는 약칭인 여러 범위 연산자가 있어요.

닫힌 범위 연산자 (Closed Range Operator)

닫힌 범위 연산자(a...b)는 a부터 b까지의 범위를 정의하며 값 ab를 모두 포함해요. a의 값은 b보다 크면 안 돼요.

닫힌 범위 연산자는 모든 값을 다 사용하고 싶은 범위를 반복할 때 유용해요. for-in 루프처럼요.

for index in 1...5 {
    print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25

for-in 루프에 대한 자세한 내용은 doc:ControlFlow에서 다뤄요.

반개방 범위 연산자 (Half-Open Range Operator)

반개방 범위 연산자(a..<b)는 a부터 b까지의 범위를 정의하지만 b는 포함하지 않아요. 첫 번째 값은 포함하지만 마지막 값은 포함하지 않는다는 뜻에서 반개방이라고 불러요. 닫힌 범위 연산자와 마찬가지로 a의 값은 b보다 크면 안 돼요. a의 값이 b와 같으면 결과 범위는 비어 있어요.

반개방 범위는 배열처럼 0부터 시작하는 목록을 다룰 때 특히 유용해요. 목록의 길이까지(단, 길이는 포함하지 않고) 세어야 할 때가 많거든요.

let names = ["Anna", "Alex", "Brian", "Jack"]
let count = names.count
for i in 0..<count {
    print("Person \(i + 1) is called \(names[i])")
}
// Person 1 is called Anna
// Person 2 is called Alex
// Person 3 is called Brian
// Person 4 is called Jack

배열에 요소가 네 개 있는데도 0..<count3까지만 세는 걸 볼 수 있어요. 요소의 마지막 인덱스죠. 반개방 범위이기 때문이에요. 배열에 대한 자세한 내용은 doc:CollectionTypes#Arrays에서 다뤄요.

한쪽 범위 (One-Sided Ranges)

닫힌 범위 연산자에는 한 방향으로 가능한 한 멀리 이어지는 범위를 위한 대체 형태가 있어요. 예를 들어 배열의 인덱스 2부터 끝까지의 모든 요소를 포함하는 범위 같은 거죠. 이런 경우에는 범위 연산자의 한쪽 값을 생략할 수 있어요. 이렇게 연산자의 한쪽에만 값이 있는 범위를 한쪽 범위라고 불러요. 예를 들어 보죠.

for name in names[2...] {
    print(name)
}
// Brian
// Jack

for name in names[...2] {
    print(name)
}
// Anna
// Alex
// Brian

반개방 범위 연산자에도 마지막 값만 적는 한쪽 형태가 있어요. 양쪽에 값을 넣을 때와 마찬가지로, 마지막 값은 범위에 포함되지 않아요. 예를 들어 보죠.

for name in names[..<2] {
    print(name)
}
// Anna
// Alex

한쪽 범위는 서브스크립트에서뿐 아니라 다른 맥락에서도 쓸 수 있어요. 첫 번째 값을 생략한 한쪽 범위는 반복할 수 없어요. 어디서 반복을 시작해야 할지 분명하지 않기 때문이에요. 마지막 값을 생략한 한쪽 범위는 반복할 수 있어요. 다만 범위가 무한히 이어지기 때문에 루프에 명시적인 종료 조건을 꼭 추가해야 해요. 아래 코드처럼 한쪽 범위가 특정 값을 포함하는지도 확인할 수 있어요.

let range = ...5
range.contains(7)   // false
range.contains(4)   // true
range.contains(-1)  // true

논리 연산자 (Logical Operators)

논리 연산자는 불리언 논리값 truefalse를 바꾸거나 결합해요. Swift는 C 계열 언어에서 볼 수 있는 세 가지 표준 논리 연산자를 지원해요.

  • 논리 NOT (!a)
  • 논리 AND (a && b)
  • 논리 OR (a || b)

논리 NOT 연산자 (Logical NOT Operator)

논리 NOT 연산자(!a)는 불리언 값을 뒤집어서 truefalse가 되고, falsetrue가 돼요.

논리 NOT 연산자는 전위 연산자라서 작용하는 값 바로 앞에 공백 없이 붙어요. "a가 아님"으로 읽을 수 있는데, 다음 예시에서 볼 수 있어요.

let allowedEntry = false
if !allowedEntry {
    print("ACCESS DENIED")
}
// Prints "ACCESS DENIED".

if !allowedEntry라는 표현은 "입장이 허용되지 않았다면"으로 읽을 수 있어요. 다음 줄은 "입장이 허용되지 않았다"가 참일 때, 즉 allowedEntryfalse일 때만 실행돼요.

이 예시처럼 불리언 상수와 변수의 이름을 신중하게 지으면 코드를 읽기 쉽고 간결하게 유지하면서, 이중 부정이나 헷갈리는 논리문을 피할 수 있어요.

논리 AND 연산자 (Logical AND Operator)

논리 AND 연산자(a && b)는 두 값이 모두 true일 때에만 전체 표현식도 true가 되는 논리식을 만들어요.

어느 한 값이라도 false이면 전체 표현식도 false가 돼요. 사실 번째 값이 false이면 두 번째 값은 아예 평가되지 않아요. 어차피 전체 표현식을 true로 만들 수 없으니까요. 이것을 *단락 평가(short-circuit evaluation)*라고 불러요.

이 예시는 두 Bool 값을 고려해서, 두 값이 모두 true일 때만 접근을 허용해요.

let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
    print("Welcome!")
} else {
    print("ACCESS DENIED")
}
// Prints "ACCESS DENIED".

논리 OR 연산자 (Logical OR Operator)

논리 OR 연산자(a || b)는 파이프 문자 두 개가 나란히 붙어 만든 중위 연산자예요. 두 값 중 하나만 true여도 전체 표현식이 true가 되는 논리식을 만들 때 사용해요.

위의 논리 AND 연산자와 마찬가지로 논리 OR 연산자도 표현식을 고려할 때 단락 평가를 사용해요. 논리 OR 식의 왼쪽이 true면 오른쪽은 평가되지 않아요. 전체 표현식의 결과를 바꿀 수 없기 때문이에요.

아래 예시에서 첫 번째 Bool 값(hasDoorKey)은 false지만, 두 번째 값(knowsOverridePassword)은 true예요. 한 값이 true이므로 전체 표현식도 true로 평가되고, 접근이 허용돼요.

let hasDoorKey = false
let knowsOverridePassword = true
if hasDoorKey || knowsOverridePassword {
    print("Welcome!")
} else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"

논리 연산자 결합하기 (Combining Logical Operators)

여러 논리 연산자를 결합해서 더 긴 복합 표현식을 만들 수 있어요.

if enteredDoorCode && passedRetinaScan || hasDoorKey || knowsOverridePassword {
    print("Welcome!")
} else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"

이 예시는 여러 &&|| 연산자를 사용해서 더 긴 복합 표현식을 만들어요. 하지만 &&|| 연산자는 여전히 두 값에만 작용하므로, 사실은 세 개의 더 작은 표현식이 이어진 것이에요. 이 예시는 이렇게 읽을 수 있어요.

올바른 도어 코드를 입력하고 망막 스캔을 통과했거나, 유효한 도어 키가 있거나, 비상 대체 비밀번호를 알고 있다면 접근을 허용해라.

enteredDoorCode, passedRetinaScan, hasDoorKey의 값에 따라 처음 두 하위 표현식은 false예요. 하지만 비상 대체 비밀번호를 알고 있으므로 전체 복합 표현식은 여전히 true로 평가돼요.

Note: Swift의 논리 연산자 &&||는 왼쪽 결합(left-associative)이에요. 즉 논리 연산자가 여러 개인 복합 표현식은 가장 왼쪽의 하위 표현식을 먼저 평가해요.

명시적 괄호 (Explicit Parentheses)

복잡한 표현식의 의도를 읽기 쉽게 하려면 엄밀히 필요하지 않을 때에도 괄호를 넣는 게 유용할 때가 있어요. 위의 도어 접근 예시에서 복합 표현식의 첫 부분 주위에 괄호를 넣으면 그 의도를 명확히 드러낼 수 있어요.

if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowsOverridePassword {
    print("Welcome!")
} else {
    print("ACCESS DENIED")
}
// Prints "Welcome!"

괄호는 처음 두 값이 전체 논리에서 따로 떼어 놓은 하나의 가능한 상태로 취급된다는 걸 분명히 해 줘요. 복합 표현식의 출력은 변하지 않지만, 읽는 사람에게는 전체 의도가 더 명확해지죠. 읽기 쉬움이 항상 간결함보다 우선이에요. 의도를 분명히 하는 데 도움이 된다면 주저하지 말고 괄호를 사용하세요.

더 알아보기