Search

[Swift] Control Flow - Loops

The Swift Programming Language Documents를 참고해서 정리했습니다. 여러 가지 생각들이 버무러져 있습니다.
contents
*이전 포스팅은 [Swift] The Basics 에서 보실 수 있습니다.

Control Flow

Swift는 다양한 Control Flow를 제공합니다.
for-in, while과 같은 반복문, 특정 조건에 따른 분기문, guard, switch, 그리고 실행 흐름 중간에 다른 지점으로 보내주는 break, continue 등을 제공해줍니다.
제공해주는 Control Flow를 사용해서 코드의 흐름을 원하는대로 제어할 수 있습니다.

For-in

Swift에서는 배열에 있는 항목이나, 숫자 범위, String에 있는 각 Character와 같은 시퀀스를 반복하기 위해서 for-in 루프를 사용하게 됩니다.
다른 언어들은 for문으로 배열에 있는 항목이나 숫자 범위를 반복하게 됩니다.
하지만, Swift에서는 for-in 루프를 사용해서 배열에 있는 항목들을 가져오고 숫자 범위를 반복할 수 있습니다.
흔하게 봤던 for문은 이런 형식을 가졌을 겁니다.
for(int i = 0; i < 10; i++) { }
C++
복사
for-in 루프는 조금 다른 형식을 가집니다.
for i in 0..<10 { }
Swift
복사
위의 두 코드는 같은 의미를 가지지만 i라는 값이 설정되는 방식이 조금 다릅니다.
for문의 경우에는 ⓵ 첫번째 부분 int i = 0에 의해서 값이 0으로 초기화되는 과정을 먼저 겪습니다. 그리고, ⓶ 두번째 부분에서 반복문이 종료되는 조건을 겁니다. ⓷ 마지막 부분에서는 매번 for문을 한 번 돌고나서 어떤 작업을 해줄지를 적습니다. 위의 예시에서는 i를 1씩 증가시키는 일을 하라고 작성했네요.
그러면, for-in 루프는 어떻게 이해하면 될까요?
for-in 루프는 in 뒷 부분에서 값이 하나씩 나와서 i로 설정된다고 보시면 됩니다.
그렇기 때문에, for문처럼 값을 1씩 증가시키라는 코드없이도 값이 커질 수 있는겁니다. 0부터 9까지의 숫자가 i로 알아서 설정되니깐요.
for-infor문과 어떤 차이가 있는지 알아봤으니, 어떤 타입들이 for-in을 사용해서 반복적으로 값을 가져올 수 있는지 알아봅시다.
먼저, 위에서 봤던 숫자 범위를 넣을 수 있습니다. 원하는 Range를 넣으면 해당 Range만큼 for-in 블록이 반복하게 됩니다.
for index in 1...9 { print("5 * \(index) = \(index * 5)") }
Swift
복사
Range에 해당하는 값을 가지고 나오는 index상수입니다.
그렇기 때문에, 따로 선언해주지 않더라도 for-in 루프 안에서는 해당 상수를 자유롭게 사용할 수 있습니다. 하지만, 상수이기 때문에 해당 값을 다른 값으로 변경하진 못합니다.
만약, for-in 루프를 사용해서 “안녕”이라는 단어를 10번 반복해서 출력한다고 해봅시다.
이런 경우에는 Range로부터 나오는 숫자가 불필요하게 됩니다. 그냥 단어를 10번 출력하는 것이 중요하니깐요. 그렇다면, 굳이 index라는 상수를 만들 필요가 없게 됩니다.
for-in에서 주는 상수가 불필요하다면 _ 로 무시할 수 있습니다.
for _ in 0..<10 { print("안녕") }
Swift
복사
숫자 범위뿐만 아니라 배열에서도 값을 가지고 올 수 있습니다.
숫자 범위 안에서 숫자를 하나씩 빼온 것처럼 배열 안에서도 항목을 차례대로 하나씩 빼옵니다.
let animal = ["고양이", "강아지", "거북이", "햄스터", "앵무새"] for pet in animal { print("우리집에 사는 귀여운 \(pet)") } /* 우리집에 사는 귀여운 고양이 우리집에 사는 귀여운 강아지 우리집에 사는 귀여운 거북이 우리집에 사는 귀여운 햄스터 우리집에 사는 귀여운 앵무새 */
Swift
복사
숫자 범위와 동일하게 for-in주어지는 상수에는 배열의 항목 값이 들어가게 됩니다.
배열말고도 Dictionary도 for-in을 사용해서 항목을 가져올 수 있습니다.
물론, Dictionary는 순서가 없는 Collection이기 때문에 상수에 들어오는 항목의 순서가 랜덤합니다.
let pair = ["고양이": "Nick", "강아지": "Snoopy", "거북이": "Skip", "햄스터": "Dake", "앵무새": "Duna"] for p in pair { print("\(p.value)의 귀여운 \(p.key)") } /* Dake의 귀여운 햄스터 Skip의 귀여운 거북이 Duna의 귀여운 앵무새 Nick의 귀여운 고양이 Snoopy의 귀여운 강아지 */
Swift
복사
Dictionary는 Key-Value쌍의 항목을 가지고 있기 때문에, 주어지는 상수의 타입이 튜플 형식입니다.
튜플 형식이기 때문에 이를 튜플 형식의 명시적 상수로 분해해서 사용할 수도 있습니다.
for (pet, owner) in pair { print("\(owner)의 귀여운 \(pet)") }
Swift
복사
현재까지 알아본 for-in 루프를 보면 순서대로 들어오는 값에 대한 처리만 할 수 있는 것처럼 보입니다.
만약, 0부터 10까지 1씩 증가하지 않고 2씩 증가하는 값이 필요하다면 어떻게 해야할까요?
숫자 범위는 이 문제를 해결할 수 없습니다. for문처럼 얼마씩 증가한다는걸 작성할 수 있어야 합니다.
이를 도와줄 수 있는 함수가 바로 stride 입니다. stride는 어디서부터 어디까지 몇 씩 증가 혹은 감소한다는 내용을 적을 수 있습니다.
for index in stride(from: 1, through: 9, by: 2) { print("5 * \(index) = \(index * 5)") } /* 5 * 1 = 5 5 * 3 = 15 5 * 5 = 25 5 * 7 = 35 5 * 9 = 45 */
Swift
복사
through해당 숫자를 포함해서 반복문을 돕니다. 따라서, 9까지 값이 나오는걸 볼 수 있어요.
through말고도 to가 있는데, to해당 숫자 전까지만 반복문을 돕니다.
for index in stride(from: 1, to: 9, by: 2) { print("5 * \(index) = \(index * 5)") } /* 5 * 1 = 5 5 * 3 = 15 5 * 5 = 25 5 * 7 = 35 */
Swift
복사
그렇기 때문에, 9 이전까지만 값이 나오는걸 볼 수 있어요.
만약, 직접 만든 타입을 for-in 루프에서 사용하고 싶다면 한 가지 프로토콜을 준수해주어야 합니다.
바로, Sequence 프로토콜입니다. 위에서 사용했던 Collection 타입도 Sequence 프로토콜을 따릅니다.
protocol Collection<Element> : Sequence
Swift
복사

