애플사이다의 iOS 개발 일지

[Swift Language Guide 정독 시리즈] 5. Control Flow 본문

Swift/영문 공식문서 뜯어보기-Swift

[Swift Language Guide 정독 시리즈] 5. Control Flow

Applecider 2021. 9. 29. 00:34

안녕하세요. 애플사이다 입니다.

[Swift Language Guide 정독 시리즈]의 다섯 번째 챕터 Control Flow에 대해 정리해보겠습니다.

*Swift Language Guide를 읽어야 하는 이유는 시리즈 0. Language Guide란? 포스팅을 참고해주세요.

 


Control Flow (흐름 제어)

Swift에는 코드의 흐름을 제어하기 위한 여러 종류의 구문 (statements)이 있다.

1) 작업을 여러 번 반복하는 while문 (while loop), 2) 특정 조건 (conditions)에 따라 다른 코드 덩어리 (branches of code)를 실행하는 if / guard/ switch문, 3) 코드의 실행 흐름 (flow of execution)을 다른 곳으로 이동시키는 break, continue 등의 구문이 있다. 

또한 4) for-in문 (for-in loop)를 통해 Array, Dictionary, 범위 (range), 문자열 (String) 그리고 그 외 시퀀스 (Sequence)를 쉽게 반복 (iterate)할 수 있다.

5) Swift의 switch문 (switch statement)은 C 언어에 비해 강력하다. switch문의 case는 인터벌 매치 (interval matches), 튜플 (tuple), 특정 타입으로의 캐스팅 (cast) 등의 여러 패턴 (patterns)을 비교 (match)할 수 있다. 특정 case에 매치된 값 (matched values)은 임시 상수/변수에 저장 (bound)하여 사용할 수 있다. 복잡한 조건은 각 case에서 where문 (where clause)을 사용하여 나타낼 수 있다.

For-In Loops (for-in문)

for-in문을 사용하여 시퀀스 (Sequence)에 반복 (iterate)하여 접근할 수 있다. 이때, 시퀀스는 Array의 item, 숫자의 범위 (ranges), 문자열 (String)의 문자 (Characters) 등을 말한다.

 

Dictionary의 각 item은 (key, value) 형태의 튜플 (tuple)로 반환된다.

for-in문 내부에서 해당 튜플을 특정 이름의 상수/변수로 분해 (decompose)하여 튜플 요소에 접근할 수 있다.

Dictionary는 순서 없이 item을 저장하는 타입 (inherently unordered)이므로 꺼낼 (retrieved) item의 순서를 알 수 없다.

*Array 및 Dictionary에 대한 자세한 내용은 [Swift Language Guide 정독 시리즈] 4. Collection Types 참고해주세요.

let names = ["Anna", "Alex", "Brian", "Jack"] // Array를 iterate 한다
for name in names {
    print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4] // Dictionary의 item은 (Key, Value) 튜플로 반환된다
for (animalName, legCount) in numberOfLegs {
    print("\(animalName)s have \(legCount) legs")
}
// cats have 4 legs - sorted() 메서드가 없으면, 실행할 때마다 출력 순서가 바뀐다
// ants have 6 legs
// spiders have 8 legs

for character in "AppleCider" { // String을 iterate 한다
    print(character, terminator: " ")
} // "A p p l e C i d e r " 출력

숫자 범위 (numeric ranges)도 반복 (iterate)할 수 있다. 아래 예시에서 1...5 는 1 이상 5 이하의 숫자가 반복할 시퀀스이다.

닫힌 범위 연산자 (closed range operator) ... 를 사용했으므로 1 및 5를 포함 (from 1 to 5, inclusive)한다.

상수 index에 범위의 첫 번째 값인 1이 할당되고, 반복문 (loop) 내부의 코드가 실행된다. 그다음으로 상수 index에 범위의 두 번째 값인 2가 할당되고, 반복문 (loop) 내부의 코드가 실행된다. 이것을 범위의 마지막 값까지 반복한다.

✅ 이때, 상수 index는 별도 선언 없이 (let 키워드 없이) 사용할 수 있다. 반복문을 선언함과 동시에 암시적으로 선언된 (implicitly declared) 상태이기 때문이다.

*범위 연산자에 대한 자세한 내용은 [Swift Language Guide 정독 시리즈] 2. Basic Operators의 범위 연산자를 참고해주세요.

 

시퀀스의 값을 사용할 필요가 없을 경우, 밑줄 (underscore, _ )을 사용하여 값을 무시할 수 있다.

for index in 1...5 { // 숫자 범위를 iterate 한다
    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 index in 3.0...5.0 { // 컴파일 에러 - Protocol 'Sequence' requires that 'Double.Stride' (aka 'Double') conform to 'SignedInteger'
    print("\(index) times 5 is \(index * 5)")
}

let base = 3
let power = 10
var answer = 1
for _ in 1...power { // 밑줄을 사용하여 시퀀스 (숫자 범위)의 값을 무시한다
    answer *= base   // 3을 반복하여 곱하는 연산이므로 범위 1...power (counter values)와 무관하다
}
print("\(base) to the power of \(power) is \(answer)")
// Prints "3 to the power of 10 is 59049"

워치 화면 (watch face)에 매 분 단위로 체크 표시 (tick marks)를 하는 기능을 구현해보자.

0 분부터 시작하여 60개의 체크 표시를 할 때, 처음 값 (lower bound)은 포함하되 마지막 값 (upper bound)은 제외하려면, 반폐쇄 범위 연산자 (half-open range operator) ..< 를 사용한다.

*범위 연산자에 대한 자세한 내용은 [Swift Language Guide 정독 시리즈] 2. Basic Operators의 범위 연산자를 참고해주세요.

 

체크 표시를 매 분 단위가 아니라 5분 단위로 하려면, stride(from:to:by:) 메서드로 특정 범위를 건너뛴다.

stride(from:to:by:) 메서드는 마지막 값을 포함하지 않고, stride(from:through:by:) 메서드는 마지막 값을 포함한다.

let minutes = 60
for tickMark in 0..<minutes { 
    // render the tick mark each minute (60 times)
}

let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) { // draw one mark every 5 minutes
    // render the tick mark every 5 minutes (0, 5, 10, 15 ... 45, 50, 55) <- 60 미포함 (매개변수 to)
}

