제어 흐름
제어 흐름 (Control Flow)
조건에 따라 다른 코드를 실행하고, 같은 코드를 여러 번 반복하고, 상황에 따라 흐름을 중간에 끊어내는 것. 이 모든 걸 묶어서 **제어 흐름(control flow)**이라고 불러요. Swift는 이걸 다루는 여러 제어문을 제공하는데요, 어떤 작업을 여러 번 반복할 때 쓰는 while 루프부터, 조건에 따라 다른 분기를 실행하는 if·guard·switch, 그리고 실행 흐름을 다른 지점으로 옮기는 break·continue 같은 문장까지 있죠.
배열, 딕셔너리, 범위, 문자열 같은 시퀀스를 손쉽게 돌아볼 수 있게 해주는 for-in 루프도 있어요. 또 defer 문장은 현재 스코프를 벗어날 때 실행될 코드를 감싸 두는 데 쓰죠.
특히 Swift의 switch는 C 계열 언어의 그것보다 훨씬 강력해요. 각 case가 구간 매칭, 튜플, 특정 타입으로의 캐스팅 등 여러 다양한 패턴을 매칭할 수 있고, 매칭된 값을 임시 상수나 변수에 바인딩해서 case 본문 안에서 쓸 수도 있죠. 복잡한 매칭 조건은 각 case에 where 절을 붙여서 표현할 수 있어요.
본문
For-In 루프
for-in 루프는 시퀀스를 하나씩 돌아볼 때 써요. 배열의 항목, 숫자의 범위, 문자열의 글자 같은 것들을 차례로 순회하죠.
아래 예시는 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 루프 본문 안에서 쓸 수 있죠. 아래 코드에서는 딕셔너리의 키를 animalName이라는 상수로, 값을 legCount라는 상수로 분해했어요.
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
Dictionary의 내용은 본질적으로 순서가 없어서, 순회할 때 어떤 순서로 꺼내올지는 보장되지 않아요. 특히 딕셔너리에 항목을 넣은 순서가 순회 순서를 정해 주지 않죠. 배열과 딕셔너리에 대한 더 자세한 내용은 컬렉션 타입 문서를 참고해요.
for-in 루프는 숫자 범위로도 쓸 수 있어요. 아래 예시는 5의 배수 표에서 앞의 몇 개 항목을 출력해요.
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
여기서 순회되는 시퀀스는 1부터 5까지의 숫자 범위인데, 닫힌 범위 연산자(...)를 썼으니 양 끝을 모두 포함하는 범위예요. index에는 범위의 첫 번째 숫자(1)가 들어가고, 루프 안의 문장이 실행돼요. 이 경우 루프 안에는 문장이 하나뿐이라 현재 index 값에 대한 5의 배수 항목을 출력하죠. 그 문장이 실행된 뒤에는 index에 범위의 두 번째 값(2)이 들어가고 print(_:separator:terminator:) 함수가 다시 호출돼요. 이 과정이 범위의 끝에 도달할 때까지 계속되는 거예요.
위 예시에서 index는 루프의 각 반복이 시작될 때 값이 자동으로 설정되는 상수예요. 그래서 index는 쓰기 전에 따로 선언할 필요가 없고, 루프 선언에 포함되는 것만으로 암시적으로 선언돼요. let 선언 키워드도 필요 없죠.
시퀀스에서 각 값을 다 쓸 필요가 없다면, 변수 이름 대신 밑줄(_)을 써서 그 값들을 무시할 수 있어요.
let base = 3
let power = 10
var answer = 1
for _ in 1...power {
answer *= base
}
print("\(base) to the power of \(power) is \(answer)")
// Prints "3 to the power of 10 is 59049".
위 예시는 어떤 수의 거듭제곱 값을 계산해요. 여기서는 3의 10승을 구하죠. 시작 값 1(즉 3의 0승)에 3을 열 번 곱하는데, 1로 시작해서 10으로 끝나는 닫힌 범위를 사용했어요. 이 계산에서는 매번 루프를 지날 때의 개별 카운터 값이 필요 없어요. 그냥 루프를 올바른 횟수만큼 실행하면 되죠. 그래서 루프 변수 자리에 밑줄 문자(_)를 쓰면 개별 값이 무시되고, 루프의 각 반복 동안 현재 값에 접근할 수도 없게 돼요.
어떤 상황에서는 양 끝을 모두 포함하는 닫힌 범위가 쓰기 어려울 때도 있어요. 시계판에서 매분마다 눈금을 그리는 경우를 생각해 봐요. 0분부터 시작해서 60개의 눈금을 그리고 싶다면, 반개방 범위 연산자(..<)를 써서 하한은 포함하고 상한은 제외하면 돼요. 범위에 대한 더 자세한 내용은 기본 연산자 문서의 범위 연산자 부분을 참고해요.
let minutes = 60
for tickMark in 0..<minutes {
// render the tick mark each minute (60 times)
}
어떤 사용자는 UI에 눈금을 더 적게 넣고 싶을 수도 있죠. 5분마다 하나씩만 표시하는 걸 선호할 거예요. 그럴 땐 stride(from:to:by:) 함수로 원하지 않는 눈금을 건너뛸 수 있어요.
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// render the tick mark every 5 minutes (0, 5, 10, 15 ... 45, 50, 55)
}
닫힌 범위가 필요하다면 stride(from:through:by:)를 쓰면 돼요.
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
// render the tick mark every 3 hours (3, 6, 9, 12)
}
위 예시들은 for-in 루프로 범위, 배열, 딕셔너리, 문자열을 순회했어요. 하지만 이 문법은 어떤 컬렉션이든 순회할 수 있어요. 직접 만든 클래스나 컬렉션 타입도, 그 타입들이 Sequence 프로토콜을 따른다면 순회 대상이 될 수 있죠.
While 루프
while 루프는 조건이 false가 될 때까지 문장들의 집합을 수행해요. 이런 루프는 첫 반복이 시작되기 전에 반복 횟수를 알 수 없을 때 가장 잘 쓰여요. Swift는 두 종류의 while 루프를 제공해요.
while은 루프를 한 바퀴 돌 때마다 시작할 때 조건을 평가해요.repeat-while은 루프를 한 바퀴 돌 때마다 끝날 때 조건을 평가해요.
While
while 루프는 단일 조건을 평가하는 것으로 시작해요. 조건이 true라면 문장들의 집합이 조건이 false가 될 때까지 반복되죠.
while 루프의 일반적인 형태는 다음과 같아요.
while <#condition#> {
<#statements#>
}
이 예시는 뱀과 사다리(Snakes and Ladders) 게임을 아주 단순하게 구현해요. Chutes and Ladders라고도 부르죠.
게임의 규칙은 다음과 같아요.
- 보드에는 25개의 칸이 있고, 25번 칸에 도달하거나 그 너머로 가면 이겨요.
- 플레이어의 시작 칸은 "0번 칸"인데, 보드의 왼쪽 아래 모서리 바로 바깥이에요.
- 매 턴마다 6면체 주사위를 굴려서 그 숫자만큼 이동하는데, 위의 점선 화살표가 가리키는 가로 경로를 따라가요.
- 턴이 사다리 아래에서 끝나면 그 사다리를 타고 올라가요.
- 턴이 뱀의 머리에서 끝나면 그 뱀을 타고 내려가요.
게임 보드는 Int 값의 배열로 표현돼요. 그 크기는 finalSquare라는 상수에 기반하는데, 이 상수는 배열을 초기화하는 데도 쓰이고 나중에 승리 조건을 확인하는 데도 쓰여요. 플레이어가 보드 밖인 "0번 칸"에서 시작하므로, 보드는 25개가 아니라 26개의 0 Int 값으로 초기화해요.
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
그런 다음 일부 칸에 뱀과 사다리를 위한 더 구체적인 값을 설정해요. 사다리 아래쪽 칸은 플레이어를 보드 위로 올려 보내는 양수 값을 갖고, 뱀의 머리 칸은 아래로 내려 보내는 음수 값을 가져요.
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
3번 칸에는 11번 칸으로 올라가는 사다리 아래쪽이 있어요. 이를 나타내기 위해 board[03]을 +08로 두는데, 이는 정수 값 8(3과 11의 차이)과 같아요. 값과 문장을 정렬하기 위해 단항 플러스 연산자(+i)를 단항 마이너스 연산자(-i)와 함께 명시적으로 사용했고, 10보다 작은 숫자에는 0을 채워 넣었어요. (두 스타일 모두 꼭 필요한 건 아니지만, 코드를 더 깔끔하게 보이게 해줘요.)
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!")
위 예시는 주사위를 굴리는 데 아주 단순한 방법을 써요. 랜덤 숫자를 생성하는 대신 diceRoll 값을 0에서 시작해요. while 루프를 한 바퀴 돌 때마다 diceRoll을 하나씩 늘리고, 그 값이 너무 커졌는지 검사해요. 이 반환 값이 7이 될 때마다 주사위 굴림이 너무 커진 거라서 값을 1로 리셋하죠. 그 결과 diceRoll 값은 항상 1, 2, 3, 4, 5, 6, 1, 2처럼 반복돼요.
주사위를 굴린 뒤 플레이어는 diceRoll 칸만큼 앞으로 이동해요. 주사위 굴림으로 25번 칸을 넘어갈 수도 있는데, 그 경우 게임이 끝나요. 이 시나리오를 처리하기 위해 코드는 square가 board 배열의 count 프로퍼티보다 작은지 확인해요. square가 유효하다면 board[square]에 저장된 값을 현재 square 값에 더해서 사다리나 뱀을 타고 올라가거나 내려가게 해요.
참고: 이 검사를 하지 않으면
board[square]가board배열의 경계를 벗어난 값에 접근하려 할 수 있고, 그러면 런타임 오류가 발생해요.
현재 while 루프의 실행이 끝나면 루프의 조건을 검사해서 루프를 다시 실행할지 결정해요. 플레이어가 25번 칸에 도달하거나 그 너머로 이동했다면 루프의 조건이 false로 평가되고 게임이 끝나요.
이 경우 while 루프가 적절한데, 그 이유는 게임의 길이가 while 루프가 시작될 때 명확하지 않기 때문이에요. 대신 특정 조건이 충족될 때까지 루프가 실행되죠.
Repeat-While
while 루프의 다른 변형은 repeat-while 루프라고 불러요. 루프 블록을 한 번 통과하는 것을 먼저 수행한 다음에야 루프의 조건을 고려하죠. 그 후에는 조건이 false가 될 때까지 루프를 계속 반복해요.
참고: Swift의
repeat-while루프는 다른 언어의do-while루프와 비슷해요.
repeat-while 루프의 일반적인 형태는 다음과 같아요.
repeat {
<#statements#>
} while <#condition#>
뱀과 사다리 예시를 while 루프 대신 repeat-while 루프로 다시 작성한 버전이에요. finalSquare, board, square, diceRoll의 값은 while 루프와 똑같이 초기화돼요.
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
이 버전의 게임에서 루프의 첫 번째 동작은 사다리나 뱀이 있는지 확인하는 거예요. 보드의 어떤 사다리도 플레이어를 25번 칸으로 직접 보내지는 않으므로, 사다리를 타고 올라가서 이길 수는 없어요. 그래서 루프의 첫 동작으로 뱀이나 사다리를 확인하는 게 안전하죠.
게임이 시작될 때 플레이어는 "0번 칸"에 있어요. board[0]은 항상 0이라 아무 효과가 없어요.
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!")
코드가 뱀과 사다리를 확인한 뒤에는 주사위를 굴리고 플레이어를 diceRoll 칸만큼 앞으로 이동시켜요. 그러면 현재 루프의 실행이 끝나죠.
루프의 조건(while square < finalSquare)은 이전과 같지만, 이번에는 루프를 첫 번 통과할 때 끝나고 나서야 평가돼요. repeat-while 루프의 구조는 앞 예시의 while 루프보다 이 게임에 더 잘 맞아요. 위 repeat-while 루프에서 square += board[square]는 루프의 while 조건이 square가 여전히 보드 위에 있다고 확인한 바로 뒤에 항상 실행되죠. 이 동작 덕분에 앞에서 봤던 while 루프 버전의 배열 경계 검사가 필요 없어져요.
조건문
특정 조건에 따라 서로 다른 코드 조각을 실행하는 건 자주 유용해요. 오류가 발생했을 때 추가 코드를 실행하고 싶을 수도 있고, 값이 너무 높거나 낮아졌을 때 메시지를 표시하고 싶을 수도 있죠. 이렇게 하기 위해 코드의 일부를 *조건적(conditional)*으로 만들어요.
Swift는 코드에 조건 분기를 추가하는 두 가지 방법을 제공해요. if 문과 switch 문이죠. 보통 if 문은 가능한 결과가 몇 개뿐인 단순한 조건을 평가할 때 쓰고, switch 문은 가능한 조합이 여러 개인 더 복잡한 조건에 더 잘 맞아요. 패턴 매칭이 실행할 적절한 코드 분기를 고르는 데 도움이 되는 상황에서도 유용하죠.
If
가장 단순한 형태에서 if 문은 단일 if 조건을 가져요. 조건이 true일 때만 문장들의 집합을 실행하죠.
var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
}
// Prints "It's very cold. Consider wearing a scarf."
위 예시는 온도가 32도 화씨(물의 어는점) 이하인지 확인해요. 그렇다면 메시지를 출력하죠. 그렇지 않으면 어떤 메시지도 출력하지 않고, 코드 실행이 if 문의 닫는 중괄호 뒤에서 계속돼요.
if 문은 if 조건이 false인 상황을 위한 대체 문장 집합도 제공할 수 있는데, 이를 else 절이라고 해요. 이 문장들은 else 키워드로 표시돼요.
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."
이 두 분기 중 하나는 항상 실행돼요. 온도가 40도 화씨로 올라갔으니 더 이상 스카프를 권할 만큼 춥지 않고, 그래서 else 분기가 대신 실행되는 거예요.
여러 if 문을 이어 붙여서 추가 절을 고려할 수도 있어요.
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."
여기서는 특히 더운 온도에 응답하도록 if 문이 하나 더 추가됐어요. 마지막 else 절은 그대로 남아서, 너무 덥지도 너무 춥지도 않은 모든 온도에 대한 응답을 출력하죠.
그런데 마지막 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.")
}
온도가 if 조건을 발동시킬 만큼 춥지도 않고 else if 조건을 발동시킬 만큼 덥지도 않으므로, 어떤 메시지도 출력되지 않아요.
Swift는 값을 설정할 때 쓸 수 있는 if의 축약 표기법도 제공해요. 예를 들어 다음 코드를 생각해 봐요.
let temperatureInCelsius = 25
let weatherAdvice: String
if temperatureInCelsius <= 0 {
weatherAdvice = "It's very cold. Consider wearing a scarf."
} else if temperatureInCelsius >= 30 {
weatherAdvice = "It's really warm. Don't forget to wear sunscreen."
} else {
weatherAdvice = "It's not that cold. Wear a T-shirt."
}
print(weatherAdvice)
// Prints "It's not that cold. Wear a T-shirt."
여기서 각 분기는 weatherAdvice 상수에 값을 설정하고, 이 상수는 if 문 뒤에서 출력돼요.
대체 문법인 *if 표현식(if expression)*을 쓰면 이 코드를 더 간결하게 작성할 수 있어요.
let weatherAdvice = if temperatureInCelsius <= 0 {
"It's very cold. Consider wearing a scarf."
} else if temperatureInCelsius >= 30 {
"It's really warm. Don't forget to wear sunscreen."
} else {
"It's not that cold. Wear a T-shirt."
}
print(weatherAdvice)
// Prints "It's not that cold. Wear a T-shirt."
이 if 표현식 버전에서는 각 분기가 단일 값을 담아요. 어떤 분기의 조건이 true라면 그 분기의 값이 weatherAdvice 할당에서 if 표현식 전체의 값으로 사용되죠. 모든 if 분기에는 대응하는 else if 분기나 else 분기가 있어서, 어떤 조건이 true인지와 무관하게 항상 분기 중 하나가 매칭되고 if 표현식이 항상 값을 만들어 내는 걸 보장해요.
할당 문법이 if 표현식 밖에서 시작하므로, 각 분기 안에서 weatherAdvice =를 반복할 필요가 없어요. 대신 if 표현식의 각 분기가 weatherAdvice의 세 가지 가능한 값 중 하나를 만들어 내고, 할당이 그 값을 사용하죠.
if 표현식의 모든 분기는 같은 타입의 값을 담아야 해요. Swift는 각 분기의 타입을 따로 검사하므로, nil처럼 여러 타입에 쓸 수 있는 값은 Swift가 if 표현식의 타입을 자동으로 결정하지 못하게 막아요. 그럴 땐 타입을 명시적으로 지정해야 해요. 예를 들어:
let freezeWarning: String? = if temperatureInCelsius <= 0 {
"It's below freezing. Watch for ice!"
} else {
nil
}
위 코드에서 if 표현식의 한 분기는 문자열 값을, 다른 분기는 nil 값을 가져요. nil 값은 어떤 옵셔널 타입의 값으로도 쓸 수 있으므로, 기본 타입 문서의 타입 어노테이션 부분에서 설명한 대로 freezeWarning이 옵셔널 문자열임을 명시적으로 써야 해요.
이 타입 정보를 제공하는 다른 방법은 freezeWarning에 명시적 타입을 주는 대신 nil에 명시적 타입을 주는 거예요.
let freezeWarning = if temperatureInCelsius <= 0 {
"It's below freezing. Watch for ice!"
} else {
nil as String?
}
if 표현식은 예상치 못한 실패에 응답해서 오류를 던지거나, fatalError(_:file:line:)처럼 결코 반환하지 않는 함수를 호출할 수도 있어요. 예를 들어:
let weatherAdvice = if temperatureInCelsius > 100 {
throw TemperatureError.boiling
} else {
"It's a reasonable temperature."
}
이 예시에서 if 표현식은 예보 온도가 물의 끓는점인 100°C보다 더운지 확인해요. 이렇게 뜨거운 온도는 if 표현식이 텍스트 요약을 반환하는 대신 .boiling 오류를 던지게 만들죠. 이 if 표현식이 오류를 던질 수 있더라도, 앞에 try를 쓰지는 않아요. 오류 처리에 관한 내용은 오류 처리 문서를 참고해요.
위 예시들처럼 if 표현식을 할당의 오른쪽에 쓰는 것 외에도, 함수나 클로저가 반환하는 값으로도 쓸 수 있어요.
Switch
switch 문은 값을 고려해서 여러 가능한 매칭 패턴과 비교해요. 그다음 처음으로 성공적으로 매칭된 패턴에 따라 적절한 코드 블록을 실행하죠. switch 문은 여러 잠재 상태에 응답해야 할 때 if 문의 대안이 돼요.
가장 단순한 형태에서 switch 문은 값을 같은 타입의 값 하나 이상과 비교해요.
switch <#some value to consider#> {
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로 구성되는데, 각 case는 case 키워드로 시작해요. 특정 값과 비교하는 것 외에도 Swift는 각 case가 더 복잡한 매칭 패턴을 지정하는 여러 방법을 제공해요. 이런 옵션들은 이 챕터에서 나중에 설명할게요.
if 문의 본문처럼 각 case는 코드 실행의 별도 분기예요. switch 문은 어느 분기를 선택할지 결정하죠. 이 과정을 고려 중인 값에 대해 *스위칭(switching)*한다고 불러요.
모든 switch 문은 *완전(exhaustive)*해야 해요. 즉, 고려 중인 타입의 모든 가능한 값이 switch case 중 하나와 매칭되어야 해요. 모든 가능한 값에 case를 제공하는 게 적절하지 않다면, 명시적으로 다루지 않는 값을 모두 덮는 기본 case를 정의할 수 있어요. 이 기본 case는 default 키워드로 표시하고 항상 마지막에 와야 해요.
이 예시는 someCharacter라는 소문자 Character 하나를 switch 문으로 고려해요.
let someCharacter: Character = "z"
switch someCharacter {
case "a":
print("The first letter of the Latin alphabet")
case "z":
print("The last letter of the Latin alphabet")
default:
print("Some other character")
}
// Prints "The last letter of the Latin alphabet".
switch 문의 첫 번째 case는 영어 알파벳의 첫 글자인 a를, 두 번째 case는 마지막 글자인 z를 매칭해요. switch는 모든 알파벳 글자뿐 아니라 모든 가능한 문자에 대한 case를 가져야 하므로, 이 switch 문은 a와 z 외의 모든 문자를 매칭하기 위해 default case를 사용해요. 이렇게 하면 switch 문이 완전해지죠.
if 문처럼 switch 문에도 표현식 형태가 있어요.
let anotherCharacter: Character = "a"
let message = switch anotherCharacter {
case "a":
"The first letter of the Latin alphabet"
case "z":
"The last letter of the Latin alphabet"
default:
"Some other character"
}
print(message)
// Prints "The first letter of the Latin alphabet".
이 예시에서 switch 표현식의 각 case는 해당 case가 anotherCharacter와 매칭될 때 사용할 message 값을 담아요. switch는 항상 완전하므로 할당할 값이 항상 존재하죠.
if 표현식과 마찬가지로, 특정 case에 값을 제공하는 대신 오류를 던지거나 fatalError(_:file:line:)처럼 결코 반환하지 않는 함수를 호출할 수도 있어요. 위 예시처럼 switch 표현식을 할당의 오른쪽에 쓸 수도 있고, 함수나 클로저가 반환하는 값으로도 쓸 수 있어요.
암시적 폴스루 없음
C나 Objective-C의 switch 문과 달리 Swift의 switch 문은 각 case의 끝에서 다음 case로 기본적으로 폴스루(fall through)되지 않아요. 대신 처음 매칭된 switch case가 완료되는 즉시 switch 문 전체의 실행이 끝나죠. 명시적인 break 문도 필요 없어요. 이 덕분에 switch 문이 C보다 더 안전하고 사용하기 쉬워지고, 실수로 두 개 이상의 switch case를 실행하는 일도 피할 수 있어요.
참고: Swift에서는
break가 필수는 아니지만, 특정 case를 매칭해서 무시하거나 매칭된 case의 실행이 끝나기 전에 빠져나올 때break문을 쓸 수 있어요. 자세한 내용은 Switch 문 안의 Break 부분을 참고해요.
각 case의 본문은 반드시 실행 가능한 문장을 하나 이상 포함해야 해요. 첫 번째 case가 비어 있으므로 다음 코드를 쓰는 건 유효하지 않아요.
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.
C의 switch 문과 달리 이 switch 문은 "a"와 "A"를 둘 다 매칭하지 않아요. 대신 case "a":가 실행 가능한 문장을 포함하지 않는다는 컴파일 타임 오류를 알려주죠. 이 접근 방식은 한 case에서 다른 case로 우연히 폴스루되는 걸 방지하고, 의도가 더 명확한 더 안전한 코드를 만들어 줘요.
"a"와 "A"를 모두 매칭하는 단일 case를 가진 switch를 만들려면 두 값을 복합 case로 결합하면 돼요. 값들을 쉼표로 구분하죠.
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// Prints "The letter A".
가독성을 위해 복합 case를 여러 줄에 걸쳐 쓸 수도 있어요. 복합 case에 대한 더 자세한 내용은 복합 Case 부분을 참고해요.
참고: 특정
switchcase의 끝에서 명시적으로 폴스루하려면 폴스루 부분에서 설명한 것처럼fallthrough키워드를 사용해요.
구간 매칭
switch case의 값이 구간에 포함되는지 검사할 수 있어요. 이 예시는 숫자 구간을 사용해서 어떤 크기의 숫자도 자연어로 된 표현을 제공해요.
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 문에서 평가돼요. 각 case는 그 값을 숫자나 구간과 비교하죠. approximateCount의 값이 12와 100 사이에 있으므로 naturalCount에는 "dozens of"가 할당되고, 실행은 switch 문 밖으로 이동해요.
튜플
튜플을 사용해서 같은 switch 문에서 여러 값을 검사할 수 있어요. 튜플의 각 요소를 서로 다른 값이나 값의 구간과 비교하죠. 또는 밑줄 문자(_), 즉 와일드카드 패턴을 써서 어떤 가능한 값이든 매칭할 수 있어요.
아래 예시는 간단한 튜플 타입 (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".
switch 문은 점이 원점 (0, 0)에 있는지, 빨간 x축 위에 있는지, 초록 y축 위에 있는지, 원점을 중심으로 한 파란 4x4 박스 안에 있는지, 아니면 박스 밖에 있는지를 결정해요.
C와 달리 Swift는 여러 switch case가 같은 값이나 값들을 고려할 수 있게 해줘요. 사실 점 (0, 0)은 이 예시의 네 case 모두에 매칭될 수 있어요. 하지만 매칭이 여러 개 가능하면 항상 첫 번째 매칭 case가 사용돼요. 점 (0, 0)은 case (0, 0)에 먼저 매칭되므로, 나머지 매칭 case들은 모두 무시되죠.
값 바인딩
switch case는 매칭된 값이나 값들을 임시 상수나 변수에 이름 붙일 수 있어서 case 본문에서 사용할 수 있어요. 이 동작을 *값 바인딩(value binding)*이라고 불러요. 값들이 case 본문 안의 임시 상수나 변수에 바인딩되기 때문이죠.
아래 예시는 튜플 타입 (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 문은 점이 빨간 x축 위에 있는지, 초록 y축 위에 있는지, 아니면 다른 어딘가(어느 축에도 없음)에 있는지를 결정해요.
세 switch case는 x와 y라는 자리표시자 상수를 선언해서, anotherPoint의 튜플 값 하나 또는 둘 다를 일시적으로 받아요. 첫 번째 case인 case (let x, 0)은 y 값이 0인 어떤 점이든 매칭하고 점의 x 값을 임시 상수 x에 할당하죠. 비슷하게 두 번째 case인 case (0, let y)는 x 값이 0인 어떤 점이든 매칭하고 점의 y 값을 임시 상수 y에 할당해요.
임시 상수들이 선언된 뒤에는 case의 코드 블록 안에서 사용할 수 있어요. 여기서는 그 상수들로 점의 분류를 출력하죠.
이 switch 문에는 default case가 없어요. 마지막 case인 case let (x, y)는 어떤 값이든 매칭할 수 있는 두 자리표시자 상수의 튜플을 선언하거든요. anotherPoint는 항상 두 값의 튜플이므로 이 case가 모든 가능한 나머지 값을 매칭하고, 그래서 switch 문을 완전하게 만드는 데 default case가 필요 없어요.
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 문은 점이 x == y인 초록 대각선 위에 있는지, x == -y인 보라 대각선 위에 있는지, 아니면 둘 다 아닌지를 결정해요.
세 switch case는 x와 y라는 자리표시자 상수를 선언해서 yetAnotherPoint의 두 튜플 값을 일시적으로 받아요. 이 상수들은 where 절의 일부로 쓰여 동적 필터를 만들어요. switch case는 where 절의 조건이 그 값에 대해 true로 평가될 때만 point의 현재 값을 매칭하죠.
앞 예시처럼 마지막 case가 모든 가능한 나머지 값을 매칭하므로, switch 문을 완전하게 만드는 데 default case가 필요 없어요.
복합 Case
본문을 공유하는 여러 switch case는 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".
switch 문의 첫 번째 case는 영어의 다섯 소문자 모음을 모두 매칭해요. 비슷하게 두 번째 case는 모든 소문자 자음을 매칭하죠. 마지막으로 default case가 그 외의 어떤 문자든 매칭해요.
복합 case는 값 바인딩도 포함할 수 있어요. 복합 case의 모든 패턴은 같은 값 바인딩 집합을 포함해야 하고, 각 바인딩은 복합 case의 모든 패턴에서 같은 타입의 값을 받아야 해요. 이렇게 하면 복합 case의 어떤 부분이 매칭됐든 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".
위 case에는 두 패턴이 있어요. (let distance, 0)은 x축 위의 점을, (0, let distance)는 y축 위의 점을 매칭하죠. 두 패턴 모두 distance에 대한 바인딩을 포함하고 distance는 두 패턴에서 정수예요. 이는 case 본문의 코드가 항상 distance의 값에 접근할 수 있다는 뜻이에요.
패턴
앞의 예시들에서 각 switch case는 어떤 값이 그 case와 매칭되는지 나타내는 패턴을 포함했어요. if 문의 조건으로도 패턴을 쓸 수 있어요. 어떻게 생겼는지 볼게요.
let somePoint = (12, 100)
if case (let x, 100) = somePoint {
print("Found a point on the y=100 line, at \(x)")
}
// Prints "Found a point on the y=100 line, at 12".
이 코드에서 if 문의 조건은 case로 시작하는데, 조건이 불리언 값이 아니라 패턴이라는 뜻이에요. 패턴이 매칭하면 if의 조건이 true로 간주되고 그래서 if 문 본문의 코드가 실행되죠. if case 뒤에 쓸 수 있는 패턴은 switch case에 쓸 수 있는 패턴과 같아요.
for-in 루프에서는 코드에 case를 쓰지 않더라도 값 바인딩 패턴으로 값의 일부에 이름을 붙일 수 있어요.
let points = [(10, 0), (30, -30), (-20, 0)]
for (x, y) in points {
if y == 0 {
print("Found a point on the x-axis at \(x)")
}
}
// Prints "Found a point on the x-axis at 10".
// Prints "Found a point on the x-axis at -20".
위 for-in 루프는 튜플 배열을 순회하면서 튜플의 첫 번째와 두 번째 요소를 x와 y 상수에 바인딩해요. 루프 안의 문장들은 그 상수를 사용할 수 있는데, 점이 x축 위에 있는지 검사하는 if 문이 그 예시죠. 이 코드를 더 간결하게 쓰는 방법은 값 바인딩과 조건을 결합한 for-case-in 루프를 쓰는 거예요. 아래 코드는 위 for-in 루프와 같은 동작을 해요.
for case (let x, 0) in points {
print("Found a point on the x-axis at \(x)")
}
// Prints "Found a point on the x-axis at 10".
// Prints "Found a point on the x-axis at -20".
이 코드에서 조건이 패턴의 일부로 for-case-in 루프에 통합돼요. for-case-in 루프의 문장들은 x축 위의 점에 대해서만 실행되죠. 이 코드는 위 for-in 루프와 같은 결과를 내지만, 컬렉션에서 특정 요소만 순회하는 더 간결한 방법이에요.
for-case-in 루프에 where 절을 포함해서 추가 조건을 검사할 수도 있어요. 루프 안의 문장들은 where 절이 현재 요소와 매칭될 때만 실행돼요. 예를 들어:
for case let (x, y) in points where x == y || x == -y {
print("Found (\(x), \(y)) along a line through the origin")
}
// Prints "Found (30, -30) along a line through the origin".
이 코드는 튜플의 첫 번째와 두 번째 요소를 상수 x와 y에 바인딩한 뒤, where 절에서 그 값을 검사해요. where 절이 true라면 for 루프 본문의 코드가 실행되고, 그렇지 않으면 다음 요소로 순회를 계속해요.
패턴이 값을 바인딩할 수 있으므로, if-case 문과 for-case-in 루프는 연관 값을 가진 열거형을 다룰 때 유용해요. 열거형 문서의 연관 값 부분을 참고해요.
제어 전달 문장
*제어 전달 문장(control transfer statement)*은 한 코드에서 다른 코드로 제어를 옮겨서 코드가 실행되는 순서를 바꿔요. Swift에는 다섯 개의 제어 전달 문장이 있어요.
continuebreakfallthroughreturnthrow
continue, break, fallthrough 문장은 아래에서 설명할게요. return 문장은 함수 문서에서, throw 문장은 오류 처리 문서에서 설명해요.
Continue
continue 문장은 루프에게 지금 하고 있는 일을 멈추고 루프의 다음 반복 시작부터 다시 시작하라고 말해요. "현재 루프 반복은 이걸로 끝이에요"라고 말하는 거죠. 루프를 완전히 벗어나진 않고요.
다음 예시는 소문자 문자열에서 모음과 공백을 모두 제거해서 수수께끼 같은 문구를 만들어요.
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 문장은 전체 제어 흐름 문장의 실행을 즉시 끝내요. break 문장은 switch나 루프 문장 안에서, 그 switch나 루프 문장의 실행을 평소보다 일찍 종료하고 싶을 때 쓸 수 있어요.
루프 문장 안의 Break
루프 문장 안에서 쓰면 break는 루프의 실행을 즉시 끝내고 루프의 닫는 중괄호(}) 뒤의 코드로 제어를 넘겨요. 현재 루프 반복의 나머지 코드는 실행되지 않고, 루프의 추가 반복도 시작되지 않아요.
Switch 문 안의 Break
switch 문 안에서 쓰면 break는 switch 문의 실행을 즉시 끝내고 switch 문의 닫는 중괄호(}) 뒤의 코드로 제어를 넘겨요.
이 동작은 switch 문에서 하나 이상의 case를 매칭해서 무시하는 데 쓸 수 있어요. Swift의 switch 문은 완전하고 빈 case를 허용하지 않으므로, 때로는 의도를 명확히 하기 위해 case를 의도적으로 매칭해서 무시하는 게 필요해요. 무시하려는 case의 본문 전체를 break 문 하나로 쓰면 되죠. 그 case가 switch 문에 매칭되면 case 안의 break 문이 switch 문의 실행을 즉시 끝내요.
참고: 주석만 포함하는
switchcase는 컴파일 타임 오류로 보고돼요. 주석은 문장이 아니고switchcase를 무시하게 만들지 않으니까요.switchcase를 무시할 때는 항상break문을 쓰세요.
다음 예시는 Character 값을 switch해서 네 언어 중 하나의 숫자 기호인지 결정해요. 간결함을 위해 단일 switch case에서 여러 값을 다루죠.
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
}
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."
이 예시는 numberSymbol이 숫자 1부터 4에 대한 라틴, 아랍, 중국, 태국 기호인지 확인해요. 매칭이 발견되면 switch 문의 case 중 하나가 possibleIntegerValue라는 옵셔널 Int? 변수를 적절한 정수 값으로 설정하죠.
switch 문이 실행을 마친 뒤에는 옵셔널 바인딩을 사용해서 값이 발견됐는지 확인해요. possibleIntegerValue 변수는 옵셔널 타입이므로 nil이라는 암시적 초기 값을 가지는데, 그래서 옵셔널 바인딩은 possibleIntegerValue가 switch 문의 처음 네 case 중 하나에 의해 실제 값으로 설정된 경우에만 성공해요.
위 예시에서 Character의 모든 가능한 값을 나열하는 건 실용적이지 않으므로, default case가 매칭되지 않은 모든 문자를 처리해요. 이 default case는 어떤 동작도 수행할 필요가 없어서 본문을 break 문 하나로 써요. default case가 매칭되는 즉시 break 문이 switch 문의 실행을 끝내고, 코드 실행이 if let 문부터 계속되죠.
Fallthrough
Swift에서 switch 문은 각 case의 끝에서 다음 case로 폴스루되지 않아요. 즉 첫 매칭 case가 완료되는 즉시 switch 문 전체의 실행이 완료되죠. 반면 C는 폴스루를 막기 위해 모든 switch case 끝에 명시적인 break 문을 넣어야 해요. 기본 폴스루를 피한다는 건 Swift switch 문이 C보다 훨씬 간결하고 예측 가능하다는 뜻이며, 그래서 실수로 여러 switch case를 실행하는 일도 피할 수 있어요.
C 스타일의 폴스루 동작이 필요하다면 fallthrough 키워드로 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."
이 예시는 description이라는 새 String 변수를 선언하고 초기 값을 할당해요. 그다음 함수가 switch 문으로 integerToDescribe의 값을 고려하죠. integerToDescribe의 값이 목록에 있는 소수 중 하나라면 함수는 description 끝에 텍스트를 붙여서 그 숫자가 소수임을 알리고, fallthrough 키워드를 써서 default case로도 "떨어져 들어가요". default case가 설명 끝에 추가 텍스트를 더하고 switch 문이 완료돼요.
integerToDescribe의 값이 알려진 소수 목록에 없다면 첫 번째 switch case에 전혀 매칭되지 않아요. 다른 특정 case가 없으므로 integerToDescribe는 default case에 매칭되죠.
switch 문이 실행을 마친 뒤에는 print(_:separator:terminator:) 함수로 숫자의 설명을 출력해요. 이 예시에서 숫자 5는 소수로 올바르게 식별돼요.
참고:
fallthrough키워드는 실행이 떨어져 들어가는switchcase의 case 조건을 검사하지 않아요.fallthrough키워드는 C의 표준switch문 동작처럼 코드 실행을 다음 case(또는defaultcase) 블록 안의 문장으로 직접 이동시킬 뿐이에요.
레이블이 붙은 문장
Swift에서는 루프와 조건문을 다른 루프와 조건문 안에 중첩해서 복잡한 제어 흐름 구조를 만들 수 있어요. 그런데 루프와 조건문 모두 break 문을 사용해서 실행을 일찍 끝낼 수 있어요. 그래서 어떤 루프나 조건문을 break 문이 종료시키길 원하는지 명확히 하는 게 때로 유용하죠. 비슷하게 중첩 루프가 여러 개라면 continue 문이 어떤 루프에 영향을 줘야 하는지 명확히 하는 게 유용할 수 있어요.
이런 목적을 위해 루프 문장이나 조건문에 *문장 레이블(statement label)*을 붙일 수 있어요. 조건문에서는 break 문과 함께 문장 레이블을 써서 레이블이 붙은 문장의 실행을 끝낼 수 있죠. 루프 문장에서는 break나 continue 문과 함께 문장 레이블을 써서 레이블이 붙은 문장의 실행을 끝내거나 계속할 수 있어요.
레이블이 붙은 문장은 문장을 도입하는 키워드와 같은 줄에 레이블을 두고 콜론을 붙여서 표시해요. while 루프에 대한 이 문법의 예시가 아래에 있는데, 원리는 모든 루프와 switch 문에서 같아요.
<#label name#>: while <#condition#> {
<#statements#>
}
다음 예시는 이 챕터 앞에서 봤던 뱀과 사다리 게임을 각색한 버전에서 레이블이 붙은 while 루프와 함께 break와 continue 문을 사용해요. 이번에는 게임에 추가 규칙이 있어요.
- 이기려면 25번 칸에 정확히 도착해야 해요.
어떤 주사위 굴림이 25번 칸을 넘어가게 한다면, 25번 칸에 도착하기 위해 필요한 정확한 숫자가 나올 때까지 다시 굴려야 해요.
게임 보드는 이전과 같아요.
finalSquare, board, square, diceRoll의 값은 이전과 같은 방식으로 초기화돼요.
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
이 버전의 게임은 while 루프와 switch 문으로 게임 로직을 구현해요. while 루프에는 gameLoop라는 문장 레이블이 붙어서 뱀과 사다리 게임의 주요 게임 루프임을 나타내죠.
while 루프의 조건은 while square != finalSquare인데, 25번 칸에 정확히 도착해야 한다는 점을 반영한 거예요.
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!")
각 루프가 시작될 때 주사위를 굴려요. 플레이어를 즉시 움직이는 대신 루프는 switch 문으로 이동 결과를 고려해서 그 이동이 허용되는지 결정하죠.
- 주사위 굴림이 플레이어를 마지막 칸으로 이동시키면 게임이 끝나요.
break gameLoop문이 제어를while루프 밖의 첫 번째 코드 줄로 넘겨서 게임을 끝내죠. - 주사위 굴림이 플레이어를 마지막 칸 너머로 이동시키면 그 이동은 유효하지 않고 플레이어는 다시 굴려야 해요.
continue gameLoop문이 현재while루프 반복을 끝내고 루프의 다음 반복을 시작해요. - 그 외의 모든 경우는 주사위 굴림이 유효한 이동이에요. 플레이어가
diceRoll칸만큼 앞으로 이동하고, 게임 로직이 뱀과 사다리를 확인해요. 그러면 루프가 끝나고 제어가while조건으로 돌아가 다른 턴이 필요한지 결정해요.
참고: 위
break문이gameLoop레이블을 쓰지 않았다면while문이 아니라switch문 밖으로 빠져나갔을 거예요.gameLoop레이블을 쓰면 어느 제어 문장을 종료해야 하는지 분명해지죠.
continue gameLoop로 루프의 다음 반복으로 점프할 때gameLoop레이블을 쓰는 건 꼭 필요한 건 아니에요. 게임에는 루프가 하나뿐이라continue문이 어느 루프에 영향을 줄지 모호하지 않으니까요. 하지만continue문에도gameLoop레이블을 쓰는 건 나쁠 게 없어요.break문과 함께 쓴 레이블과 일관되고, 게임 로직을 읽고 이해하기 더 명확하게 만들어 주니까요.
조기 종료
guard 문은 if 문처럼 표현식의 불리언 값에 따라 문장을 실행해요. guard 문은 guard 문 뒤의 코드가 실행되기 위해 조건이 반드시 true여야 한다고 요구할 때 써요. if 문과 달리 guard 문은 항상 else 절을 가져요. 조건이 true가 아니면 else 절 안의 코드가 실행되죠.
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 문의 닫는 중괄호 뒤에서 계속돼요. 조건의 일부로 옵셔널 바인딩을 사용해 값을 할당받은 변수나 상수는 guard 문이 있는 코드 블록의 나머지 부분에서 사용할 수 있어요.
조건이 충족되지 않으면 else 분기 안의 코드가 실행돼요. 그 분기는 제어를 이동해 guard 문이 있는 코드 블록을 빠져나가야 해요. return, break, continue, throw 같은 제어 전달 문장으로 그렇게 할 수도 있고, fatalError(_:file:line:)처럼 반환하지 않는 함수나 메서드를 호출할 수도 있어요.
요구 사항을 확인하는 데 guard 문을 쓰면 같은 검사를 if 문으로 하는 것보다 코드 가독성이 좋아져요. 보통 실행되는 코드를 else 블록으로 감싸지 않고 쓸 수 있고, 요구 사항을 위반할 때 처리하는 코드를 그 요구 사항 바로 옆에 둘 수 있으니까요.
지연된 동작 (defer)
if나 while 같은 제어 흐름 구조가 코드 일부를 실행할지, 몇 번 실행할지를 통제한다면, defer는 코드 일부가 언제 실행될지를 통제해요. defer 블록은 나중에, 프로그램이 현재 스코프의 끝에 도달했을 때 실행될 코드를 쓰는 데 사용해요. 예를 들어:
var score = 1
if score < 10 {
defer {
print(score)
}
score += 5
}
// Prints "6".
위 예시에서 defer 블록 안의 코드는 if 문 본문을 벗어나기 전에 실행돼요. 먼저 if 문 안의 코드가 실행되어 score를 5만큼 증가시키죠. 그런 다음 if 문의 스코프를 벗어나기 전에 지연된 코드가 실행되어 score를 출력해요.
defer 안의 코드는 프로그램이 그 스코프를 어떻게 벗어나든 항상 실행돼요. 함수에서 일찍 종료하는 것, for 루프를 빠져나가는 것, 오류를 던지는 것 모두 포함되죠. 이 동작 덕분에 defer는 한 쌍의 동작이 일어나는 걸 보장해야 하는 작업에 유용해요. 메모리를 수동으로 할당하고 해제하거나, 저수준 파일 디스크립터를 열고 닫거나, 데이터베이스에서 트랜잭션을 시작하고 끝내는 같은 작업이죠. 두 동작을 코드에서 나란히 쓸 수 있으니까요. 예를 들어 다음 코드는 한 코드 조각 안에서 100을 더하고 빼서 점수에 임시 보너스를 줘요.
var score = 3
if score < 100 {
score += 100
defer {
score -= 100
}
// Other code that uses the score with its bonus goes here.
print(score)
}
// Prints "103".
같은 스코프에 defer 블록을 두 개 이상 쓰면, 먼저 지정한 것이 마지막에 실행돼요.
if score < 10 {
defer {
print(score)
}
defer {
print("The score is:")
}
score += 5
}
// Prints "The score is:".
// Prints "6".
프로그램이 실행을 멈추면 — 예를 들어 런타임 오류나 크래시 때문에 — 지연된 코드는 실행되지 않아요. 하지만 오류가 던져진 뒤에는 지연된 코드가 실행돼요. 오류 처리와 함께 defer를 쓰는 방법은 오류 처리 문서를 참고해요.
API 가용성 확인
Swift는 API 가용성 확인을 기본적으로 지원해요. 이는 주어진 배포 대상에서 사용할 수 없는 API를 실수로 사용하지 않도록 보장해 주죠.
컴파일러는 SDK의 가용성 정보를 사용해서 프로젝트가 지정한 배포 대상에서 코드에 사용된 모든 API를 사용할 수 있는지 확인해요. 사용할 수 없는 API를 쓰려고 하면 Swift가 컴파일 타임에 오류를 보고해요.
if나 guard 문에서 *가용성 조건(availability condition)*을 사용해서, 런타임에 사용하려는 API를 사용할 수 있는지에 따라 코드 블록을 조건적으로 실행할 수 있어요. 컴파일러는 그 코드 블록 안의 API를 사용할 수 있는지 확인할 때 가용성 조건의 정보를 사용해요.
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에서는 if 문의 본문이 iOS 10 이상에서만, macOS에서는 macOS 10.12 이상에서만 실행된다고 지정해요. 마지막 인자 *는 필수인데, 다른 어떤 플랫폼에서는 if 본문이 대상이 지정한 최소 배포 대상에서 실행된다고 지정해요.
일반적인 형태에서 가용성 조건은 플랫폼 이름과 버전의 목록을 받아요. iOS, macOS, watchOS, tvOS, visionOS 같은 플랫폼 이름을 사용하죠. 전체 목록은 속성 문서를 참고해요. iOS 8이나 macOS 10.10 같은 주 버전 번호를 지정하는 것 외에도 iOS 11.2.6이나 macOS 10.13.3 같은 부 버전 번호를 지정할 수 있어요.
if #available(<#platform name#> <#version#>, <#...#>, *) {
<#statements to execute if the APIs are available#>
} else {
<#fallback statements to execute if the APIs are unavailable#>
}
가용성 조건을 guard 문과 함께 쓰면, 그 코드 블록 안의 나머지 코드에 사용되는 가용성 정보가 세분화돼요.
@available(macOS 10.12, *)
struct ColorPreference {
var bestColor = "blue"
}
func chooseBestColor() -> String {
guard #available(macOS 10.12, *) else {
return "gray"
}
let colors = ColorPreference()
return colors.bestColor
}
위 예시에서 ColorPreference 구조체는 macOS 10.12 이상이 필요해요. chooseBestColor() 함수는 가용성 가드로 시작하죠. 플랫폼 버전이 너무 오래되어 ColorPreference를 쓸 수 없다면 항상 사용 가능한 동작으로 대체해요. guard 문 뒤에서는 macOS 10.12 이상이 필요한 API를 사용할 수 있어요.
#available 외에도 Swift는 비가용성 조건(unavailability condition)으로 반대 검사를 지원해요. 예를 들어 다음 두 검사는 같은 일을 해요.
if #available(iOS 10, *) {
} else {
// Fallback code
}
if #unavailable(iOS 10) {
// Fallback code
}
검사가 대체 코드만 포함할 때 #unavailable 형태를 쓰면 코드를 더 읽기 쉽게 만들어 줘요.