본문 바로가기
프로그래밍/Swift

[Swift] Optional Chaining

by 별준 2022. 1. 8.

References

Contents

  • Optional Chaining as an Atlternative to Forced Unwrapping
  • Defining Model Classes for Optional Chaining
  • Accessing Properties Through Optional Chaining
  • Linking Multiple Levels of Chaining
  • Chaining on Methods with Optional Return Values

Optional Chaning은 현재 'nil'일 수도 있는 optional properties/methods/subscripts를 쿼리하고 호출하는 프로세스입니다. 만약 optional이 값을 포함하고 있다면, 속성이나 메소드, 서브스크립트은 성공적으로 호출됩니다. 만약 'nil'이라면, 이 호출은 nil을 리턴합니다. 다중 쿼리도 함께 chaining될 수 있으며, 만약 하나라도 nil이라면 전체 chain이 실패합니다.

 


Optional Chaining as an Alternative to Forced Unwrapping

Optional Chaining은 호출하고자 하는 속성, 메소드, 또는 서브스크립트가 nil이 아닌 경우, optional 값 뒤에 물음표(?)를 붙여서 Optional Chanining을 지정할 수 있습니다. 이는 optional 값을 강제로 unwrapping하는 느낌표(!)를 붙이는 것과 유사합니다. 가장 큰 차이점은 optional이 nil일 때 Optional Chaining은 nil을 리턴하며 실패하지만, 강제로 unwrapping하는 것은 런타임 에러를 트리거합니다.

 

Optional Chaining이 호출되어 nil을 반환할 수 있기 때문에 optional chaining의 결과는 항상 optional 값이어야 합니다. 이러한 optional 값을 사용하여 Optional Chaining이 성공했는지 아니면 nil을 반환하여 실패했는지 확인할 수 있습니다.

Optional chaining의 결과는 보통 예상되는 반환값과 동일한 타입이지만, optional로 래핑됩니다. 따라서, 일반적으로 Int를 반환하는 속성은 optional chaining을 통해 액세스할 때 Int?를 반환합니다.

 

아래 코드들은 optional chaining이 강제 unwarpping과 어떻게 다른지 보여줍니다.

먼저 Person과 Residence 클래스를 정의합니다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence 인스턴스는 하나의 Int 속성 numberOfRooms를 갖고 기본값은 1입니다. Person 인스턴스는 optional residence 속성(Residence?)을 갖습니다.

새로운 Person 인스턴스를 생성한다면, 이 인스턴스의 residence 속성은 처음에 nil로 초기화됩니다.

let john = Person()

만약 이 사람의 residence의 numberOfRooms 속성을 느낌표(!)를 붙여 강제 unwrapping으로 접근한다면, unwrap할 residense의 값이 없기 때문에 런타임 에러가 발생합니다.

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

위의 코드에서 john.residence가 nil이 아니라면 성공했을 것이고, roomCount의 값은 Int 값으로 설정될 것입니다. 그러나 위 코드는 residence가 nil이라면 항상 런타임 에러를 발생시킵니다.

 

numberOfRooms 값에 접근하기 위한 다른 방법으로 Optional Chaining을 사용할 수 있습니다. 이를 사용하면, 느낌표 대신 물음표를 붙여주면 됩니다.

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."

이는 Swift에게 optional residence 속성을 "chain"하고, 만약 residence의 값이 존재할 경우에 numberOfRooms의 값을 반환하도록 합니다.

numberOfRooms에 액세스하려는 시도가 실패할 수 있기 때문에 Optional Chaining은 Int? (optional Int) 타입의 값을 반환합니다. 위의 예시처럼 residence가 nil일 때, 이 optional Int 또한 nil이 됩니다. 

 

numberOfRooms가 non-optional Int여도 동일합니다. optional chain을 통해 쿼리한다는 것은 numberOfRooms의 호출이 항상 Int가 아닌 Int?를 리턴한다는 것을 의미합니다.

 