let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) { // draw one mark every 3 hours
    // render the tick mark every 3 hours (3, 6, 9, 12) <- 12 포함 (매개변수 through)
}

for-in문은 Sequence Protocol을 준수한다면, 기본 Collection 타입 (Array, Set, Dictionary) 뿐만 아니라 사용자 정의 타입의 클래스 및 collection 타입에도 사용할 수 있다.

사용자 정의 타입이 Sequence Protocol을 준수하도록 설정하려면, 1) iterator를 반환하는 makeIterator() 메서드를 구현해야 한다. 또는 2) 해당 타입이 자체적인 iterator로 기능할 수 있는 경우, IteratorProtocol Protocol을 준수하도록 설정하면, default로 makeIterator() 메서드를 사용할 수 있다.

While Loops (While 반복문)

While 반복문은 조건이 거짓이 될 때까지 (until a condition becomes false) 코드를 반복한다. (참이면 실행하고, 거짓이면 종료한다.)

반복문을 시작할 때, 반복 횟수를 알 수 없는 경우 사용한다.

Swift의 while문은 2가지 종류가 있다.

  • while문 : 반복을 시작하는 시점 (at the start of each pass through the loop)에 조건을 평가 (evaluates)한다.
  • repeat-while문 : 반복이 끝난 시점에 조건을 평가 (evaluates)한다.

while문

먼저 조건을 평가한다. 조건이 참이면 코드를 실행하고, 조건이 거짓이 될 때까지 반복한다.

while condition {  // while문의 일반적인 형식
    statements
}

예시로 뱀과 사다리 (Snakes and Ladders) 게임을 구현해보자.

게임 보드

게임 규칙은 이렇다.

  • 게임 보드는 25칸 (squares)이고, 목표는 25번째 칸이나 그 너머로 가는 것이다.
  • 플레이어의 시작 위치는 보드의 왼쪽 아래 바깥인 "0칸"이다.
  • 매 턴 (turn) 마다 주사위를 굴려서 나온 숫자만큼 칸을 이동한다.
  • 사다리의 아랫부분에서 턴이 끝나면, 사다리 윗부분으로 올라간다. 뱀의 머리에서 턴이 끝나면, 뱀의 꼬리로 내려간다.

게임 보드는 Int 값의 Array 타입인 변수 board로 나타낸다. 보드의 크기 (size)는 상수 finalSquare를 기준으로 하고, 이 값을 사용하여 Array를 초기화하고, 게임의 승리 조건을 확인한다. 플레이어가 0칸에서부터 출발하므로 변수 board를 25개가 아닌 26개의 0 (Int 값)으로 초기화한다.

 

뱀과 사다리를 나타내기 위해 일부 칸 (squares)에 특정한 값을 할당한다. 사다리의 아랫부분이 있는 칸은 양수 (positive number)를 할당하여 플레이어가 올라가도록 하고, 뱀의 머리가 있는 칸은 음수 (negative number)를 할당하여 플레이어가 내려가도록 한다.

예를 들어, 3번 칸에 사다리 아랫부분이 있으므로 플레이어가 8칸 앞에 있는 11번 칸으로 이동하도록 "+08"을 할당한다.

 

이때, 코드를 대칭적으로 보이게 하도록 1) 단항 빼기 연산자 (Unary Minus Operator) - 와 함께 단항 더하기 연산자 (Unary Plus Operator) + 를 사용했고, 2) 10 미만의 숫자는 0을 덧붙여 (padded with zeros, ex. 8 -> 08) 모든 숫자를 두 자리로 통일했다. (이러한 스타일 측면의 테크닉 (stylistic technique)은 필수는 아니지만 코드를 깔끔하게 만든다.)

*범위 연산자에 대한 자세한 내용은 [Swift Language Guide 정독 시리즈] 2. Basic Operators의 범위 연산자를 참고해주세요.

let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1) // 26개의 0으로 초기화한다

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 // 뱀과 사다리를 나타냈다
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

게임을 while문으로 구현해보자.

var square = 0 // 플레이어의 현재 위치
var diceRoll = 0

while square < finalSquare { // *플레이어의 현재 위치가 상수 finalSquare 미만인지 조건을 확인한다
    // roll the dice
    diceRoll += 1 
    if diceRoll == 7 { diceRoll = 1 }
    
    // move by the rolled amount
    square += diceRoll
    
    if square < board.count { // *플레이어의 현재 위치가 26칸 미만인지 확인한다
        // if we're still on the board, move up or down for a snake or a ladder
        square += board[square]
    }
}
print("Game over!")
  • 주사위를 단순화했다. 임의의 숫자를 생성하는 대신, while문의 반복문을 실행할 때마다 1씩 증가하도록 설정했다. 또한 값이 7이 되면 다시 1로 재할당했다. 따라서 변수 diceRoll의 시퀀스는 1, 2, 3, 4, 5, 6, 1, 2... 를 반복하다.
  • 주사위를 굴린 후, 플레이어는 주사위 수만큼 칸을 이동한다. 이때, 1) 이동한 위치가 마지막 25번 칸을 벗어나지 않았다면 (26칸 미만이면), 뱀/사다리를 반영하기 위해 board[square]에 저장된 값만큼 플레이어가 이동한다. 2) 26칸 이상이면 게임이 종료된다.
  • while문의 현재 반복문이 실행을 마치면, 다시 반복문을 실행할지 결정하기 위해 반복문의 조건 (loop’s condition)을 확인한다. 플레이어가 마지막 25번 칸이나 그 너머로 이동하면, 반복문의 조건은 거짓으로 평가되고 게임이 종료된다. 
  • 이 예시는 반복문을 실행하기 전에는 반복을 몇 번 실행할지 파악하기 어려우므로 while문이 적합하다. 대신, while문의 반복문은 특정 조건을 만족할 때까지만 실행된다.

