Swift 17. 옵셔널 체이닝 & 빠른 종료(guard) (assert)

 Swift 17. 옵셔널 체이닝 & 빠른 종료(guard)




옵셔널 체이닝과 빠른 종료는 옵셔널을 좀더 편리하게 사용하도록 해줌




1. 옵셔널 체이닝


옵셔널인 (nil일지도 모르는 ) 프로퍼티, 메소드, 서브스크립션 등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정

옵셔널에 값이 있다면 호출할 수 있고 nil이면 nil을 반환

옵셔널을 반복 사용해서 체인처럼 이어져 있다고 해서 체이닝

중첩된 옵셔널 중 하나라도 값이 존재하지 않는다면 결과적으로 nil 반환

프로퍼티, 메소드, 서브스크립트를 호출할 옵셔널 변수나 상수 뒤에 물음표(?)를 붙여서 표현

옵셔널 체이닝의 반환된 값은 항상 옵셔널 ( Optional(값) )


* !

물음표(?) 대신 느낌표(!)를 사용하면 옵셔널에서 강제 추출함

옵셔널에 값이 없다면 런타임 에러 발생

반환값이 옵셔널이 아닌 그냥 값임

100% nil이 아니라는 확신이 있더라도 사용을 지양하는 편이 좋음



* 예시를 위한 클래스와 스트럭트

// 호실

class Room {

    var number: Int  // 호실 번호

    

    init(number: Int) {

        self.number = number

    }

}


// 건물

class Building {

    var name: String // 건물 이름

    var room: Room?   // 호실 정보 (옵셔널)

    

    init(name: String) {

        self.name = name

    }

}



// 주소

struct Address {

    var province: String    // 광역시/도

    var city: String        // 시/ 군/ 구

    var street: String      // 도로명

    var building: Building? // 건물 (옵셔널)

    var detialAddress: String? // 건물 외 상세 정보 (옵셔널)

}


// 사람

class Person {

    var name: String

    var address: Address? // 주소 (옵셔널)

    

    init(name: String) {

        self.name = name

    }

}


* 예시 - 사람 한 명 생성

let kim: Person = Person(name: "Kim")



* 예시 - 옵셔널 체이닝과 강제추출로 Kim이 사는 호실 번호 출력

// 옵셔널 체이닝

print(kim.address?.building?.room?.number)

// 강제 추출 (에러 발생)

//print(kim.address!.building!.room!.number)

<실행결과>

nil

-> kim의 해당 프로퍼티에 아직 값이 없기 때문에 옵셔널 체이닝 도중에 nil 반환. 강제 추출의 경우에는 런타임 에러 발생


=> . 으로 찍어가면서 프로퍼티와 물음표(?)를 나열 했을 때 모든 요소를 만족했을 때 해당 값을 반환

가장 마지막 프로퍼티에는 물음표(?)를 붙일 수 없음




- 옵셔널 바인딩과 옵셔널 체이닝의 결합


* 옵셔널 바인딩

값이 있다면 옵셔널에서 추출한 값을 일정 블록 안에서 사용할 수 있는 상수나 변수로 할당해서 옵셔널이 아닌 상태로 사용할 수 있게 해줌

if 문이나 while 문 등과 결합해서 사용할 수 있음

조건식에 임시로 사용할 변수나 상수를 선언하고 옵셔널 값을 대입해서 값이 nil이 아니면 조건이 참이 되어 블록 안의 실행문을 실행하는데 이때 입시로 선언한 변수나 상수에 옵셔널 값이 옵셔널이 벗겨저서 대입되어 옵셔널이 아닌 값으로 블록 내에서만 사용할 수 있음



* 옵셔널 체이닝과의 결합

옵셔널 체이닝의 결과값은 옵셔널 값이기 때문에 옵셔널 바인딩과 결합할 수 있음 -> 보다 간편한 표현 가능

결과값이 nil이 아님을 확인하는 동시에 상수로 받아옴

옵셔널 체이닝을 통해 여러 단계로 복잡하게 중첩된 옵셔널 프로퍼티나 메소드 등에 매번 nil인지 체크를 하지 않아도 손쉽게 접근 가능


* 예시 - kim의 방 번호를 옵셔널 바인딩으로 추출

