Swift 정리 - 6. 흐름 제어


Swift에서 흐름을 제어하는 여러 구문을 살펴보자.


특정 조건에서 코드를 실행해야 하거나 실행하지 말아야 하는 상황들이 있고, 특정 명령어를 반복해야 하는 경우도 있다. 이때 조건문과 반복문을 사용한다.

Swift에서 중괄호는 생략할 수 없다는 점에 주의하자.

조건문

특정 조건에 따라 실행되어야 하는 부분이 다를 때 조건문을 사용한다.

Swift는 ifswitch 구문을 사용할 수 있다. 주로 간단한 조건과 나올 수 있는 결과가 제한적일 때 if를 사용하고, 더 복잡한 경우에 switch를 사용한다.

if

특정 조건이 true일 때 코드를 실행한다. ifelse를 붙여 iffalse인 경우에 실행할 코드를 작성한다.

else if는 몇 개가 이어져도 상관 없고 else는 없어도 된다. if 문 뒤의 조건문을 소괄호로 감싸는 것은 자유다.

Swift의 if문은 조건의 값이 꼭 Bool 타입이어야 한다.

var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
}
// Prints "It's very cold. Consider wearing a scarf."

temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
    print("It's very cold. Consider wearing a scarf.")
} 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 {
    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."

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

Switch

switch 문은 값을 여러 매칭 가능한 패턴과 비교한다. 만약 적절한 코드 블록을 발견하면 해당 블록을 바로 실행한다.

image

모든 switch문은 여러 가능한 케이스들로 구성되어 있고, case 키워드로 시작한다. 모든 swtich문은 모든 가능한 케이스를 포함하고 있어야 한다. 만약 모든 케이스를 작성하는 게 적합하지 않은 경우에는 default 키워드를 써서 명시되지 않은 케이스들을 처리할 수 있다. 이 default 키워드는 맨 마지막에 써야 한다.

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"

swtich 문은 위에서부터 차례로 하나씩 케이스를 비교하며 적합한 코드 블럭을 찾는다. 위 예제에서는 a, z를 제외한 나머지 모든 문자를 default 케이스로 처리했다. 이런 관점은 switch가 모든 케이스를 처리할 수 있게 도와준다.

No Implicit Fallthrough

C의 switch문과 다르게, Swift의 switch문은 기본적으로 케이스 하나가 끝나면 다음 케이스가 실행되지 않는다. 원래 다른 언어들에서는 break 키워드를 쓰지 않으면 일치하는 케이스부터 그 뒤에 있는 모든 케이스들이 다 실행되는데, Swift는 일치하는 케이스만 실행되고 끝난다.

break가 Swift에서 요구되지는 않지만 쓸 수는 있다.

각 케이스에는 최소한 한 줄의 실행가능한 문장이 있어야 한다. 아래의 코드는 a일 때 실행할 문장이 없기 때문에 유효하지 않은 코드다.

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.

“a”혹은 “A” 와 일치하는 케이스를 한 케이스에 작성하고 싶다면 아래와 같이 두 값을 컴마로 구분해서 작성한다.

let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
    print("The letter A")
default:
    print("Not the letter A")
}
// Prints "The letter A"

Interval Matching

switch 의 케이스에 있는 값들에는 범위도 사용될 수 있다.

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."

Tuples

switch문에서 튜플도 비교할 수 있다. 튜플의 각 요소는 특정 값이나 범위가 될 수 있다. 언더스코어 문자 _(wildcard 패턴)을 사용해서 값이 나올 수 있는 부분에 매칭하기 위해 사용한다.

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"

Value Bindings

위에서처럼 와일드카드 식별자를 사용하면 무시된 값을 직접 가져와야 하는 경우에는 불편하다. 따라서 임시로 상수나 변수를 값에 매칭시켜서 사용할 수 있다. 이를 value binding이라고 하는데, 그 이유는 값들이 케이스의 실행문 내에서 임시 상수나 변수에 할당되기 때문이다.

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"

여기에서 default가 사용되지 않았다. 마지막 케이스 case let (x, y)는 어떤 값이든 매칭될 수 있는 케이스이기 때문에 모든 케이스가 커버되는 것이다.

Where

where를 사용해서 추가 조건을 체크할 수도 있다.

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"

Compound Cases

위에서 여러 일치하는 케이스를 한 case문으로 작성하고 싶을 때 값들을 컴마로 구분해서 나열하라고 했다. Value binding도 이런 compound case로 작성할 수 있다.

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"

unknown

switch 구문에서 모든 case를 처리하면 문제가 없지만, 코드를 수정하며 가능한 case들이 더 생겨날 수 있다. 열거형을 switch를 통해 비교할 경우 열거형에 새로운 케이스가 추가되면 원래 switch 문은 컴파일에러가 날 것이다. Swift 5.0에서는 unknown이라는 속성을 통해 이를 유리하게 대처할 수 있다.

enum Menu {
    case chicken
    case pizza
    // later pasta could be added
}

let lunchMenu = Menu.chicken

switch lunchMenu {
case .chicken:
    print("맛있겠다")
case .pizza:
    print("배고프다")
case _: // same as default:
    print("점심 안먹나요?")
}

여기까지 작성하면 case _: 부분이 실제로 실행될 가능성이 없기 때문에 컴파일러 warning으로 “Case will never be executed”를 보여준다.