Note: if square < board.count {...}으로 if문의 조건이 참인지 확인하지 않으면, board[square]에서 유효하지 않은 index (Array 범위를 벗어난 index)로 접근하여 런타임 오류가 발생할 수 있다.

Repeat-While (Repeat-While문)

while문의 변형 (variation)인 repeat-while문이 있다. repeat-while은 반복문의 조건을 확인하기 전에 우선 반복문을 한 번 실행한다.

그다음, 반복문의 조건이 거짓이 될 때까지 반복문을 반복하여 실행한다.

repeat {  // repeat-while문의 일반적인 형식
    statements
} while condition

Note: Swift의 repeat-while문은 다른 언어의 do-while문과 유사하다.

 

뱀과 사다리 게임을 while문이 아니라 repeat-while문으로 구현해보자.

상수/변수 finalSquare, board, square, diceRoll의 값을 초기화하는 방법은 동일하다.

var square = 0 // 플레이어의 현재 위치
var diceRoll = 0

repeat {
    // move up or down for a snake or ladder
    square += board[square] // *while문의 'if square < board.count'가 필요 없다 (즉, Array bounds check이 필요 없다)
    
    // roll the dice
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    
    // move by the rolled amount
    square += diceRoll
} while square < finalSquare // 반복 조건은 while문과 동일하다
print("Game over!")
  • while문과 달리, 먼저 뱀/사다리를 반영하기 위한 board[square]가 등장한다.
    처음부터 마지막 25번 칸으로 이동할 가능성이 없으므로 안전한 코드이다. 
  • 뱀/사다리를 확인한 후, 주사위를 굴리고 플레이어는 주사위 수만큼 칸을 이동한다.
  • 현재 반복문이 종료되는 시점에 반복문의 조건을 확인한다. 
  • 반복문을 한 번 실행할 때, while문은 변수 square (현재 플레이어의 위치)가 보드를 벗어나지 않았는지 2번 확인하지만, repeat-while문은 1번 확인한다. 따라서 뱀과 사다리 게임은 repeat-while문의 구조가 보다 적절하다.

Conditional Statements (조건문)

특정 조건에 따라 다른 조각의 코드를 실행하는 기능이 유용할 때가 있다.

예를 들어, 에러가 발생하면, 특정 조각의 코드를 실행하기를 원할 수도 있다. 또는 어떤 값이 너무 크거나 작으면, 메시지를 표시하고 싶을 수도 있다. 이렇게 하려면 코드의 일부를 조건 (condition)에 따라 동작하도록 설정해야 한다.

 

Swift는 두 가지 조건문이 있다. 1) if문 (if statement), 2) switch문 (switch statement)

  • if문 : 조건이 간단하고, 조건의 개수가 적을 때 사용한다.
  • switch문 : 조건의 개수가 다양할 때 사용한다. 특히 *패턴 매칭을 통해 실행할 코드 조각을 결정할 경우 유용하다.

*패턴 매칭 (pattern matching)이란?
✅ 특정 형태의 패턴을 찾아내는 것이다. 예를 들어, 튜플 (1, "일")의 패턴은 (Int, String)이다. (2, "이")의 패턴과 패턴 매칭을 하면, 두 패턴은 형태가 일치하므로 "매치된다"고 판단한다. (두 튜플의 값이 동일해야 패턴이 매치되는 것이 아니므로 "일치한다" 보다는 "매치된다"라는 표현이 정확하다.)

If (If문)

가장 간단한 형태의 if문은 1개의 조건 (if condition)을 가진다. 조건이 참 (true)일 때만, if 구문을 실행한다.

var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
}
// Prints "It's very cold. Consider wearing a scarf."
  • 온도 (temperature)가 32 degrees Fahrenheit (화씨 32도, 물의 어는점) 보다 작거나 같은지 확인하는 예시이다.
  • 조건이 참이므로 if 구문이 실행되어 메시지가 출력됐다.
    (만약 조건이 거짓이면, if 구문이 실행되지 않아 아무런 메시지도 출력되지 않는다.)
  • if문의 실행이 종료되면, 코드 실행은 if문의 닫힌 중괄호 } (closing brace) 이후로 계속된다.

if문에서는 조건이 거짓 (false)일 때 실행하는 else 구문 (else clause)도 사용할 수 있다. 필요한 경우에만 사용하면 된다.

if 구문 및 else 구문 중 하나는 항상 실행된다. 

 

else if 구문을 연달아 (chain) 사용하면, 여러 개의 조건을 추가할 수 있다.

temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} else { // else 구문을 사용
    print("It's not that cold. Wear a t-shirt.")
}
// Prints "It's not that cold. Wear a t-shirt."

temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 { // else if 구문을 사용하여 조건을 추가했다
    print("It's really warm. Don't forget to wear sunscreen.")
} else {
    print("It's not that cold. Wear a t-shirt.")
}
// Prints "It's really warm. Don't forget to wear sunscreen."

위 예시에서 마지막 else 구문은 필요한 경우에만 사용하면 된다.

temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
    print("It's really warm. Don't forget to wear sunscreen.")
}
  • else 구문을 삭제했다.
  • if 조건 및 else if 조건이 모두 거짓이므로 아무런 메시지도 출력되지 않는다.

Switch (Switch문)

A switch statement considers a value and compares it against several possible matching patterns. It then executes an appropriate block of code, based on the first pattern that matches successfully. A switch statement provides an alternative to the if statement for responding to multiple potential states.

 

