Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Control Flow 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
Swift는 C 스타일 언어와 유사한 스타일의 Control Flow Statements를 제공한다. task를 여러 번 실행하기 위해 while 문을, 조건에 따라 다른 코드를 수행할 수 있도록 if, guard, switch 문을, 수행 도중 특정 코드로 건너뛸 수 있도록 break, continue 문을 제공한다.
Swift는 for-in loop를 제공한다. 이것은 array, dictionary, range, string 등을 순회할 때 유용하게 사용할 수 있다. 뒤에서 살펴보겠다.
Swift의 Switch 문은
- fall through (break가 나오기 전까지 쭉 다음 case를 연달아 실행) 하지 않는다. break를 빼먹어서 코드 상 오류가 발생하는 것을 방지하기 위함이다.
- Objective-C에 비하여 각각의 Case 조건을 좀 더 자세하게 정의할 수 있다. case를 range, tuple 등 다양한 패턴으로 만들 수 있으며, where 절을 사용할 수도 있다. 뒤에서 살펴보겠다.
For-In Loops
Swift는 sequence 의 각 아이템을 하나씩 돌면서 수행할 수 있는 for-in loop 를 제공한다.
<주의> Objective-C와 Swift2까지 제공했던 기존의 C-Style for 문은 Swift3에서 더이상 사용할 수 없다.
for initialization; condition; increment {
statements
}
// 위의 문법은 Swift3에서 컴파일 에러
for-in loop는 Sequence iteration 에 유용하게 사용할 수 있다. 예를 들면 특정 범위 지정, array 순회, string의 각 character 순회 등에 사용할 수 있다.
for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
// 이때 index는 자동으로 값이 채워지는 constant이며,
// 미리 선언이 될 필요가 없이 컴파일러가 유추해준다.
// index 가 아닌 다른 이름이어도 상관없음
}
// 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
let base = 3
let power = 10
var answer = 1
for _ in 1...power {
answer *= base
// for 문이 현재 몇 번 반복되었는지 알 필요가 없는 경우에는
// 이렇게 언더바를 사용해서 생략해도 된다.
}
print("\(base) to the power of \(power) is \(answer)")
// prints "3 to the power of 10 is 59049”
// for-in 을 사용하여 array iteration
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!
// for-in 을 사용하여 dictionary iteration
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
// dictionary의 key와 value를 decompose
print("\(animalName)s have \(legCount) legs")
}
// ants have 6 legs
// cats have 4 legs
// spiders have 8 legs
참고로 Sequence 프로토콜이라는 것이 있는데, 이것을 지원하는 타입은 for in loop에 쓸 수 있다 (커스텀 클래스도 구현하면 가능하다) Collection 타입들은 SequenceType을 지원해주기 때문에 for in loop 에 사용될 수 있다.
While Loops
While 문은 조건이 false가 되기 전까지 계속 반복을 하게된다. 몇 번 반복이 되어야 할지를 미리 알 수 없을 때 While 문을 사용하면 유용하다.
Swift가 제공하는 두 가지 While loop다.
- while : loop 시작하기 전마다 조건을 검사. 따라서 최소 0번 반복.
- repeat-while : loop 끝날 때마다 조건을 검사. 따라서 최소 1번 반복.
(1) While
while condition {
statements
}
condition이 true면 loop를 돌기 시작해서, condition이 false가 될 때까지 멈추지 않는다.
앞으로 뱀과 사다리 게임을 예제로 들어 While 예제코드를 설명하겠다.
<Rule> 주사위를 던져서 1부터 25까지 도달하면 성공. 사다리 아랫면에 도착하면 사다리를 타고 윗면으로 올라간다. 뱀 머리에 도착하면 뱀을 타고 뱀 꼬리로 내려간다.
let finalSquare = 25 // 게임판의 크기를 먼저 상수로 정의
var board = [Int](repeating: 0, count: finalSquare + 1)
// 게임판은 0번자리부터 25번자리까지 있음. 따라서 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 // 뱀칸
// <참고> 코드 가독성을 위해 + 연산자를 썼고 02 같은 표기를 했다고 한다
// 게임 참가자는 먼저 0번자리(게임판 바깥)에 있다가
// 주사위를 굴려 그 수만큼 가게 된다.
var square = 0
var diceRoll = 0
while square < finalSquare {
// 주사위 굴리는 부분. 예제에서는 그냥 +1을 하는 것으로 대체.
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 주사위 결과만큼 앞으로 간다.
square += diceRoll
if square < board.count { //이미 게임판을 통과하지는 않았는지
// 사다리 또는 뱀의 동작. 아무 칸도 아니라면 +0이 될 것이다.
square += board[square]
}
}
// square가 finalSquare보다 커지거나 같을때 while문 종료.
// 즉게임판을 통과했다는 말이 된다.
print("Game over!")”
이 게임에서는 참가자가 몇 번 주사위를 굴려야 게임이 끝날지 미리 알 수 없다. 따라서 이런 경우에는 While문을 유용히 쓸 수 있다.
(2) Repeat-While
Do-While이라고 생각하면 된다.
repeat {
statements
} while condition
loop block 을 일단 한 번 돌고, 그 다음 condition을 검사한다. condition이 false가 될 때까지 반복한다.
이번에도 뱀과 사다리 게임을 예로 들어보겠다.
repeat {
// 사다리 또는 뱀의 동작. 아무 칸도 아니라면 +0이 될 것이다.
// loop 문 최초실행때는 board[0]에 0이 들어있으므로 문제없다.
square += board[square]
// 주사위 굴리기
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 주사위 굴린 만큼 이동
square += diceRoll
} while square < finalSquare
print("Game over!")
앞서 While 문과 비교를 해보자. 둘 다 조건문은 square < finalSquare 로 동일하다. 그러나 While문에서는 square += board[square] 를 수행하기 전 square < board.count 조건을 한번더 검사해야 했었다(범위넘은 Array접근 방지). 그러나 Repeat-While 문에서는 그 조건검사가 while 조건문(square < finalSquare)을 통해 확인이 되기 때문에 별도의 검사없이 바로 수행할 수 있는 것이다. 이런 경우에는 Repeat-While 사용이 유리할 수 있다.
이렇게 가이드에는 나와있는데, While문 역시 square += board[square] 을 먼저 수행하고 주사위를 굴리면 똑같은 것 아닌가?
while square < finalSquare {
square += board[square]
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
square += diceRoll
}
print("Game over!")
이런 식으로 말이다. 내가 잘 이해를 못한 것 같은데... 잘 모르겠다.
Conditional Statements
Swift에서 제공하는 조건문에는 두 가지 종류가 있다. if와 switch이다. if는 주로 조건이 간단하며 몇 개 되지 않을 때 사용한다. switch는 조건의 종류가 복잡하고 다양할 때, 그리고 패턴매칭 케이스에서 유용하게 사용할 수 있다.
(1) 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."
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.”
(2) Switch
if (변수A == 값1) { 실행코드 } else if (변수A == 값2 또는 변수A == 값3일 때) { 실행코드 } else { 실행코드 } 같은 조건문이 필요할 때 Switch를 사용하면 좀 더 직관적이고 편리하다. 이때 case에 매칭할 값들과 비교할 변수의 타입은 일치해야 한다.
case 각각은 서로 독립되어 있는 코드 실행 브랜치(separate branch of code execution)이다. case 각각은 실행될 코드 구문을 최소 1개는 포함해야 한다. Switch문은 어느 case를 선택해서 실행할지 결정하게 되고, 이것을 switching 한다고 표현한다.
Switch statement must be exhaustive : 모든 switch문은 철저해야 한다. 비교하려는 값과 매칭이 되는 case가 반드시 있어야 한다 (즉, 빼먹은 case가 생기면 안 된다). 매칭되는 모든 case를 전부 정의할 수 없는 경우에는 default case를 정의할 수 있다. if의 else에 해당하는 부분이다. default case는 항상 맨 마지막에 위치해야 한다.
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"
<No Implicit Fallthrough>
C나 Objective-C에서는 Switch가 fallthrough(break가 나오기 전까지 쭉 다음 case를 연달아 실행)의 특성을 가진다. 그러나 Swift에서는 맨 먼저 매칭되는 case 만 실행이 되고 switch문의 실행이 끝난다. break문을 따로 명시할 필요도 없고, 의도하지 않게 여러 case가 실행될 일도 없다.
<note> Swift의 Switch에서 break문을 쓸 경우는 case 실행이 되는 도중 빠져나올 때이다. 나중에 설명한다.
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // invalid! case has an empty body
case "A":
print("The letter A")
default:
print("Not the letter A")
}
// case "a"에 해당하는 실행구문이 없으므로 컴파일 에러
// 하지만 C에서는 fallthrough 가 적용되므로 "The letter A"가 출력된다
// Swift 에서는 이런 식으로 작성해야한다
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A": // 콤마로 구분. 줄을 나눠써도 된다.
print("The letter A")
default:
print("Not the letter A")
}
Note : To explicitly fall through at the end of a particular switch case, use the fallthrough keyword.
<Interval Matching>
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
var 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문 안에서 여러 개의 값을 조건에 사용하고 싶다면 Tuple을 이용하면 된다. 꼭 매칭이 필수적이지 않은 값에는 언더바(_)를 사용하면 된다.
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("(0, 0) is at the origin")
case (_, 0):
print("(\(somePoint.0), 0) is on the x-axis")
case (0, _):
print("(0, \(somePoint.1)) is on the y-axis")
case (-2...2, -2...2):
print("(\(somePoint.0), \(somePoint.1)) is inside the box")
default:
print("(\(somePoint.0), \(somePoint.1)) is outside of the box")
}
// prints "(1, 1) is inside the box"
// 만약 비교대상이 (0,0) 였다면 모든 case에 포함이 되지만
// 위에서부터 가장 먼저 매칭된 케이스가 실행되고 끝난다.
<Value Bindings>
Value Binding이란 case에 매칭된 값을 임시 상수/변수에 할당해놓는 것을 말한다. case의 실행코드 내에서 그 임시 상수/변수를 사용할 수 있다. (scope도 거기까지이다)
let anotherPoint = (2, 0) // (a, b)로 지칭해보면
switch anotherPoint {
case (let x, 0): // b가 0이면 여기로 오고, x의 값이 2가 된다
print("on the x-axis with an x value of \(x)")
case (0, let y): // a가 0이면 여기로 오고, y의 값이 0이 된다
print("on the y-axis with a y value of \(y)")
case let (x, y): // 모든 경우 여기로 오고, x가 2, y가 0 이 된다
print("somewhere else at (\(x), \(y))")
}
// prints "on the x-axis with an x value of 2"
// <note> 모든 case 를 정의했기 때문에 default case는 필요가 없다
<Where>
case 정의에 where절을 사용할 수도 있다.
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
// 물론 case let (x, y) where yetAnotherPoint.0 == yetAnotherPoint.1 도 가능하고
// case (_, _) where yetAnotherPoint.0 == yetAnotherPoint.1: 도 가능
// 하지만 가독성을 위해 예제처럼 하는 것이 좋을 것 같다
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
// var (x, y) 로 받으면 이 안에서 x, y 값을 변경할 수 있다.
// 그러나 원래의 yetAnotherPoint에는 아무 영향도 없다.(저건 value type이므로)
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 로 만들 수 있다. 여러 라인에 걸쳐써도 상관없다.
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) is not a vowel or a consonant")
}
// prints "e is a vowel"
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"
Control Transfer Statements
control을 이동시켜 코드의 실행 순서를 바꿔줄 수 있다.
Swift에는 다음과 같은 control transfer statement들이 있다.
- continue
- break
- fallthrough
- return
- throw
이 중에서 return과 throw는 나중에 다룹니다.
<Continue>
루프의 현재 이터레이션을 지금 중단하고 바로 다음 이터레이션을 시작하라는 명령이다. 루프를 중단하는 것이 아님! continue 는 단순히 루프 본문의 나머지 코드를 스킵하는 것이라고 생각해야 한다.
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
let charactersToRemove: [Character] = ["a", "e", "i", "o", "u", " "]
for character in puzzleInput.characters {
if charactersToRemove.contains(character) {
continue
} else {
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// prints "grtmndsthnklk"
<Break>
break문은 control flow statement를 즉각적으로 완전히 종료시킨다. 루프문이나 switch문 안에서 사용할 수 있고, 루프문의 실행과 switch문의 case 실행을 중단하고 바로 종료시키고 싶을 때 사용한다.
<Break in a Loop Statement>
루프문(for, while 같은)에서 쓰이는 break 문은 루프의 실행을 즉시 종료시키고 다음 이터레이션은 실행되지 않는다. continue와 차이가 나는 부분이다.
<Break in a Switch Statement>
Switch 문에서 쓰이는 break문은 Switch문 전체를 즉각 종료하는 역할을 한다. break는 Switch문에서 유용하게 쓰이게 된다. Switch문은 항상 모든 case를 정의해야 하기 때문에 딱히 코드 실행이 필요하지 않은 case도 전부 정의가 되어야 하기 때문이다. 그런데 case가 유효하기 위해서는 한 줄이라도 실행코드가 있어야 한다. 이런 case에 break를 사용하면 된다.
let numberSymbol: Character = "三" // 석 삼
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break // 아무것도 하지 않고 Switch문을 끝낼 수 있다
}
if let integerValue = possibleIntegerValue {
print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
print("An integer value could not be found for \(numberSymbol).")
}
// prints "The integer value of 三 is 3."
<Fallthrough>
Swift의 Switch에서도 C언어의 Fallthrough가 되게 하고 싶다면, 즉 case 실행 뒤 바로 뒤의 case를 이어서 실행하고 싶다면 fallthrough 키워드를 case 본문 마지막줄에 넣는다. 다음 case는 조건체크 없이 무조건 실행이 된다.
또한 fallthrough 키워드를 case 본문 중간에 삽입한다면, 해당 case의 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."
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 100: // 이 조건은 참이 아니지만 그것과 상관없이 코드실행.
description += "100"
fallthrough
case 200:
description += "200"
// 여기에 fallthrough가 없으므로 여기서 switch문이 중단된다.
case 300:
description += "300"
default:
description += " an integer."
}
// case 300과 default는 실행되지 않는다
Note : fallthrough 는 단순히 다음 case도 실행할지 말지를 결정해주는 것이다. 특정 case로 이동시킬 수는 없다.
<Labeled Statements>
루프문이나 switch문에 따로 이름을 붙일 수 있다.
왜 이럴 필요가 있는가 하면, 코드를 작성하다보면 루프문 안에 switch문을 넣는 경우, 또는 그 반대, 또는 루프문안에 루프문을 넣는 경우도 있을 것이다. 이럴 때 (1) break 같은 명령을 어떤 루프에 적용할지를 알려주기 위하여 (2) 어떤 루프가 어떤 목적을 위해 만들어진 것인지 명시하기 위하여, 따로 이름을 붙일 수 있도록 지원을 해준다.
이름 붙이는 방법은 이렇게 하면 된다.
- 만약 break gameLoop 가 아니라 break만 했다면 while문이 아니라 switch문에 적용이 되어 switch문만 종료되었을 것이다.
- continue gameLoop 대신 continue만 써도 사실 문제는 없다. continue를 적용할 수 있는 루프문이 while문 하나 밖에 없기 때문이다. 그러나 코드 상에서 일관성을 유지하고, 의미를 명확히 하기 위해서 이름을 명시한 것이다.
Early Exit
guard문은 if문처럼 Boolean 조건에 따라 실행을 다르게 할 수 있도록 해준다. if문과 다르게 항상 else 절이 필요하다. guard문은 반드시 어떤 조건이 참이 되어야 그 이후의 코드를 실행할 수 있는 경우에 사용하면 유용하다.
함수의 맨 앞에 체크가 필요한 모든 guard 문을 모아주는 것이 깔끔해보인다.
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
// 나머지 코드 블럭에서도 name을 사용할 수 있다.
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."
// 추가설명1
// 아래의 guard 문은
guard let name = person["name"] else {
return
}
// 아래의 if 문으로 표현할 수 있지만, 차이가 있다.
if let name = person["name"] {
// if 조건 뒤에는 반드시 대괄호 쌍이 있어야 하며
// name 의 scope가 여기서 끝이 난다는 것이다
} else {
return
}
// guard는 이처럼 뎁스를 줄여주는 효과가 있어 Early Exit에 유용하다.
// 추가설명2
guard let name = person["name"] else {
// guard의 else문 안에는 반드시 흐름을 중단하는 코드가 들어있어야 한다.
// return, break, continue, throw, 또는 fatalError(_:file:line:) 등
// 그렇지 않다면 컴파일 에러가 난다
}
Checking API Availability
if 또는 guard 에서 availability condition 을 사용하여 플랫폼과 OS버전을 체크할 수 있다.
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에서부터 좀 변경이 된 API를 사용하고 싶을 때 활용할 수 있다. if 문 안에는 변경이 된 것대로 구현을 하고, else 문 안에는 변경이 되기 전대로 구현을 하면 된다.
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Closures (0) | 2017.02.21 |
---|---|
Functions (0) | 2017.02.19 |
Collection Types - Dictionary (0) | 2017.02.11 |
Collection Types - Set (0) | 2017.02.11 |
Collection Types - Array (0) | 2017.02.11 |