ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Optional Chaining
    iOS/Swift 공식문서 2022. 2. 22. 16:56

    Optional Chaining이란?

    nil일 수 있는 프로퍼티, 메서드, subscript를 query하고 호출하는 과정을 말한다. optional이 값을 포함하고 있다면 호출이 성공하지만, optional의 값이 nil이라면 해당 호출은 nil을 반환한다. 여러 query들이 연결되는 경우 하나라도 nil이면 전체가 nil을 반환한다.

     

    note

    Objective-C와는 달리, optional chaining은 primitive type에서도 사용이 가능하다.

     

    Optional Chaining as an Alternative to Forced Unwrapping

    optional chaining을 명시하기 위해서는 프로퍼티, 메서드, subscript를 호출하고자 하는 optional 값 뒤에 물음표(?)를 붙여주면 된다. 이는 느낌표를 사용하는 forced unwrapping과 비슷하다. 다만 차이가 있는데, optional chaining의 경우 해당 optional 값이 nil이라면 전체 호출이 nil을 반환하는데, forced unwrapping은 optional 값이 nil이라면 runtime error를 출력한다.

     

    nil을 반환할 수도 있기 때문에, optional chaining의 반환값은 optional 값이다. 원래 반환값을 optional로 감싼 것이다. 따라서 호출이 성공했는지 여부를 nil인지 여부로 확인할 수 있다.

     

    optional chaining과 forced unwrapping의 차이를 보기 위해 다음 예시를 살펴보자. 우선 두 클래스를 정의한다.

    class Person {
        var residence: Residence?
    }
    
    class Residence {
        var numberOfRooms = 1
    }

     

    다음과 같이 default initializer로 인스턴스를 생성하면 residence의 값은 nil이 될 것이다.

    let john = Person()

     

    forced unwrapping으로 프로퍼티에 접근하려 하면 에러가 발생한다.

    let roomCount = john.residence!.numberOfRooms
    // this triggers a runtime error

     

    반면 optional chaining을 사용할 경우 값이 nil로 처리되어 실행이 계속된다.

    if let roomCount = john.residence?.numberOfRooms {
        print("John's residence has \(roomCount) room(s).")
    } else {
        print("Unable to retrieve the number of rooms.")
    }
    // Prints "Unable to retrieve the number of rooms."

     

    residence가 값을 갖도록 한뒤 조건문을 실행하면 정상적으로 실행된다.

    john.residence = Residence()
    
    if let roomCount = john.residence?.numberOfRooms {
        print("John's residence has \(roomCount) room(s).")
    } else {
        print("Unable to retrieve the number of rooms.")
    }
    // Prints "John's residence has 1 room(s)."

     

    Defining Model Classes for Optional Chaining

    optional chaining은 여러 레벨에서 사용할 수 있다. 이러한 예시를 보여주기 위해 우선 클래스들을 정의하자. 우선 Person 클래스와 Room 클래스는 다음과 같다.

    class Person {
        var residence: Residence?
    }
    
    class Room {
        let name: String
        init(name: String) { self.name = name }
    }

     

    Address 클래스는 다음과 같다. 주소를 나타내기 위해 건물 이름, 번호, 거리를 나타내야 하는데 3개의 프로퍼티 모두 optional이다. 이 프로퍼티들을 가지고 identifier를 반환하는 buildingIdentifier() 메서드가 있다.

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

     

    Residence 클래스는 이전 예시보다 더 복잡하다. roomsRoom들의 배열이며, subscript를 통해 각 Room 인스턴스에 접근할 수 있다. 또한 printNumberOfRooms() 메서드로 방 개수를 출력할 수 있다. 주소인 address는 optional이다.

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

     

    Accessing Properties Through Optional Chaining

    다음과 같이 optional chaining을 통해 프로퍼티에 접근할 수 있다.

    let john = Person()
    if let roomCount = john.residence?.numberOfRooms {
        print("John's residence has \(roomCount) room(s).")
    } else {
        print("Unable to retrieve the number of rooms.")
    }
    // Prints "Unable to retrieve the number of rooms."

     

    optional chaining을 값을 할당하는데 사용할 수도 있다. 값을 할당받는 경우 접근하고자 하는 프로퍼티를 가진 인스턴스가 nil이면 할당이 실행되지 않는다. 다음 코드의 마지막 줄의 경우 residencenil이기 때문에 할당이 되지 않는다.

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"
    john.residence?.address = someAddress

     

    할당이 실행될 수 없는 경우 우변이 아예 실행되지 않는다. 이를 확인하기 위해 다음 예시를 살펴보자. 마지막 줄에서 createAddress()가 호출되지 않아서 함수 첫번째 줄의 print가 실행되지 않는 것을 확인할 수 있다.

    func createAddress() -> Address {
        print("Function was called.")
    
        let someAddress = Address()
        someAddress.buildingNumber = "29"
        someAddress.street = "Acacia Road"
    
        return someAddress
    }
    
    john.residence?.address = createAddress()

     

    Calling Methods Through Optional Chaining

    optional chaining을 통하여 메서드를 호출해보자. Residence 클래스의 printNumberOfRooms() 메서드는 반환값이 없다. 따라서 반환 타입이 Void이다. optional chaining으로 메서드를 호출할 경우 optional로 감싸기 때문에 반환 타입은 Void?이다. 따라서 이와 같이 nil인지를 확인하여 함수가 호출되는지 확인할 수 있다.

    if john.residence?.printNumberOfRooms() != nil {
        print("It was possible to print the number of rooms.")
    } else {
        print("It was not possible to print the number of rooms.")
    }
    // Prints "It was not possible to print the number of rooms."

     

    optional chaining을 통해 프로퍼티에 값을 할당하는 연산도 마찬가지로 반환 타입이 Void?이다. 따라서 nil인지를 확인하여 할당 연산이 이루어졌는지 확인할 수 있다.

    if (john.residence?.address = someAddress) != nil {
        print("It was possible to set the address.")
    } else {
        print("It was not possible to set the address.")
    }
    // Prints "It was not possible to set the address."

     

    Accessing Subscripts Through Optional Chaining

    optional 값의 subscript를 통해 값을 받거나 변경하기 위해 optional chaining을 사용할 수 있다. 다음 코드의 경우 john.residence의 값이 nil이기 때문에 else문이 실행되는 것을 확인할 수 있다.

    if let firstRoomName = john.residence?[0].name {
        print("The first room name is \(firstRoomName).")
    } else {
        print("Unable to retrieve the first room name.")
    }
    // Prints "Unable to retrieve the first room name."

     

    값을 할당하는 것도 다음과 같이 가능하다. 다음 코드의 경우, john,residencenil이기 때문에 할당이 실패한다.

    john.residence?[0] = Room(name: "Bathroom")

     

    다음과 같이 john.residenceResidence 인스턴스를 할당한다면 optional chaining을 통해 Room 인스턴스에 접근할 수 있다.

    let johnsHouse = Residence()
    johnsHouse.rooms.append(Room(name: "Living Room"))
    johnsHouse.rooms.append(Room(name: "Kitchen"))
    john.residence = johnsHouse
    
    if let firstRoomName = john.residence?[0].name {
        print("The first room name is \(firstRoomName).")
    } else {
        print("Unable to retrieve the first room name.")
    }
    // Prints "The first room name is Living Room."

     

    Accessing Subscripts of Optional Type

    Dictionary 타입의 key subscript와 같이 원래 subscript가 optional을 반환하는 경우, subscript의 닫는 괄호 뒤에 물음표를 붙여서 다음 subscript에 접근하면 된다.

    var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
    testScores["Dave"]?[0] = 91
    testScores["Bev"]?[0] += 1
    testScores["Brian"]?[0] = 72
    // the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]

     

    Linking Multiple Levels of Chaining

    여러 레벨의 optional chaining을 연결할 수 있다. 다만, optional chaining을 여러 레벨에 적용하더라도 optional인 값에 optional이 추가적으로 감싸지지는 않는다.

     

    a. 얻고자 하는 값의 타입이 optional이 아니라면, optional이 될 것이다.
    b. 얻고자 하는 값의 타입이 이미 optional이라면, 추가적으로 optional로 감싸지지 않을 것이다.

     

    다음 예시를 살펴보자. john.residence는 vaild한 인스턴스이다. 그러나 john.residence?.addressnil이기 때문에 optional chaining 전체가 nil을 반환하고, else가 실행된다.

    if let johnsStreet = john.residence?.address?.street {
        print("John's street name is \(johnsStreet).")
    } else {
        print("Unable to retrieve the address.")
    }
    // Prints "Unable to retrieve the address."

     

    address에 유효한 인스턴스 값을 할당한 다음에는 if문이 실행된다.

    let johnsAddress = Address()
    johnsAddress.buildingName = "The Larches"
    johnsAddress.street = "Laurel Street"
    john.residence?.address = johnsAddress
    
    if let johnsStreet = john.residence?.address?.street {
        print("John's street name is \(johnsStreet).")
    } else {
        print("Unable to retrieve the address.")
    }
    // Prints "John's street name is Laurel Street."

     

    Chaining on Methods with Optional Return Values

    optional 타입을 반환하는 메서드에 대해서도 optional chaining을 사용할 수 있다. 다음 예시의 경우 조건문에서 john.residence, john.residence?.address, john.residence?.address?.buildingIdentifier()nil인지를 확인하여 조건문이 실행된다. 세 개의 값이 모두 nil이 아니라면 if문이 실행될 것이다.

    if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
        print("John's building identifier is \(buildingIdentifier).")
    }
    // Prints "John's building identifier is The Larches."

     

    메서드 반환값에 대해서도 optional chaining이 필요하다면 메서드 호출하는 괄호 뒤에 물음표를 붙이면 된다.

    if let beginsWithThe =
        john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
        if beginsWithThe {
            print("John's building identifier begins with \"The\".")
        } else {
            print("John's building identifier doesn't begin with \"The\".")
        }
    }
    // Prints "John's building identifier begins with "The"."

     

    Reference

    https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html

    'iOS > Swift 공식문서' 카테고리의 다른 글

    Concurrency  (0) 2022.02.23
    Error Handling  (0) 2022.02.22
    Deinitialization  (0) 2022.02.21
    Initialization  (0) 2022.02.21
    Inheritance  (0) 2022.02.20

    댓글

Designed by Tistory.