switch문은 특정 값에 대해 *패턴 매칭 (pattern matching)을 한다. 그다음 첫 번째로 매치되는 패턴의 코드 블록을 실행한다. (다수의 패턴과 매치될 수도 있지만, 그중에서 가장 빠른 순서의 패턴에 해당하는 코드를 실행하는 것이다.)

switch문은 if문보다 다양한 조건에 대응할 수 있다.

 

*패턴 매칭 (pattern matching)이란?
✅ 특정 형태의 패턴을 찾아내는 것이다. 예를 들어, 튜플 (1, "일")의 패턴은 (Int, String)이다. (2, "이")의 패턴과 패턴 매칭을 하면, 두 패턴은 형태가 일치하므로 "매치된다"고 판단한다. (두 튜플의 값이 동일해야 패턴이 매치되는 것이 아니므로 "일치한다" 보다는 "매치된다"라는 표현이 정확하다.)

 

가장 간단한 형태의 switch문은 특정 값을 기준으로 1개 이상의 값과 비교한다. 이때 비교값 (some value to consider)과 선택지 값 (예시의 value 1, value 2, value 3)의 타입은 동일해야 한다.

switch 비교값 { // switch문의 간단한 형태
case value 1:
    respond to value 1
case value 2,
     value 3:
    respond to value 2 or 3
default:
    otherwise, do something else
}
  • 모든 switch문은 다수의 "case"로 구성된다. 
  • 위 예시처럼 1) 단순히 값으로 패턴 매칭을 하는 방법 외에도 2) 복잡한 패턴 매칭을 하는 방법도 있다. (아래에서 설명)

if문의 "if 구문" 및 "else 구문"과 같이 switch문의 각 case는 별개의 코드 블록이다.

switch문은 어떤 코드 블록을 실행할지 결정한다. 이 과정을 스위칭 (switching on the value)이라고 한다.

 

Every switch statement must be exhaustive. That is, every possible value of the type being considered must be matched by one of the switch cases. If it’s not appropriate to provide a case for every possible value, you can define a default case to cover any values that aren’t addressed explicitly. This default case is indicated by the default keyword, and must always appear last.

 

❗️switch문의 case는 빠뜨리는 것이 없어야 (exhaustive) 한다. 즉, "비교값 (some value to consider)의 타입이 가질 수 있는 모든 값"을 나열했을 때, 모든 값이 switch문의 case 중 적어도 하나와 매치되어야 한다.

만약 가능한 값들을 모두 나열할 수 없는 경우, default case를 사용해야 한다. default case는 case로 명시한 조건에 해당하지 않는 값들을 다룬다. default case는 default 키워드를 통해 나타내며, 반드시 마지막에 위치해야 한다.

 

아래 예시는 1개의 소문자 알파벳이 할당된 상수 someCharacter을 비교값으로 하는 switch문을 사용한다.

let someCharacter: Character = "z"
switch someCharacter {
case "a":
    print("The first letter of the alphabet")
case "z":
    print("The last letter of the alphabet")
default:
    print("Some other character")
}
// Prints "The last letter of the alphabet"
  • switch문의 첫 번째 case는 "a"와 매치되고, 두 번째 case는 "z"와 매치된다.
  • switch문은 상수 someCharacter의 타입인 Character가 가질 수 있는 모든 값을 case에 나타내야 한다. 여기서 "Character 타입이 가질 수 있는 모든 값"이란 영어 알파벳뿐만 아니라, 유니코드로 표현할 수 있는 다양한 문자를 포함한다. 따라서 default case를 사용하여 "a" 및 "z" 외의 다른 문자를 다루도록 했다.
    ❗️switch문의 비교값이 열거형 (Enumeration)처럼 한정적인 경우가 아니면, 항상 defalut를 구현하는 것이 안전하다.

No Implicit Fallthrough (암시적으로 Fallthrough가 적용되지 않음)

C 및 Objective-C와 달리 Swift의 switch문은 자동으로 (default) 각 case의 바닥에서 다음 case로 넘어가지 않는다. (don’t fall through) 대신, break문 (break statement)을 명시하지 않더라도 비교값에 매치되는 첫 번째 case를 발견하는 즉시, switch문을 종료한다. 이를 통해 실수로 여러 개의 case를 실행하는 것을 방지하므로 C 보다 안전하게 switch문을 사용할 수 있다.

Note: Although break isn’t required in Swift, you can use a break statement to match and ignore a particular case or to break out of a matched case before that case has completed its execution.

 

Swift에서 break문은 필수적이지 않지만, 필요 시 사용할 수 있다. 예를 들어, 특정 case를 무시할 때, 또는 매치된 case를 실행하는 도중에 빠져나가야 할 경우 (break out of a matched case)에 사용한다. (아래의 Break in a Switch Statement 섹션 참고)

 

case 구문 (body)은 최소한 1개의 실행 가능한 구문 (executable statement)을 포함해야 한다.

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // Invalid, the case has an empty body
case "A":
    print("The letter A")
default:
    print("Not the letter A")
}
// This will report a compile-time error.

 

  • 첫 번째 case의 내용이 비어있으므로 런타임 에러가 발생한다.
  • C의 switch문과 달리, 예시의 switch문은 case "a" 및 case "A"에 매치되지 않는다. 

위 예시에서 문자 "a" 및 "A"에 모두 매치되는 경우를 1개의 case로 나타내려면, 쉼표 (comma, , )를 사용하여 case를 합친다.

가독성을 위해 합친 case를 여러 줄에 걸쳐서 나타낼 수도 있다. (아래의 Compound Cases 섹션 참고)

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A": // 문자 "a" 및 "A"에 모두 매치된다
    print("The letter A")
default:
    print("Not the letter A")
}
// Prints "The letter A"