Sequence

Sequence는 Sequence가 가지는 Element 타입에 대한 순차적이고, 반복적인 접근을 제공하는 프로토콜입니다. 순차적인 접근을 도와주기 때문에, Sequence를 준수하는 타입은 한 번에 하나씩 단계별로 항목을 가져올 수 있습니다. 가장 일반적으로는 for-in 루프를 사용해서 항목을 하나씩 가져옵니다.
Sequence 프로토콜은 Sequence(항목들을 가진 리스트)를 쉽게 다룰 수 있는 메서드를 많이 제공합니다.
해당 Collection 내부에 특정 항목이 있는지 확인하는 contains 메서드도 Sequence 프로토콜에서 제공해주는 메서드입니다. 그 외에도 많은 메서드들이 Sequence 프로토콜에 의해서 제공됩니다.
그러면, 커스텀 타입이 Sequence를 준수하려면 어떤 부분을 신경써야 할까요?
Sequence 프로토콜은 Sequence에 들어가는 Element의 타입과 Iterator라는 IteratorProtocol 타입을 설정해주어야 합니다. 따라서, 그냥 Sequence를 준수하면 해당 타입이 Sequence를 준수하지 않았다는 컴파일 에러가 발생합니다.
public protocol Sequence<Element> { associatedtype Element where Self.Element == Self.Iterator.Element associatedtype Iterator : IteratorProtocol /// ... }
Swift
복사
그럼, 뭘 설정해줘야 할까요?
Sequence 프로토콜에서 제공하는 makeIterator라는 메서드를 사용해서 IteratorProtocol을 설정해주면 됩니다. 해당 메서드가 리턴한 IteratorProtocol타입이 Iterator의 타입으로 설정될 겁니다.
근데, IteratorProtocol이 뭔가요? 어떤 역할을 하길래 Sequence에서 필수적으로 설정해줘야 하나요?
Sequence에서 순차적이고 반복적으로 값을 가지고 올 때, 한 번에 하나씩 Sequence 값을 제공할 수 있도록 해주는게 바로 IteratorProtocol 입니다.
Sequence에서 어떻게 요소를 반환하는지에 대한 내용을 IteratorProtocol이 다루게 됩니다.
위에서 사용했던 Collection 타입과 Sequence 타입을 준수하는 값들은 for-in 루프를 사용할 때에 Type의 iterator를 사용해서 요소에 하나씩 접근하게 됩니다.
어떻게 접근하는지 직접 IteratorProtocol 타입을 만들어서 확인해봅시다.
일단, Sequence를 준수하는 타입을 하나 만들게요. 해당 타입의 이름은 Countdown입니다. 그리고, IteratorProtocol을 준수하는 타입도 만들겠습니다. 이름은 CountdownIterator 입니다.
SequencemakeIterator 메서드를 사용해서 IteratorProtocol 타입을 반환할 겁니다.
struct CountdownIterator: IteratorProtocol { let countdown: Countdown init(_ countdown: Countdown) { self.countdown = countdown } } struct Countdown: Sequence { func makeIterator() -> CountdownIterator { return CountdownIterator(self) } }
Swift
복사
위에서 배운대로 세팅이 완료되었습니다. 하지만, 컴파일 에러가 발생할겁니다.
아직 구현을 안해준 부분이 있기 때문이죠. 바로, next 메서드 입니다.
IteratorProtocol을 준수하면 next 메서드를 필수적으로 작성해주어야 합니다. 해당 메서드는 이름처럼 순차적으로 다음 요소를 반환해줍니다. 메서드 작성자가 그런 방식으로 코드를 작성했다면 말이죠.
CountdownIterator는 Countdown 구조체를 통해서 카운트 다운을 시작할 숫자가 들어오면, 해당 숫자부터 0까지 1씩 감소시키면서 카운트 다운 기능을 해주어야 합니다.
먼저, Countdown에 카운트 다운 시작 숫자를 받을 수 있도록 설정해줄게요.
struct Countdown: Sequence { let start: Int func makeIterator() -> CountdownIterator { return CountdownIterator(self) } }
Swift
복사
그리고, CountdownIterator에서 해당 숫자에서 점차 숫자를 1씩 빼면서 해당 값을 반환해줄게요. 0이 되는 순간 더이상 요소가 반환되지 않도록 nil를 반환할겁니다.
struct CountdownIterator: IteratorProtocol { let countdown: Countdown var times = 0 init(_ countdown: Countdown) { self.countdown = countdown } mutating func next() -> Int? { let nextNumber = countdown.start - times guard nextNumber >= 0 else { return nil } times += 1 return nextNumber } }
Swift
복사
Countdown 구조체를 사용해서 for-in 루프를 돌리면 원하는대로 출력되는걸 볼 수 있습니다.
let count = Countdown(start: 10) for i in count { print(i, terminator: " ") } /* 10 9 8 7 6 5 4 3 2 1 0 */
Swift
복사
위에서는 Sequence 프로토콜을 준수하는 구조체 따로, IteratorProtocol 프로토콜을 준수하는 구조체 따로 작성했지만, Sequence와 IteratorProtocol 프로토콜을 같이 준수하게 되면 코드가 더 간단해집니다.
makeIterator 메서드를 필수적으로 작성하지 않아도 됩니다. IteratorProtocol를 준수하기 때문이죠. 하지만, next 메서드는 동일하게 작성해주셔야 합니다.
struct Countdown: Sequence, IteratorProtocol { let start: Int var times = 0 mutating func next() -> Int? { let nextNumber = start - times guard nextNumber >= 0 else { return nil } times += 1 return nextNumber } }
Swift
복사

While

While은 조건이 false가 될 때까지 While 내부를 반복해서 실행합니다.
for-in 루프와는 다르게 얼마나 반복할지에 대한 정보를 알 수 없는 경우에 While을 사용하게 됩니다.
Swift에서는 2가지 종류의 While 문을 제공합니다.
1.
루프 시작부터 조건 평가 → While
2.
루프 통과 후에 조건 평가 → Repeat-while
While은 하나의 조건을 평가하는 것으로 시작합니다.
만약, 해당 조건을 만족한다면(true), 만족하지 않을 때(false)까지 while 문을 반복해서 실행하게 됩니다.
이렇게만 설명하면 이해가 안될테니 직접 코드를 한 번 짜봅시다.
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
Swift
복사
총 25개의 칸이 있고, 양수가 있는 칸에는 사다리가, 음수가 있는 칸에는 있다고 보시면 됩니다.
보드판이 완성되었으니, 현재 칸을 나타내는 변수와 주사위 숫자를 나타내는 변수를 하나씩 만들어봅시다.
var square = 0 var diceRoll = 0
Swift
복사
뱀, 사다리 게임은 언제 끝날지 모르는 게임입니다. 즉, 얼마나 반복될지 모르는 게임이에요.
그저 현재 칸의 숫자가 마지막 칸의 숫자(finalSqaure)보다 크거나 같다면 게임을 끝낼겁니다.
while square < finalSquare { // ... }
Swift
복사
이런 경우에 while문을 사용하는 것이 적절합니다.
위에서 얘기했듯 게임의 길이가 명확하지 않기 때문에 for-in 루프로는 뱀, 사다리 게임을 진행할 수 없습니다. 특정 조건이 만족될 때까지 루프를 실행하는 while문을 사용하는 것이 제격입니다.

Repeat-while

Repeat-while은 루프 조건을 고려하기 전에 루프 블록을 한 번 통과합니다.
블록을 통과하고나서야 해당 조건을 만족하는지, 아닌지를 확인하고 만족한다면 만족하지 않을때까지 실행을 반복합니다.
이번엔 repeat-while을 사용해서 뱀, 사다리 게임 코드를 수정해볼게요.
while문으로 게임 코드를 짜면 이런식으로 코드가 구성이 됩니다.
while square < finalSquare { // 주사위 굴리기 diceRoll += 1 if diceRoll == 7 { diceRoll = 1 } // 주사위 굴린만큼 칸 이동 square += diceRoll if square < board.count { // 칸에 있다면 사다리, 뱀에 의해서 자리 이동 square += board[square] } }
Swift
복사
하지만, 해당 코드를 repeat-while로 짜면 코드가 약간 달라집니다.
repeat { // 칸에 사다리, 뱀에 의해서 자리 이동 square += board[square] // 주사위 굴리기 diceRoll += 1 if diceRoll == 7 { diceRoll = 1 } // 주사위 굴린만큼 칸 이동 square += diceRoll } while square < finalSquare
Swift
복사
이전과 다르게 현재 칸이 보드칸을 넘어갔는지 확인하지 않습니다. 주사위를 굴리고 칸을 이동하고 나서 경계를 넘었는지 while 조건을 통해 평가하기 때문에 굳이 경계를 검사할 필요가 없어집니다.
따라서, while보다는 repeat-while이 뱀, 사다리 게임에 조금 더 적합하다고 볼 수 있습니다.

️ 참고 자료