티스토리 뷰

Swift 공식 가이드/Swift 2

Optional Chaining

찜토끼 2016. 5. 24. 09:39

Swift 3.0.1 가이드에 대응하는 정리글을 작성하였습니다!!!

Optional Chaining 정리 최신버전 > http://wlaxhrl.tistory.com/52




Apple 제공 Swift 프로그래밍 가이드(2.2)의 Optional Chaining 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.



들어가며

옵셔널 체이닝Optional chaining은 nil이 될 수도 있는 옵션을 가진 프로퍼티, 메서드, 서브스크립트에 대고 질의querying하거나 호출하는 프로세스를 가리킨다. 옵셔널이 값을 가지고 있다면 호출은 성공하고, 옵셔널이 nil이라면 호출은 nil을 리턴한다. 여러개의 쿼리가 한 번에 묶일chained 수 있으며 전체 체인 중 하나라도 nil이 되면 전체 체인이 실패한다.

<note> Swift에서의 옵셔널 체이닝은 Objective-C에서 nil에 메세지를 보내는 것과 유사하다. 다른 점은 Swift에서는 모든 타입에 대해 가능하며, 성공과 실패 여부를 확인할 수 있다는 것이다.



강제 언랩핑의 대안이 되는 옵셔널 체이닝

옵셔널 타입의 프로퍼티, 메서드, 스크립트를 non-nil로 간주할 경우, 호출 시에 뒤에 물음표(?)를 붙여서 옵셔널 체인을 명시할 수 있다. 이것은 느낌표(!)를 붙여서 강제 언랩핑 하는 것과 유사하다. 그러나 옵셔널 체이닝의 경우에는 만약 옵셔널 값이 nil이라도 그냥 nil을 리턴하며 실패만 할 뿐, 런타임 에러를 내지 않는다.

옵셔널 체이닝 호출의 결과는 항상 옵셔널 타입의 값이다. 기대되는 리턴 타입과 동일한 타입인데, 단지 옵셔널로 감싸져있을 뿐이다. nil에 대고 호출했을 때 nil이 리턴되는 과정을 생각해보면 옵셔널 타입이 되는 것이 당연하다. (이 옵셔널 값이 nil이라면 옵셔널 체이닝 호출이 실패한 것이고, 값이 들어있다면 성공한 것이다.)


그럼 예제를 통해 살펴보자.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person() // 이때 john.residence 는 nil

let roomCount = john.residence!.numberOfRooms // 강제언랩핑. runtime error

if let roomCount = john.residence?.numberOfRooms {
    // numberOfRooms는 옵셔널 타입이 아니지만
    // 옵셔널 체이닝을 사용하고 있으므로
    // john.residence?.numberOfRooms 의 리턴값은 옵셔널 Int.
    print("john의 주거지에는 \(roomCount)개 방이 있다")
} else {
    print("Jone의 주거지의 방 개수를 얻어올 수 없다")
}
let john = Person()
john.residence = Residence()

// 이제 john.residence?.numberOfRooms 의 리턴값은
// 1이라는 값을 가지는 옵셔널 Int이다.
if let roomCount = john.residence?.numberOfRooms {
    print("john의 주거지에는 \(roomCount)개 방이 있다")
} else {
    print("Jone의 주거지의 방 개수를 얻어올 수 없다")
}



옵셔널 체이닝을 위한 모델 클래스를 정의해보자.

프로퍼티, 메서드, 서브스크립트를 한 단계 더 깊게 호출할 수 있는 옵셔널 체이닝을 사용할 수 있다. 이것을 이용하면 서로 연관되어 있는 복잡한 모델들에서, 각 서브프로퍼티의 프로퍼티, 메서드, 서브스크립트에 접근할 수 있는지 체크해볼 수 있을 것이다.

예제를 통해 알아보자.

  • Person 클래스 : 위에서 정의한 것과 같다. Residence 인스턴스를 가진다.

  • Room 클래스 : 방 이름을 가진다.

  • Address 클래스 : 옵셔널 스트링 타입의 빌딩이름, 빌딩번호, 도로이름을 가지고 있다.

  • Residence 클래스 : Room 인스턴스들의 배열을 가지고 있다. 인덱스를 통해 해당 Room 인스턴스에 접근할 수 있는 서브크립트를 구현하고 있다. 

class Person {
    var residence: Residence?
}

class Residence {
    var rooms = [Room]()
    var address: Address?
    
    var numberOfRooms: Int {
        return rooms.count
    }
    
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }
    
    func printNumberOfRooms() {
        print("\(numberOfRooms)개")
    }
}

class Room {
    let name: String
    init(name: String) {
        self.name = name
    }
}

class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    
    func buildingIdentifier() -> String? {
        if buildingName != nil {
            return buildingName
        } else if buildingNumber != nil && street != nil {
            return "\(buildingNumber) \(street)"
        } else {
            return nil
        }
    }
}



옵셔널 체이닝을 통해 프로퍼티에 접근해보자.

let john = Person() // 이때 john.residence 는 nil이다
if let roomCount = john.residence?.numberOfRooms {
    print("john의 거주지에는 \(roomCount)개의 방이...")
} else {
    print("방 개수를 알 수 없음")
}

// 옵셔널 체이닝을 통해 프로퍼티의 값을 set하려고 "시도"할 수도 있다
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress // residence가 nil이니까 실패한다