Note: fallthrough 키워드를 사용하면, 특정 case에서 다음 case로 넘어갈 것을 명시할 수 있다. (아래의 Fallthrough 섹션 참고)

단, 바로 다음에 위치한 1개 case까지만 실행된다.

Interval Matching (인터벌 매칭)

case는 값의 범위를 나타낼 수 있다. 따라서 비교값이 특정 간격 (interval)을 두고 case에 포함되는지 여부를 확인할 수 있다. 

아래 예제는 모든 크기의 숫자를 판단하여 특정 간격을 두고 수를 세는 단위 (natural-language count)로 변환한다.

let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
    naturalCount = "no"
case 1..<5:
    naturalCount = "a few"
case 5..<12:
    naturalCount = "several"
case 12..<100:
    naturalCount = "dozens of"
case 100..<1000:
    naturalCount = "hundreds of"
default:
    naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")
// Prints "There are dozens of moons orbiting Saturn."
  • 위 예제에서 상수 approximateCount는 switch문에서 평가되는 (evaluated) 비교값이다.
  • 상수 approximateCount의 값은 각 case의 숫자 또는 범위와 비교된다. 
  • 상수 approximateCount의 값은 62이므로 12..<100의 범위에 속한다. 따라서 상수 naturalCount에 "dozens of"가 할당되고, switch문을 빠져나온다.

Tuples (튜플)

튜플을 사용하면, switch문 내부에서 여러 개의 값을 비교할 수 있다. 튜플의 각 요소 (element)를 다른 값 또는 값의 범위에 대해 비교할 수 있다. 다른 방법으로는 밑줄 (underscore, _ )을 사용하여 모든 값에 매치할 수도 있다. 이것을 와일드카드 패턴 (wildcard pattern)이라고 한다.

 

아래 예시는 (Int, Int) 타입의 간단한 튜플을 사용한 (x, y) 좌표 형태를 표현한다. 또한 좌표를 그래프로 나타낸다.

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("\(somePoint) is at the origin")
case (_, 0):
    print("\(somePoint) is on the x-axis")
case (0, _):
    print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
    print("\(somePoint) is inside the box")
default:
    print("\(somePoint) is outside of the box")
}
// Prints "(1, 1) is inside the box"

case를 나타낸 그래프

  • switch문은 좌표가 1) 원점 (0, 0)에 있는지, 2) 빨간색 x축 (x-axis)에 있는지, 3) 주황색 y축 (y-axis)에 있는지, 4) 원점을 중심으로 한 파란색 4x4 박스 내부에 있는지, 5) 박스 외부에 있는지 확인한다.
  • C와 달리, Swift는 switch문의 비교값 (값 또는 튜플)을 여러 개의 case와 비교하는 것을 허용한다.
    사실은 좌표 (0, 0)은 예시의 모든 4개 case에 매치된다. 하지만 이 경우 항상 첫 번째로 매치된 case를 사용한다. 즉, 좌표 (0, 0)은 첫 번째 case에 가장 먼저 매치된다. 따라서 다른 case는 모두 무시된다.

Value Bindings (값 바인딩)

switch문의 case는 매치되는 값 또는 값들을 임시 상수/변수에 할당할 수 있다. 이때 임시 상수/변수는 case 구문 (body) 내부에서 사용할 수 있다. 값을 임시 상수/변수에 묶는다 (bound)는 뜻에서 이것을 값 바인딩 (value binding)이라고 한다.

 

아래 예시는 (Int, Int) 타입의 튜플을 사용한 (x, y) 좌표 형태를 표현한다. 또한 좌표를 그래프로 나타낸다.

let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
    print("on the x-axis with an x value of \(x)")
case (0, let y):
    print("on the y-axis with a y value of \(y)")
case let (x, y):
    print("somewhere else at (\(x), \(y))")
}
// Prints "on the x-axis with an x value of 2"

  • switch문은 좌표가 1) 빨간색 x축 (x-axis)에 있는지, 2) 주황색 y축 (y-axis)에 있는지, 3) 그 외 다른 곳에 있는지 확인한다.
  • 3개 case는 placeholder 상수인 x 및 y를 선언했다. anotherPoint 튜플의 값을 1개 또는 2개 할당할 임시 상수이다. 
  • 예를 들어, 첫 번째의 case (let x, 0)는 y값이 0인 모든 좌표에 매치되며, 해당 좌표의 x값을 임시 상수 x에 할당한다.
    임시 상수 x는 case의 코드 블록 내부에서 사용할 수 있다.
  • ✅ 예시의 switch문은 defalut case를 구현하지 않았다. 마지막의 case let (x, y)가 비교값 anotherPoint의 타입인 (Int, Int)의 모든 값을 다룰 수 있기 때문이다.

Where (Where절)

switch문의 case는 where절을 사용하면, 추가 조건을 확인할 수 있다.

아래 예시는 좌표 (x, y)를 그래프에 나타낸다.

let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
    print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
    print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
    print("(\(x), \(y)) is just some arbitrary point")
}
// Prints "(1, -1) is on the line x == -y"

  • switch문은 좌표가 1) x == y인 초록색 대각선에 있는지, 2) x == -y인 보라색 대각선에 있는지, 3) 그 외 다른 곳에 있는지 확인한다.
  • placeholder 상수 x 및 y는 where절에서 dynamic filter로 사용된다. case는 where절의 조건이 참일 때만, 좌표 값을 매치한다.
  • 이전의 예시와 마찬가지로 default case를 구현할 필요가 없다.

✅ where절은 switch문뿐만 아니라, for-in문에서도 사용할 수 있다.

let array: [Int?] = [nil, 2, 3]

for case let num? in array where num > 2 {
    print("num is \(num)")
}

Compound Cases (복합 case)

여러 개의 case가 동일한 구문 (body)를 가진다면, case 뒤에 여러 패턴을 합쳐서 나타낼 수 있다. 여러 패턴 중에 하나라도 매치되면, 해당 case에 매치되었다고 간주한다. 패턴은 여러 줄에 걸쳐서 나타낼 수 있다.