let kim: Person = Person(name: "Kim")

var roomNumber: Int? = nil

// 옵셔널 바인딩

if let kimAddress: Address = kim.address {

    if let kimBuilding: Building = kimAddress.building {

        if let kimRoom: Room = kimBuilding.room {

            roomNumber = kimRoom.number

        }

    }

}

if let number = roomNumber {

    print(number)

}

else {

    print("Can not find room number")

}


* 예시 -> 위의 코드를 옵셔널 체이닝을 결합해서 표현

let kim: Person = Person(name: "Kim")


if let roomNumber: Int = kim.address?.building?.room?.number {

    print(roomNumber)

}

else {

    print("Can not find room number")

}

<실행결과>

Can not find room number


=> 옵셔널 체이닝을 결합하면 보다 간결하게 표현할 수 있음





- 옵셔널 체이닝을 이용한 값 할당

값을 받아오는 것과 마찬가지로 값을 할당해 줄 수도 있음


* 예시

let kim: Person = Person(name: "Kim")


kim.address = Address(province: "서울특별시", city: "강남구", street: "강남로", building: nil, detailAddress: nil)


// 옵셔널 체이닝을 이용한 값 할당

kim.address?.building = Building(name: "ABC")

kim.address?.building?.room = Room(number: 0)

kim.address?.building?.room?.number = 505


print(kim.address?.building?.room?.number)

<실행결과>

Optional(505)





- 옵셔널 체이닝을 이용한 메소드 호출

프로퍼티 호출과 동일


* 예시 - Address 구조체에 전체 주소를 반환하는 메소드 추가

// 주소

struct Address {

    var province: String    // 광역시/도

    var city: String        // 시/ 군/ 구

    var street: String      // 도로명

    var building: Building? // 건물 (옵셔널)

    var detailAddress: String? // 건물 외 상세 정보 (옵셔널)

    

    // 전체 주소 반환 메소드 

    func fullAddress() -> String? {

        var restAddress: String? = nil

        

        if let buildingInfo: Building = self.building {

            restAddress = buildingInfo.name

        } else if let detail = self.detailAddress {

            restAddress = detail

        }

        

        if let rest: String = restAddress {

            var fullAddress: String = self.province

            fullAddress += " " + self.city

            fullAddress += " " + self.street

            fullAddress += " " + rest

            return fullAddress

        } else {

            return nil

        }

    }

}

* 예시 - 메소드 호출

// 옵셔널 체이닝으로 메소드 호출

if let kimFullAddress = kim.address?.fullAddress() {

    print(kimFullAddress)

}

<실행결과>

서울특별시 강남구 강남로 ABC




- 옵셔널 체이닝으로 서브스크립트 호출

서브스크립트를 가장 많이 사용하는 곳은 배열(Array)과 사전(Dictionary)

이전까지와는 다르게 대괄호보다 앞에 물음표(?)를 붙여줌 

-> 서브스크립트 외에도 옵셔널 체이닝을 인덱스 요소에 사용할 때 동일하게 적용



* 예시 - 옵셔널 체이닝으로 서브스크립트 호출

// 옵셔널 배열

let optionalArray: [Int]? = [1, 2, 3]


print(optionalArray?[1]) // 옵셔널 체이닝을 이용한 서브스크립트 호출




// 옵셔널 딕셔너리

var optionalDictionary: [String: [Int]]? = [String: [Int]]()


// 옵셔널 체이닝을 이용한 할당

optionalDictionary?["numberArray"] = optionalArray


// 옵셔널 체이닝을 이용한 서브스크립트 호출

print(optionalDictionary?["numberArray"]?[2])

<실행결과>

Optional(2)

Optional(3)






2. 빠른 종료 (guard)


if 문과 유사한게 Bool 타입의 값으로 동작하는 기능

if 문과는 다르게 guard문 다음에는 반드시 else 문이 나와야 함

guard 다음의 값이 false이면 else 안의 코드 실행

else 문 블록 안에는 반드시 자신보다 상위 블록을 종료하는 제어문 전환 명령 (break, return, continue, throw 등) 이 들어가야 함

(또는 fatalError() 와 같은 비반환 메소드을 호출할 수도 있음)


guard [Bool 타입의 값] else {

    [실행코드]

    [제어문 전환 명령어]

}