func createAddress() -> Address {
    print("function was called")
    
    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"
    
    return someAddress
}

john.residence?.address = createAddress()
// 아무것도 print되지 않는다.
// john.residence?.address의 리턴값이 nil이기 때문에
// createAddress() 메서드 자체가 호출될 필요가 없었다.



옵셔널 체이닝을 통해 메서드를 호출해보자.

옵셔널 체이닝을 통해 메서드를 호출하면 성공/실패 여부를 알 수 있다. 해당 메서드가 리턴 타입이 없더라도 말이다. (리턴 타입이 없는 메서드는 사실은 Void 리턴 타입을 가진다. 리턴하는 것은 빈 튜플이다)

예제에서는 Residence 클래스에 다음과 같은 리턴 타입 없는 메서드가 있다.

func printNumberOfRooms() {
    print("\(numberOfRooms)개")
}

옵셔널 체이닝을 통해 이 메서드를 호출할 경우 리턴 타입은 Void가 아니라 Void? 가 될 것이다.

if john.residence?.printNumberOfRooms() != nil { // nil이 리턴된다
    print("방 개수를 알 수 있음")
} else {
    print("방 개수를 알 수 없음")
}
// 옵셔널 체이닝을 통해 프로퍼티를 set하려고 "시도"할 때도
// 성공하든 실패하든 Void? 가 리턴된다.
// 따라서 다음과 같이 프로퍼티 셋팅이 성공했는지를 체크할 수 있다
if (john.residence?.address = someAddress) != nil {
    print("프로퍼티 셋팅 성공")
} else {
    // john.residence가 nil이라면 실패해서 여기로 들어올 것이다
    print("프로퍼티 셋팅 실패")
}



옵셔널 체이닝을 통해 서브스크립트에 접근해보자.

서브스크립트의 경우에도 크게 다르지 않다. 옵셔널체이닝을 통해 get/set을 시도할 수 있고 성공/실패 여부를 체크해볼 수 있다.

// get 시도
// 물음표(?)가 위치할 곳은 항상 옵셔널 표현식의 뒤다.
if let firstRoomName = john.residence?[0].name {
    print("첫 번째 방은 \(firstRoomName)")
} else {
    print("첫 번째 방 가져올 수 없음")
}


// set 시도
john.residence?[0] = Room(name: "욕실")


// 이 시점에서는 john.residence 가 nil이므로 시도는 둘 다 실패한다
let johnHouse = Residence()
johnHouse.rooms.append(Room(name: "거실"))
john.residence = johnHouse

// get 시도가 이때는 성공한다
if let firstRoomName = john.residence?[0].name {
    print("첫 번째 방은 \(firstRoomName)")
} else {
    print("첫 번째 방 가져올 수 없음")
}


<옵셔널 타입의 서브스크립트에 접근해보자>

만약 서브스크립트가 옵셔널 타입을 리턴한다면(ex: 딕셔너리의 key를 받아서 value를 돌려주는 서브스크립트), 물음표(?)를 서브스크립트의 괄호 뒤에 위치시켜서 옵셔널 체이닝을 해보자.

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72 // 이 경우는 실패하지만 crash는 나지 않는다. nil만 리턴한다.



체이닝을 여러 레벨로 연결하기Linking Multiple Levels of Chaining

옵셔널 체이닝을 여러 개 연결할 수 있다. 그러나 여러 개 연결한다고 해서 옵셔널 타입이 겹겹이 감싸지는 건 아니다. 옵셔널이 아닌 값은 옵셔널로 반환이 되고, 이미 옵셔널인 값은 그냥 그대로 옵셔널로 반환이 된다. (옵셔널을 한 번 더 옵셔널로 감싸서 반환되는 게 아니라는 말)

if let johnsStreet = john.residence?.address?.street {
    // 두 번 옵셔널 체이닝이 걸렸더라도
    // john.residence?.address?.street 의 반환값은 옵셔널 String
    print("john이 사는 거리이름은 \(johnsStreet)")
} else {
    print("거리이름 가져올 수 없음")
}



옵셔널 리턴 값을 이용해서 메서드 체이닝를 해보자Chaining on Methods with Optional Return Values

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    // john.residence?.address?.buildingIdentifier() 의 리턴 값은
    // 옵셔널 String이다.
    // 이 리턴값에 대해 또 한번 옵셔널 체이닝을 걸고 싶다면 아래처럼 하면 된다
    print("John의 빌딩 identifier는 \(buildingIdentifier)")
}

if let buildingIdentifier = john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
    print("John의 빌딩 identifier는 \(buildingIdentifier)")
}

// 만약 buildingIdentifier()가 반환하는 값이 nil이 될 경우에
// (빌딩 이름도 번호도 없으면 nil 반환한다)
// 다음과 같이 강제 언랩핑을 하면 런타임 에러가 날 것이기 때문에
// 위와 같이 하는 것임
if let buildingIdentifier = john.residence?.address?.buildingIdentifier()!.hasPrefix("The") {
    print("John의 빌딩 identifier는 \(buildingIdentifier)")
}


'Swift 공식 가이드 > Swift 2' 카테고리의 다른 글

Type Casting  (0) 2016.06.07
Error Handling  (0) 2016.06.07
Automatic Reference Counting (ARC)  (0) 2016.05.23
Deinitialization  (0) 2016.05.08
Initialization (3/3)  (0) 2016.05.07
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함