let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
    print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
     "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
    print("\(someCharacter) is a consonant")
default:
    print("\(someCharacter) isn't a vowel or a consonant")
}
// Prints "e is a vowel"
  • 첫 번째 case는 5개의 소문자 모음 알파벳에 매치된다. 두 번째 case는 모든 소문자 자음 알파벳에 매치된다. defalut case는 그 외 모든 문자 (알파벳뿐만 아니라 모든 다른 문자)에 매치된다.
Compound cases can also include value bindings. All of the patterns of a compound case have to include the same set of value bindings, and each binding has to get a value of the same type from all of the patterns in the compound case. This ensures that, no matter which part of the compound case matched, the code in the body of the case can always access a value for the bindings and that the value always has the same type.

 

복합 case는 값 바인딩을 포함할 수 있다. 이때 하나의 case에 속하는 모든 패턴들은 값 바인딩 형태가 동일해야 하고 (the same set of value bindings), 값 바인딩을 통해 할당하는 값의 타입이 동일해야 한다. 이 때문에 어떠한 패턴에 매치되더라도 항상 case 구문 (body)은 문제없이 값 바인딩을 통해 값에 접근할 수 있다.

let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
    print("On an axis, \(distance) from the origin")
default:
    print("Not on an axis")
}
// Prints "On an axis, 9 from the origin"
  • case에 2개의 패턴이 들어있다. 두 패턴 모두 임시 상수 distance에 바인딩을 하며, distance의 타입이 Int로 동일하다.
    따라서 case 구문은 어떠한 패턴에 매치되더라도 문제 없이 distance의 값에 접근할 수 있다.

Control Transfer Statements (제어 전달 구문)

제어 전달 구문은 코드의 실행 순서를 바꾼다. 코드의 실행 흐름 (flow of execution), 또는 제어 (control)를 이동시키는 것이다.

Swift의 제어 전달 구문은 다섯 가지가 있다.

1) continue, 2) break, 3) fallthrough, 4) return, 5) throw

 

*1) continue, 2) break, 3) fallthrough은 아래에서 설명하고,
4) return은 Functions 챕터에서, 5) throw는 Error Handling 챕터에서 다루겠습니다.

continue

continue 키워드는 현재 실행 중인 반복문 (loop)을 중단하고, 다음 차례의 반복 (next iteration)을 처음부터 실행한다.

(반복문을 완전히 종료하는 것이 아니다.)

 

아래 예시는 퍼즐 문구를 만들기 위해 모든 모음 및 공백 (space)을 삭제한다.

let puzzleInput = "great minds think alike"
var puzzleOutput = ""
let charactersToRemove: [Character] = ["a", "e", "i", "o", "u", " "] // 삭제할 모음 및 공백
for character in puzzleInput {
    if charactersToRemove.contains(character) {
        continue
    }
    puzzleOutput.append(character)
}
print(puzzleOutput)
// Prints "grtmndsthnklk"
  • 모음 및 공백과 일치할 때마다 continue 키워드를 호출한다. 

Break

break 키워드는 전체 흐름 제어 구문 (control flow statement)의 실행을 즉시 종료한다. switch문 및 반복문에서 사용할 수 있다.

Break in a Loop Statement (반복문의 break)

반복문 전체를 종료하고, 해당 반복문에서 빠져나온다. (다음 차례의 반복을 진행하지 않는다.)

Break in a Switch Statement (switch문의 break)

switch문 전체를 종료하고, 해당 switch문에서 빠져나온다.

 

따라서 break를 1개 이상의 case를 무시할 목적으로 사용할 수 있다. Swift의 switch문은 빠뜨리는 것이 없어야 (exhaustive) 하고, 빈 case를 허용하지 않으므로 가끔 의도적으로 특정 case를 무시해야만 하는 상황이 발생하기 때문이다. 이 경우, 무시하고자 하는 case에 break 키워드만 넣는다. 

 

Note: case에 주석 (comment)만 들어있으면 컴파일 에러가 발생한다. 주석은 실행 가능한 구문이 아니기 때문이다. 특정 case를 무시하려면, 항상 break 키워드를 사용해야 한다.

 

아래 예시는 Character 타입의 값을 비교값으로 하며, 4개 언어 중 하나에 해당하는 숫자 기호인지 확인한다.

let numberSymbol: Character = "三"  // Chinese symbol for the number 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
    possibleIntegerValue = 1
case "2", "٢", "二", "๒":
    possibleIntegerValue = 2
case "3", "٣", "三", "๓":
    possibleIntegerValue = 3
case "4", "٤", "四", "๔":
    possibleIntegerValue = 4
default:
    break // 위의 case에 해당하지 않는 나머지 case는 무시한다
}
if let integerValue = possibleIntegerValue {
    print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
    print("An integer value couldn't be found for \(numberSymbol).")
}
// Prints "The integer value of 三 is 3."
  • switch문은 비교값이 Latin, Arabic, Chinese, or Thai의 숫자 1~4를 나타내는 기호가 맞는지 확인한다.
    매치되는 case가 있으면, Int? (옵셔널 Int) 타입의 변수 possibleIntegerValue에 정수값을 할당한다.
  • switch문이 종료되면, 변수 possibleIntegerValue를 옵셔널 바인딩 (optional binding)하여 값이 존재하는지 확인한다.
    변수 possibleIntegerValue는 옵셔널 타입이므로 암시적으로 nil을 초기값으로 가진다. 따라서 옵셔널 바인딩이 성공하려면, switch문의 첫 번째~네 번째 case에 매치되어 변수 possibleIntegerValue에 정수값이 할당되어야 한다.
  • default case는 특별한 기능 없이, 단순히 위의 case에서 매치되지 않은 모든 값을 다루기 위해 사용한다. 따라서 break 키워드만 작성했다. default case가 매치되면, break 키워드가 전체 switch문을 종료한다.