guard 문을 이용하면 if 문 보다 간결하게 표현 가능

예외사항만을 처리하고 싶다면 guard 문을 사용하는 것이 더 간편


* 예시 - for 문을 이용해 짝수만 출력

// if문 이용

for i in 1...6 {

    if i % 2 == 0{

        print(i)

    }

}


// guard문 이용

for i in 1...6 {

    guard i % 2 == 0 else {

        continue

    }

    print(i)

}

<실행결과>

2

4

6

2

4

6



- guard문과 옵셔널 바인딩

guard 다음에 옵셔널 바인딩 표현을 작성하면 옵셔널에 값이 없으면 else 안의 코드를 실행하고 있으면 guard 구문 밖의 코드 실행

값이 있으면 옵셔널 바인딩된 상수를 guard 문 다음 코드부터 지역상수처럼 사용할 수 있음


* 예시

func hello(_ name: String?) -> Void {

    guard let name: String = name else {

        return // 값이 없으면 함수 종료

    }

    

    // 값이 있으면

    print("Hello \(name)") // 바인딩된 상수를 지역상수처럼 사용

}



var person: String? = nil

hello(person) // 옵셔널에 값이 없어서 함수 종료


person = "Lee"

hello (person) // 옵셔널에 값이 있어서 guard 문 다음의 코드가 실행됨

<실행결과>

Hello Lee



guard 문에 조건을 여러개 주고 싶다면 쉼표(,)를 이용해서 여러개의 조건을 나열

쉼표(,)는 AND 논리연산과 같은 결과 -> 쉼표(,)를 '&&'로 치환해도 됨


* 예시 

func enterClub(_ name: String?, _ age: Int?) {

    // 여러 조건을 쉼표(,)를 기준으로 나열

    guard let name: String = name, let age = age, age > 19 else {

        print("You can't enter Club")

        return

    }

    

    print("Welcome \(name)")

}



enterClub("Kim", 19)

enterClub("Park", 23)

<실행결과>

You can't enter Club

Welcome Park





* guard 문의 한계

guard문은 코드 블록 안에서만 사용할 수 있음

(조건문, 반복문, 함수, 메소드 등)

특정 블록 내부에 위치하지 않는다면 guard 문 사용 불가






* assert

애플리케이션이 동작하는 도중에 생성되는 다양한 결과값을 동적으로 확인하고 안전하게 처리할 수 있도록 확인하고 빠르게 처리

디버깅 모드에서만 동작

배포하는 애플리케이션에서는 제외됨

주로 디버깅 중 조건의 검증을 위해서 사용됨


assert( [검사할 조건], [결과가 false일 때 출력할 문자열 (생략 가능)] )


검사할 조건이 true면 assert 함수를 지나치고 다음으로 넘어각고 false면 문자열을 출력하고 프로그램을 종료시킴



* 예시

var a: Int = 0


assert(a == 0, "오류 발생")


print("good")


assert(a == 1, "오류 발생")


print("good")

<실행결과>

good

__lldb_expr_16/MyPlayground.playground:7: Assertion failed: 오류 발생

-> 첫번 째 assert함수는 조건이 참이라 지나치고 두번 째 assert 함수는 조건이 거짓이어서 프로그램을 종료하고 오류 메시지를 출력



함수의 매개변수로 전달된 값이 원하는 조건을 충족하는지 확인하는 용도로도 사용할 수 있음


* 예시

func ageWithAssert(age: Int?) { // 옵셔널 매개변수

    // nil 이 아닌지 검사

    assert(age != nil)

    // 유효한 나이값인지 검사

    assert(age! >= 0 && age! < 130, "나이값이 잘못 입력되었습니다.")

    

    // 모든 조건을 만족하면

    print("당신은 \(age!)세 입니다.")

}


ageWithAssert(age: 25)

//ageWithAssert(age: -1)

//ageWithAssert(age: nil)

<실행결과>

당신은 25세 입니다.

(주석 처리한 부분 실행 시 프로그램 종료됨)

__lldb_expr_25/MyPlayground.playground:5: Assertion failed: 나이값이 잘못 입력되었습니다.

__lldb_expr_27/MyPlayground.playground:3: Assertion failed






댓글