-
Optional ChainingiOS/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
클래스는 이전 예시보다 더 복잡하다.rooms
는Room
들의 배열이며, 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
이면 할당이 실행되지 않는다. 다음 코드의 마지막 줄의 경우residence
가nil
이기 때문에 할당이 되지 않는다.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,residence
가nil
이기 때문에 할당이 실패한다.john.residence?[0] = Room(name: "Bathroom")
다음과 같이
john.residence
에Residence
인스턴스를 할당한다면 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?.address
는nil
이기 때문에 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