Fallthrough

Swift의 switch문은 암시적으로 fallthrough 키워드가 적용되지 않는다. 즉, 각 case의 바닥에서 다음 case로 넘어가지 않는다.

따라서 비교값에 매치되는 첫 번째 case를 발견하는 즉시, switch문을 종료한다. 

이와 달리, C에서는 fallthrough를 방지하기 위해 모든 case 구문의 끝에 break 키워드를 작성해야 했다.

 

default로 fallthrough를 하지 않으므로 Swift의 switch문은 C에 비해 간결하고 예측 가능하며, 실수로 여러 개의 case를 실행하는 것을 방지한다.

 

C와 같은 형태의 fallthrough가 필요하면, fallthrough 키워드를 사용할 수 있다.

✅ fallthrough 키워드를 사용하면, 특정 case에서 다음 case로 넘어갈 수 있다. 단, 바로 다음에 위치한 1개 case까지만 실행된다.

아래 예시는 숫자를 설명하는 텍스트를 나타내기 위해 fallthrough 키워드를 사용했다.

let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " a prime number, and also"
    fallthrough
default:
    description += " an integer."
}
print(description)
// Prints "The number 5 is a prime number, and also an integer."
  • fallthrough 키워드를 사용하여 default case로 넘어가서 (fall through) 코드를 실행한다.

✅ Note: fallthrough 키워드는 넘어갈 case의 조건을 확인하지 않고, 키워드가 등장한 즉시, 무조건 다음 case로 넘어간다. 

let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " a prime number, and also"
    fallthrough // 즉시, 다음 case를 실행한다 (바로 다음 case 1개만)
    description += " a prime number, and also" // 실행되지 않는다 (경고 메시지 - Will never be executed)
case 100:
    description += "*****" // fallthrough 키워드의 다음 case가 실행된다
default:
    description += " an integer." // 그 다음 case는 실행되지 않는다
}
print(description) // The number 5 is a prime number, and also*****

Labeled Statements (레이블이 있는 구문)

Swift에서는 복잡한 구조를 나타내기 위해 어떤 반복문/조건문 내부에 다른 반복문/조건문을 중첩 (nest)하여 구현할 수 있다. 이때 반복문/조건문은 모두 break 키워드를 사용하여 실행을 조기에 종료할 수 있다. 이 경우, break 키워드를 통해 종료할 반복문 또는 조건문을 레이블을 붙여서 명시하는 것이 유용하다. 비슷하게, 여러 개의 중첩된 반복문을 사용할 경우에도 continue 키워드를 적용할 반복문을 레이블을 붙여서 명시하는 것이 유용하다.

 

반복문 또는 조건문을 구문 레이블 (statement label)로 표시할 수 있다. 

  • 조건문 : break 키워드를 적용할 부분을 구문 레이블로 표시한다. (조건문은 continue 키워드를 사용하지 않는다.)
  • 반복문 : break/continue 키워드를 적용할 부분을 구문 레이블로 표시한다.
label name: while condition { // while문에 구문 레이블을 표시한 예시
    statements
}

아래 예시는 뱀과 사다리 게임의 다른 버전이다. break/continue 키워드를 사용하며, while문을 구문 레이블로 표시했다.

이번에는 새로운 규칙이 추가되었다.

  • 게임에서 승리하려면 정확히 25번 칸에 위치해야 한다.
    만약 주사위를 굴려서 25번 칸을 넘어서면, 계속해서 다시 주사위를 굴려야 한다.

게임 보드는 이전과 동일하다. 또한 상수/변수 finalSquare, board, square, diceRoll의 값을 초기화하는 방법도 이전과 동일하다.

게임 보드

let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1) // 26개의 0으로 초기화한다

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 // 뱀과 사다리를 나타냈다
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

이번 버전에서는 게임의 로직을 구현하기 위해 while문 및 switch문을 사용한다.

while문은 게임의 주요한 반복문임을 나타내기 위해 gameLoop이라는 구문 레이블로 표시했다.

gameLoop: while square != finalSquare { // 정확히 25번 칸에 위치해야 한다는 조건
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    
    switch square + diceRoll {
    case finalSquare:
        // diceRoll will move us to the final square, so the game is over
        break gameLoop // break 키워드를 적용할 대상을 표시
    case let newSquare where newSquare > finalSquare:
        // diceRoll will move us beyond the final square, so roll again
        continue gameLoop // continue 키워드를 적용할 대상을 표시
    default:
        // this is a valid move, so find out its effect
        square += diceRoll
        square += board[square]
    }
}
print("Game over!")
  • 반복문이 시작될 때, 주사위를 굴린다. 플레이어를 바로 움직이지 않고, switch문을 통해 이동한 결과를 미리 판단하여 플레이어의 이동 여부를 결정한다.
  • break 키워드가 실행되면, 현재 반복문인 gameLoop를 즉시 종료하고 빠져나간다.
  • continue 키워드가 실행되면, 현재 반복문인 gameLoop의 현재 반복 (loop)을 종료하고, 다음 차례의 반복을 시작한다.

Note: 만약 break 키워드 뒤에 gameLoop 레이블을 사용하지 않으면, while문 대신 switch문을 종료한다.

엄밀히는 continue 키워드 뒤에 gameLoop 레이블을 사용하지 않아도 된다. continue는 반복문에만 사용되기 때문이다. 하지만 위 예시와 같이 두 부분에 gameLoop 레이블이 등장하므로 코드가 대칭적이고, 가독성이 좋으므로 레이블을 사용하는 것이 바람직하다.

Early Exit (빠른 종료)

if문처럼, guard문 (guard statement)은 수식의 불리언값 (참 또는 거짓)에 따라 코드 블록을 실행한다.