여기에서 만약 내가 Menu 열거형에 case pasta를 추가하면 컴파일러 경고조차 사라져 switch문에서 pasta 케이스가 처리가 안됐다는 걸 모르고 넘어가기 쉽다. 이를 방지하기 위해 unknown 속성을 사용한다.

enum Menu {
    case chicken
    case pizza
    // later pasta could be added
    case pasta
}

let lunchMenu = Menu.chicken

switch lunchMenu {
case .chicken:
    print("맛있겠다")
case .pizza:
    print("배고프다")
@unknown case _: // same as default:
    print("점심 안먹나요?")
}

아래와 같은 경고를 통해 해당 switch 구문이 모든 case에 대응하지 않는다는 사실을 상기시킬 수 있다. unknown 속성을 부여한 case는 switch 구문의 마지막 case로 작성해야 한다.

image

Fallthrough

swtich 문에서는 일치하는 케이스 실행후 다음 케이스가 실행이 되지 않는다고 했다. 만약 C 같이 fallthrough 동작을 구현하고 싶다면 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."

반복문

For-In Loops

for-in 문을 사용해서 반복적인 데이터(배열, 수의 범위, 문자열 내 문자)나 시퀀스를 반복할 수 있다.

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

딕셔너리에서는 키-값 쌍을 반복할 수 있다. 딕셔너리의 각 아이템은 (key, value) 튜플로 반환된다. 그리고 튜플을 분해해서 for-in 문 안에서 상수나 변수에 할당할 수 있다.

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
    print("\(animalName)s have \(legCount) legs")
}
// cats have 4 legs
// ants have 6 legs
// spiders have 8 legs

시퀀스에 해당하는 값이 필요 없다면 와일드카드 식별자를 사용할 수 있다.

for _ in 1...10 {
  result += 10
}

While Loops

while문은 특정 조건이 false가 될때까지 코드들을 실행한다. while문은 첫 번째 반복이 시작되기 전에 반복을 몇 번 해야하는지 모를 경우에 유용하다. Swift는 두 종류의 while 문을 제공한다.

  • 일반적인 while
  • repeat-while : 각 반복의 마지막 부분에서 조건을 검사한다.

image

var square = 0
var diceRoll = 0
while square < finalSquare {
    // roll the dice
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    // move by the rolled amount
    square += diceRoll
    if square < board.count {
        // if we're still on the board, move up or down for a snake or a ladder
        square += board[square]
    }
}
print("Game over!")

image

repeat {
    // move up or down for a snake or ladder
    square += board[square]
    // roll the dice
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    // move by the rolled amount
    square += diceRoll
} while square < finalSquare
print("Game over!")

Labeled Statements

Swift에서는 반복문 안에 반복문을 중첩으로 작성할 수 있다. break, continue 문을 사용해서 어떤 범위에 이를 적용해야 할지 판단하기 힘든 경우가 있는데, 이럴 경우 반복문 앞에 이름과 함께 콜론을 붙여 구문의 이름을 지정해주는 구문 이름표를 사용할 수 있다. 즉 구문 이름표는 내가 조작할 반복문을 명확하게 표시하기 위해 사용한다.

image

gameLoop: while square != finalSquare {
    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
    case let newSquare where newSquare > finalSquare:
        // diceRoll will move us beyond the final square, so roll again
        continue gameLoop
    default:
        // this is a valid move, so find out its effect
        square += diceRoll
        square += board[square]
    }
}
print("Game over!")

Early Exit

guard문은 if 문과 비슷하게 표현식의 불리언 값에 따라 문장을 실행한다. guard 문 뒤에 있는 문장을 실행시키기 전에 특정조건이 꼭 참이 맞는지를 확인하기 위해 사용한다. if 문과는 다르게, guard 문은 항상 else문이 따라온다. else 괄호 내에 있는 문장은 조건이 true가 아닐 때 실행된다.

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        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문의 괄호 밖의 뒷 부분에 있는 문장이 실행된다. 만약 조건이 참이 아니라면 else 괄호 안에 있는 문장이 실행되고, guard 문이 속한 코드 블럭을 빠져나오게 된다.

guard 문을 사용하면 if를 사용하는 것에 비해 코드의 가독성을 향상시킨다.

Checking API Availability

Swift는 API 사용이 가능한지 확인할 수 있는 기능이 있다. 이는 주어진 배포 타겟에 맞지 않는 API를 실수로 사용하는 것을 방지한다.

컴파일러는 내가 사용한 모든 API들이 프로젝트에 명시된 배포 타겟에서 사용 가능한지 확인하기 위해 SDK의 availability 정보를 사용한다. Swift는 사용할 수 없는 API를 내가 사용하려고 했을 때 컴파일 타임에서 에러를 발생시킨다.

런타임에서 어떤 API를 사용할지를 ifguard 문 내에서 availability condition을 사용할 수 있다. 컴파일러는 블럭 내의 API가 사용가능한지를 검증할 때 availability condition의 정보를 사용한다.

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

위 예시에서는 iOS 10 이상, macOS 10.12 이상을 요구하고, 그리고 마지막 인자 *는 꼭 써야 하고 다른 플랫폼에서는 if 부분의 코드가 실행될 것임을 명시한다.

image