Residence 인스턴스를 john.residence에 할당하면, 더 이상 nil이 아니게 됩니다.

john.residence = Residence()

john.residence는 이제 실제 Residence 인스턴스를 포함합니다. 만약 전과 같이 동일한 optional chaining으로 numberOfRooms에 액세스하려고 시도한다면, 이제 numberOfRooms의 값을 가지고 있는 Int?를 리턴합니다.

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은 2단계 이상의 속성이나 메소드, 서브스크립트를 호출할 때에도 사용할 수 있습니다. 이는 상호연관된 복잡한 모델에서 하위 속성으로 들어가서 해당 속성의 속성/메소드/서브스크립트에 액세스할 수 있는지 여부를 확인할 수 있습니다.

 

아래 코드는 다중 optional chaining을 포함하여 아래 예제들에서 사용되는 4가지 모델의 클래스를 정의합니다. 아래 모클래스는 위에서 살펴본 Person과 Residence를 확장합니다.

class Person {
    var residence: Residence?
}

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?
}

Person 클래스는 동일하고, Residence 클래스가 조금 더 복잡해졌습니다. Residence 클래스는 rooms 라는 변수 속성을 정의하고 이는 처음에 [Room] 타입의 빈 배열로 초기화됩니다.

 

Residence는 Room 인스턴스들의 배열을 저장하기 때문에, numberOfRooms 속성은 더 이상 저장 속성이 아닌 연산 속성(computed property)가 됩니다. 연산 속성인 numberOfRooms는 간단히 rooms 배열으로부터 count 속성의 값을 반환합니다.

그리고, rooms 배열에 쉽게 접근하기 위해서 read-write 서브스크립트를 구현하여 요청된 인덱스의 room에 액세스할 수 있도록 합니다.

또한, printNumberOfRooms라는 메소드를 제공하는데, 이는 간단하게 현재 residence의 방 갯수를 출력합니다.

마지막으로 Residence는 optional 속성인 address를 정의합니다. Address 클래스는 아래에서 살펴보겠습니다.

 

rooms 배열에 사용되는 Room 클래스는 다음과 같이 name 속성을 가진 간단한 클래스로 정의됩니다.

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

마지막으로 Address 클래스는 3개의 optional String(String?) 속성을 가집니다.

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
        }
    }
}

Address 클래스는 또한 buildingIdentifier() 메소드를 제공하는데, 이는 String? 타입을 반환합니다. 이 메소드는 buildNumber와 street의 값이 모두 있다면, buildingNumber에 street를 붙여서 리턴하고, buildNumber의 값만 있다면 buildingName을 리턴합니다. 그 이외에는 nil을 리턴합니다.

 


Accessing Properties Through Optional Chaining

위에서 설명한 것처럼 optional chaining을 사용하여 속성의 optional 값에 액세스하고 액세스가 성공했는지 확인할 수 있습니다.

 

이제 위에서 정의한 클래스로 새로운 Person 인스턴스를 생성하고, numberOfRooms에 액세스해보도록 하겠습니다.

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."

john.residence가 nil이기 때문에 optional chaining 호출은 이전과 같이 실패합니다.

 

또한, optional chaining을 통해서 속성의 값을 시도할 수 있습니다.

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

john.residence가 아직 nil이기 때문에, 위 코드에서 john.residence의 address 속성을 설정하려는 시도는 실패합니다. 

 

이 할당은 optional chaining의 일부입니다. 즉, = 연산자 오른쪽에 있는 코드는 체크되지 않습니다. 다만, someAddress가 앞으로 쓰이지 않는다는 것을 쉽게 알 수 있지 않습니다. 아래 코드는 위와 동일한 할당을 수행하지만, 함수를 통해 address 인스턴스를 생성합니다. 이 함수는 값을 반환하기 전에 "Function called"를 호출하여 = 연산자의 우측부분이 실행되었는지 확인할 수 있습니다.