guard문은 guard문의 다음 코드를 실행하기 위해 특정 조건이 반드시 참이어야 할 경우에 사용한다.

if문과 달리, guard문은 항상 else 구문 (else clause)을 가진다. else 구문은 조건이 거짓인 경우 실행된다.

 

반드시 필요한 조건에 대해 guard문을 사용하면, if문을 사용한 것에 비해 코드의 가독성이 개선된다.

 

✅ guard의 역할은 잘못된 것을 막아주는 것이다. 따라서 guard문을 통과한 것은 이미 검증이 완료된 것으로 볼 수 있다.

따라서 if-let과 달리 guard-let의 상수는 guard문 외부에서도 사용할 수 있다. (guard문이 등장한 코드 블럭 내에서 사용 가능하다.)

guard-let은 1) 반드시 참이어야 할 조건을 확인하고, 2) 옵셔널 바인딩을 하는 두 가지 기능이 있다.

func greet(person: [String: String]) {
    guard let name = person["name"] else { // 1) 반드시 참이어야 할 조건을 확인하고, 2) 옵셔널 바인딩을 한다
        return
    }

    print("Hello \(name)!")

    guard let location = person["location"] else {
        print("I hope the weather is nice near you.")
        return
    }

    print("I hope the weather is nice in \(location).")
}

greet(person: ["name": "John"])
// Prints "Hello John!"
// Prints "I hope the weather is nice near you."
greet(person: ["name": "Jane", "location": "Cupertino"])
// Prints "Hello Jane!"
// Prints "I hope the weather is nice in Cupertino."
  • guard문의 조건이 참이면, guard문 조건의 일부로 옵셔널 바인딩을 통해 값이할당된 상수/변수는 guard문이 존재하는 코드 블럭 내에서 사용할 수 있다.
  • ❗️guard문의 조건이 거짓이면, else 구문이 실행된다. else 구문은 guard문이 존재하는 코드 블록을 종료하기 위해 반드시 제어을 이동 (transfer control)시켜야 한다. 즉, 1) 제어 전달 구문 (continue, break, return, throw) 또는 2) 반환하지 않는 함수 (fatalError(_:file:line:))를 사용해야 한다. 

Checking API Availability (API 사용 가능 여부 확인)

Swift는 API 사용 가능 여부를 확인하는 기능으로 #available 형태의 사용 가능 조건 (availability condition)을 제공한다. 이를 통해 배포 대상 (deployment target)인 완성된 프로그램 내부에서 사용 불가한 (unavailable) API를 실수로 사용하지 않도록 방지한다.

 

✅ 사용 가능 조건 (availability condition)을 사용하여 프로그램을 실행하는 도중 (runtime 시점)에 API 사용이 가능한지 판단한다.

if문, guard문, while문과 함께 사용할 수 있다. (단, && 또는 || 등의 논리 연산자는 사용 불가하다.)

컴파일러는 사용 가능 조건을 통해 API를 확인하고, 어떤 코드 블럭을 실행할지 판단한다.

 

일반적으로 사용 가능 조건은 플랫폼 이름 및 버전을 목록 형식으로 나타낸다.

if #available(platform name version, ..., *) { // 사용 가능 조건의 일반적인 형식
    // API가 사용 가능 (available)하면 실행할 코드
} else {
    // API가 사용 불가 (unavailable)하면 실행할 코드
}
  • 플랫폼 이름 : iOS, macOS, watchOS, tvOS 등이 있다. (전체 리스트는 Declaration Attributes에서 확인 가능하다.)
  • 버전 : major version numbers (ex. iOS 8, macOS 10.10) 또는 minor versions numbers (ex. iOS 11.2.6, macOS 10.13.3)로 나타낸다.
  • 별표 (asterisk, * )는 모든 플랫폼을 의미한다. 플랫폼 목록 끝에는 항상 *를 붙여야 한다. 

컴파일러는 *SDK에 들어있는 사용 가능한 API 정보를 확인하고, 개발이 완료된 코드 내부의 모든 API가 적절한지 확인한다. 사용 불가한 (unavailable) API를 사용하면 컴파일 오류가 발생한다.

*SDK (Software Development Kit)란?

개발자가 쉽게 개발을 하도록 도와주는 개발 도구 모음이다. 특정 기능을 구현하기 위해 필요한 클래스/함수/프로토콜 등을 개발자가 모두 구현하지 않고, Swift 표준 라이브러리와 같이 외부에서 구현한 코드 패키지를 사용하는 방식이다.

if #available(iOS 10, macOS 10.12, *) {
    // Use iOS 10 APIs on iOS, and use macOS 10.12 APIs on macOS
} else {
    // Fall back to earlier iOS and macOS APIs
}
  • if 구문은 iOS 플랫폼은 iOS 버전 10 이상, macOS 플랫폼은 macOS 버전 10.12 이상에서 실행된다. 또한 마지막 전달인자 * 는 필수 인자이며, 다른 플랫폼에서는 버전에 상관없이 실행됨 (executes on the minimum deployment target)을 의미한다.
  • else 구문은 iOS 버전 10 미만, macOS 버전 10.12 미만에서만 실행된다.

참고 - 이와 유사한 @available 은 플랫폼/운영체제의 버전에 관련된 속성 (Attribute)을 나타낸다. 아래와 같이 사용한다.

@available(iOS 13.0, macOS 11.06, watchOS 5.0, *) // 해당 함수를 사용 가능한 플랫폼 및 최소 버전
func someFunction() { }

@available(tvOS, unavailable) // 해당 클래스를 사용 불가한 플랫폼
class SomeClass { }

속성이란 1) 선언, 2) 타입, 3) switch case에 대한 부가 정보를 나타내는 세 가지 종류가 있으며, @속성이름 형태로 명시한다.

*속성에 대한 내용은 Swift Language Reference의 Attribute 챕터에서 자세히 다루겠습니다. 

 

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments