[CH6] 흐름 제어

Tabber·2021년 9월 27일
0

대부분의 프로그래밍 언어에서 조건문과 반복문을 다루지만 스위프트는 다른 언어와 차이가 있으니 유의해야 한다. 스위프트의 흐름 제어 구분에서는 소괄호(())를 대부분 생략할 수 있다. 물론 사용해도 무관하지만, 중괄호({})는 생략할 수 없다.

6.1 조건문

조건문에서 if 문과 switch 구문을 소개한다.
그러나 스위프트의 조건문에는 guard 구문도 있다. guard 구문은 나중에 소개한다.

6.1.1 if 구문

if 구문은 대표적인 조건문으로 if, else 등의 키워드를 사용하여 구현할 수 있다.

정수, 실수 등 0이 아닌 모든 참으로 취급하여 조건 값이 될 수 있었던 다른 언어와는 달리 스위프트트의 if 구문은 조건의 값이 꼭 Bool 타입이어야 한다.

let first: Int = 5
let second: Int = 7

if first > second {
    print("first > second")
} else if first < second {
    print("first < second")
} else {
    print("first == second")
}

// "first < second"

else if 는 몇 개가 이어져도 상관 없으며, else 블록은 없어도 괜찮다.

당연히 위 **else if 조건을 충족해 블록 내부의 명령문이 실행되면 그 다음에 이어진 else if조건을 충족하더라도 실행되지 않고** 조건문을 빠져나온다.

if 키워드 뒤에 따라오는 조건수식을 소괄호로 감싸주는 것은 선택 사항이다.

**// 소괄호가 없는 코드**
let first: Int = 5
let second: Int = 5
var biggerValue: Int = 0

if first > second {
    biggerValue = first
} else if first == second {
   biggerValue = first
} else if first < second {
    biggerValue = second
} else if first == 5 {
    // 조건을 충족하더라도 이미 first == second 조건을 만족했기 때문에
    // 실행되지 않는다.
    biggerValue = 100
}

print(biggerValue)
**// 소괄호가 있는 코드**
if (first > second) {
    biggerValue = first
} else if (first == second) {
   biggerValue = first
} else if (first < second) {
    biggerValue = second
} else if (first == 5) {
    // 조건을 충족하더라도 이미 first == second 조건을 만족했기 때문에
    // 실행되지 않는다.
    biggerValue = 100
}

print(biggerValue)

6.1.2 switch 구문

switch 구문은 다른 언어와 비교했을 때 많이 달라진 문법 중 하나이다.

switch 구문도 소괄호(())가 생략 가능하다. 단, break 키워드 사용은 선택 사항이다.

즉, case 내부의 코드를 모두 실행하면 break 키워드 없이도 switch 구문이 종료된다라는 뜻이다.

이 방식은 예상치 못한 실수를 줄이는 데도 큰 도움이 된다. 따라서 break 를 쓰지 않고 case 를 연속 실행하던 트릭을 더이상 사용하지 못한다.

스위프트에서 switch 구문의 case 를 연속 실행하려면 fallthrough 키워드를 사용한다.


Swift 문서에서의 fallthrough

제어 전송 구문(Control Transfer Statements)

제어 전송 구문은 코드의 진행을 계속 할지 말지를 결정하거나, 실행되는 코드의 흐름을 바꾸기 위해 사용한다.

Swift에서는 5가지의 제어 전송 구문을 제공한다.

그 중 하나가 fallthrough 이다.
(continue, break, fallthrough, return, throw)


let integerToDescribe = 5
var description = "숫자 \(integerToDescribe)는"

switch integerToDescribe {
case 2,3,5,7,11,13,17,19:
    description += " 소수고, 또한 "
    fallthrough
default:
    description += "정수이다."
}
print(description)
// 숫자 5는 소수고, 또한정수이다.

간단한 switch 구문 활용을 살펴보자.

let integerValue: Int = 5

switch integerValue {
case 0:                          // 1
    print("value == zero")
case 1...10:                     // 2
    print("value == 1~10")
    fallthrough
case Int.min..<0, 101..<Int.max: // 3
    print("value < 0 or value > 100")
    break
default:
    print("10 < value <= 100")
}

// Result
// value == 1~10
// value < 0 or value > 100

스위프트에서는 1 번처럼 단 하나의 값으로 case 를 만들 수도 있지만, 2 처럼 범위를 사용할 수도 있다.

3 이 실행된 이유는 앞의 case 블록에서 fallthrough 키워드를 사용하여 다음 case 도 실행되게끔 했기 때문이다.

let doubleValue: Double = 3.0

switch doubleValue {
case 0:
    print("Value == zero")
case 1.5...10.5:
    print("1.5 <= Value <= 10.5")
default:
    print("Value == \(doubleValue)")
}

// 1.5 <= Value <= 10.5

범위 연산자는 정수뿐만 아니라 부동소수 타입도 사용할 수 있다.

let stringValue: String = "IBY"

switch stringValue {
case "IBY":
    print("He is IBY")
case "Jay":
    print("He is Jay")
case "Jenny", "Nova":
    print("He or She is \(stringValue)")
default:
    print("\(stringValue) said 'I don't know who you are'")
}

switch 구문의 입력 값으로 숫자표현이 아닌 문자, 문자열, 열거형, 튜플 등의 다양한 타입의 값도 사용이 가능하다.

case "Jenny", "Nova": 처럼 여러개의 항목을 한번에 case 로 지정해주는 것도 가능하다. 그렇지만 여러 항목을 나타내기 위해 **case 를 연달아 쓰는 것은 불가능**하다.

**case XXX: 다음에는 꼭 실행가능한 코드가 위치해야 한다.**

let stringValue: String = "IBY"

switch stringValue {
case "IBY":
    print("He is IBY")
case "Jay":
    print("He is Jay")
case "Jenny", "Nova":
    print("He or She is \(stringValue)")
case "Joker":
    // 비어있으므로 오류 발생.
default:
    print("\(stringValue) said 'I don't know who you are'")
}

위 코드처럼 stringValue 가 "Joker" case 에 해당될 때 실행될 코드가 있어야 하는데, 비어있기 때문에 컴파일 오류가 발생한다.

만약 C언어의 switch 구문처럼 break 를 사용하지 않은 경우 그 다음 case 를 실행하도록 했던 트릭을 스위프트에서 구현하고 싶다면 fallthrough 를 사용해야 한다.

let stringValue: String = "Joker"

switch stringValue {
case "IBY":
    print("He is IBY")
case "Jay":
    print("He is Jay")
case "Jenny", "Nova":
    print("He or She is \(stringValue)")
case "Joker":
    fallthrough
default:
    print("\(stringValue) said 'I don't know who you are'")
}

// He of She is Joker

switch 구문의 입력 값으로 튜플도 사용 가능하다.

typealias NameAge = (name: String, age: Int)

let tupleValue: NameAge = ("IBY", 23)

switch tupleValue {
case ("IBY", 23):
    print("정답.")
default:
    print("오답.")
}

// 정답.

위 코드는 굳이 switch 구문이 필요한 것은 아니지만, case 의 값으로 튜플이 사용이 가능하다라는 것을 보여주는 예시이다.

튜플은 이외에도 와일드 카드 식별자와 함께 사용하면 더 유용하다. 와일드 카드 식별자는 사용한 곳은 무시된다라는 뜻이다.

typealias NameAge = (name: String, age: Int)

let tupleValue: NameAge = ("IBY", 23)

switch tupleValue {
case ("IBY", 50):
    print("정답.")
case ("IBY", _):
    print("이름만 정답, 나이는 \(tupleValue.age).")
case (_, 23):
    print("나이만 정답, 이름은 \(tupleValue.name).")
default:
    print("찾는 사람이 없음.")
}

// 이름만 정답, 나이는 23.

where 키워드를 사용하여 case 의 조건을 확장할 수 있다.


where?

특정 패턴에 Bool 타입조건을 지정하거나 어떤 타입의 특정 프로토콜 준수 조건을 추가 하는 등의 기능이다.

where 절은 크게 두 가지 용도로 활용된다.

  • 패턴과 결합하여 조건을 추가
  • 타입에 대한 제약을 추가

let 직급: String = "사원"
let 연차: Int = 1
let 인턴인가: Bool = false

switch 직급 {
case "사원" where 인턴인가 == true:
    print("인턴입니당.")
case "사원" where 연차 < 2 && 인턴인가 == false:
    print("신입사원입니다.")
case "사원" where 연차 > 5:
    print("연식 좀 된 사원입니다.")
case "사원":
    print("사원입니다.")
case "대리":
    print("대리입니다.")
default:
    print("사장입니까??")
}

// 신입사원입니다.

열거형과 같이 한정된 범위의 값을 입력 값으로 받게 될 때 값에 대응하는 각 case 를 구현한다면 default 를 구현하지 않아도 된다.

만약 값에 대응하는 각 case 를 구현하지 않는다면 default 는 필수이다.

enum School {
	case primary, middle, high, collage, university, graduate
}

let 최종학력: School = School.university

switch 최종학력 {
case .primary:
	print("최종학력은 유치원입니다")
case .middle:
	print("최종학력은 중학교입니다.")
case .high:
	print("최종학력은 고등학교입니다.")
case .university:
	print("최종학력은 대학교입니다.")
}

// 최종학력은 대학교입니다.

만약 열거형에 case 가 추가될 가능성이 있다면 switch 구문에서는 어떻게 대비해야할까?

앞서 살펴본대로 switch 구문에서 현재의 열거형에 정의한 모든 case 를 처리해주면 지금은 정상적으로 컴파일 되겠지만, 나중에 열거형에 case  를 추가하면 기존의 switch  구문은 컴파일 오류가 발생할 것이다. switch 구문이 모든 case 에 대비하지 못하기 때문이다.

스위프트 5.0 버전에서 추가된 unknown 이라는 속성은 이런 문제를 조금 유려하게 대처할 수 있도록 도와준다.

enum Menu {
    case chicken
    case pizza
}

let lunchMenu: Menu = .chicken

switch lunchMenu {
case .chicken:
    print("반반 무많이")
case .pizza:
    print("핫소스 많이 주세요")
case _:
    print("오늘 메뉴가 뭐죠?")
}

 위 코드의 Menu라는 열거형은 나중에 case 를 추가할 것 같다는 예상을 했다.

그래서 해당 열거형의 값을 처리하는 switch 구문의 마지막 case 로 와일드 카드 case 를 미리 추가해두었다. 이렇게 하면 컴파일 오류가 발생하지 않을 것이다.

그러나, 만약 미래의 나 혹은 다른 동료가 새로운 case 를 추가했다고 생각해보자.

그리고 깜빡하고 코드에 구현해 둔 switch 구문의 내부 코드는 수정하지 않았다면?

오히려 case _ 의 상황이 발생할 가능성이 있기 때문에, 컴파일러 경고조차 사라진다.

문법적으로는 오류가 없지만, 논리적으로는 오류가 발생하는 것이다.

이런 문제를 방지하기 위해서 unknown 속성을 사용할 수 있다.

enum Menu {
    case chicken
    case pizza
}

let lunchMenu: Menu = .chicken

switch lunchMenu {
case .chicken:
    print("반반 무많이")
case .pizza:
    print("핫소스 많이 주세요")
@unknown case _:
    print("오늘 메뉴가 뭐죠?")
}

이렇게 하면 컴파일러에서 경고를 통해 해당 switch 구문이 모든 case 에 대응하지 않는다는 사실을 상기할 수 있다.

이렇게 논리적인 오류에 대해 도움을 받을 수 있는 unknown 속성을 부여할 수 있는 casecase _ 혹은 default case 뿐이다.

또, unknown 속성을 부여한 caseswitch 구문의 가장 마지막 case 로 작성해야 한다.

6.2 반복문

조건에 따라 실행되어야 하는 명령어를 조건문을 통해 분기한다면, 같거나 비슷한 명령을 반복실행할 때는 반복문 같은 중요한게 없다.

특히나 배열과 같은 시퀀스, 순서가 있는 데이터는 반복문으로 더욱 편리하게 처리할 수 있다.

스위프트의 반복문은 기존 프로그래밍 언어의 반복문과 크게 다를게 없다.
다만, 전통적인 C 스타일의 for 구문이 사라졌다(전통적인 구분은 스위프트 3.0에서 사라졌다고 한다.)는 것과 조건에 괄호를 생략할 수 있다는 정도이다.

또한 do-while 구문은 스위프트에서 repeat-while 구문으로 구현되어있다.
(이 또한 스위프트 2 부터 변경되었다. 스위프트 1.0 은 do-while를 사용한다)

6.2.1 for-in 구문

for-in 반복 구문은 반복적인 데이터나 시퀀스를 다룰 때 많이 사용한다.

for 임시상수 in 시퀀스아이템 {
	실행코드
}

몇 가지 간단한 반복 구문 예시를 보자.

import Foundation

for i in 0...2 {
    print(i)
}

for i in 0...5 {
    if i.isMultiple(of: 2) {
        print(i)
        continue
    }
    
    print("\(i) == 홀수")
}

let helloSwift: String = "Hello Swift!"

isMultiple 은 지정된 값의 배수이면 true를, 아니면 false를 반환하는 메서드이다.

컬렉션 타입에서도 유용하게 사용할 수 있다.

import Foundation

let friends: [String: Int] = ["Jay": 35, "Kane": 28]

for tuple in friends {
    print(tuple)
}

// (key: "Jay", value: 35)
// (key: "Kane", value: 28)

let address: [String: String] = ["도": "충청남도", "시군구": "당진시", "동읍면": "수청동"]

for (key, value) in address {
    print(key, value)
}
 
// 시군구 당진시
// 동읍면 수청동
// 도 충청남도

6.2.2 while 구문

while 반복 구문도 다른 프로그래밍 언어의 while 구문과 크게 다르지 않다.

특정 조건이 성립하는 한 블록 내부의 코드를 반복해서 실행한다.

var names: [String] = ["Kane", "Joe", "IBY"]

while names.isEmpty == false {
    print("Good Bye \(names.removeFirst())")
}

// Good Bye Kane
// Good Bye Joe
// Good Bye IBY

6.2.3 repeat-while 구문

repeat-while 반복 구문은 다른 프로그래밍 언어의 do-while 구문과 크게 다르지 않다.

repeat 블록의 코드를 최초 1회 실행한 후, while 다음의 조건이 성립하면 블록 내부의 코드를 반복 실행한다.

var names: [String] = ["Kane", "Joe", "IBY"]

repeat {
    print("Good Bye \(names.removeFirst())")

} while names.isEmpty == false

// Good Bye Kane
// Good Bye Joe
// Good Bye IBY

6.3 구문 이름표

반복문을 작성하다 보면 종종 반복문을 중첩으로 작성하게 된다.

이때 반복문을 제어하는 키워드 (break, continue 등)가 어떤 범위에 적용되어야 하는지 애매하거나 큰 범위의 반복문을 종료하고 싶은데 작은 범위의 반복문만 종료되는 등 예상치 못한 실수를 할 수 있다.

그럴 때는 반복문 앞에 이름과 함께 콜론을 붙여 구문의 이름을 지정해주는 구문 이름표를 사용하면 좋다.

이름이 지정된 구문을 제어하고자 할 때는 제어 키워드와 구문 이름을 함께 써주면 된다.

import Foundation

var numbers: [Int] = [3,2342,6,2352]

numbersLoop: for num in numbers {
    if num > 5 || num < 1 {
        continue numbersLoop
    }
    
    var count: Int = 0
    
    printLoop: while true {
        print(num)
        count += 1
    
        if count == num {
            break printLoop
        }
    }
    
    removeLoop: while true {
        if numbers.first != num {
            break numbersLoop
        }
        numbers.removeFirst()
    }
}

// 3
// 3
// 3
// numbers에는 [ 2342, 6 , 2352 ] 가 남는다.
profile
iOS 정복중인 Tabber 입니다.

0개의 댓글