func createAddress() -> Address {
    print("Function was called.")

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"

    return someAddress
}
john.residence?.address = createAddress()

위 코드를 실행해보면 createAddress 함수가 실행되지 않았고, 출력도 없습니다.

 


Calling Methods Through Optional Chaining

Optional Chaining을 사용하여 Optional 값에 대해 메소드를 호출하고, 그 메소드 호출이 성공했는지 확인할 수 있습니다. 이 방법은 메소드의 반환값이 정의되지 않은 경우에도 사용할 수 있습니다.

 

Residence 클래스의 printNumberOfRooms() 메소드는 현재 numberOfRooms의 값을 출력합니다.

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

이 메소드에 리턴 타입은 지정되지 않았습니다. 그러나 리턴타입이 없는 함수나 메소드는 암시적으로 Void 타입의 리턴 타입을 갖습니다. 이는 이 메소드가 빈 튜플인 () 값을 리턴한다는 것을 의미합니다.

 

만약 이 메소드를 optional chaining으로 호출한다면, 메소드의 리턴 타입은 Void?가 됩니다. 이는 if 구문을 사용하여 printNumberOfRooms()을 성공적으로 호출했는지 확인할 수 있습니다. 따라서 printNumberOfRooms 호출로부터 리턴값을  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을 통해 속성을 설정하려는 시도도 동일합니다. 

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 Chaining을 사용하여 optional 값에 대한 서브스크립트로 값을 탐색하거나 설정하고, 해당 서브스크립트 호출이 성공했는지 확인할 수 있습니다.

 

아래 예제는 Residence 클래스에 정의된 서브스크립트를 사용하여 john.residence 속성의 rooms 배열을 탐색합니다. john.residence는 현재 nil이기 때문에 서브스크립트 호출은 실패합니다.

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."

 

동일하게, optional chaining과 서브스크립트를 사용하여 새로운 값을 설정할 수도 있습니다.

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

아직 residence는 nil이기 때문에 위의 시도도 실패합니다.

 

만약 Residence 인스턴스를 생성하고 1개 이상의 Rooms 인스턴스를 rooms 배열에 추가하여 john.residence에 할당하면, 이제 Residence 서브스크립트를 사용해 rooms 배열의 실제 항목에 액세스할 수 있습니다.

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

만약 서브스크립트가 optional 타입의 값을 반환한다면, 물음표 마크는 서브스크립트의 괄호 뒤에 위치해야합니다.

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을 사용할 수도 있습니다. 

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."

위 코드는 john.residence?.address?.street에 액세스하고 있습니다. 여기서 2레벨의 optional chaining이 사용됩니다.

john.residence는 현재 유효한 Residence 인스턴스를 포함하고 있습니다. 그러나 john.residence.address는 현재 nil입니다. 따라서, 이 호출은 실패합니다.

 

위 예시에서 street 속성의 타입은 String?입니다. 따라서, john.residence?.address?.street의 리턴 값 또한 String? 입니다.

이제 Address 인스턴스를 생성하고 할당하여, street 속성에 액세스하여 값을 설정하도록 해보겠습니다.

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 chaining을 통해 어떻게 optional type의 속성 값을 탐색하는지 알아봤습니다.

속성 뿐만 아니라 optional type의 값을 반환하는 메소드에 대해서도 optional chaining을 사용할 수 있습니다.

 

아래 예제 코드는 Address 클래스의 buildingIdentifier() 메소드를 optional chaining을 통해 호출합니다. 이 메소드는 String? 타입의 값을 반환합니다. 

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"."

 

'프로그래밍 > Swift' 카테고리의 다른 글

[Swift] Extensions  (0) 2022.02.03
[Swift] Type Casting  (0) 2022.01.29
[Swift] Deinitialization  (0) 2022.01.06
[Swift] Initialization  (0) 2022.01.03
[Swift] Inheritance (상속)  (0) 2021.12.30

댓글