여러 쿼리를 함께 연결할 수 있으며 체인의 링크가 nil인 경우 전체 체인이 정상적으로 실패합니다.
Swift의 옵셔널 체이닝은 Objective-C의 nil 메시지와 유사하지만 모든 유형에서 작동하며 성공 또는 실패를 확인할 수 있습니다.
옵셔널이 nil이 아닌 경우 프로퍼티, 메서드 또는 서브 스크립트를 호출하려는 옵셔널 값 뒤에 물음표(?)를 배치하여 옵셔널 체이닝을 지정합니다.
이는 값을 강제로 풀기 위해 옵셔널 값 뒤에 느낌표(!)를 배치하는 것과 매우 유사합니다.
주요 차이점은 옵셔널이 nil일 때 옵셔널 체이닝이 정상적으로 실패하는 반면, 옵셔널이 nil일 때 강제 언래핑은 런타임 오류를 트리거한다는 것입니다.
옵셔널 체이닝이 nil 값에 대해 호출될 수 있다는 사실을 반영하기 위해 옵셔널 체이닝 호출의 결과는 쿼리하는 프로퍼티, 메서드 또는 서브 스크립트가 옵셔널이 아닌 값을 반환하더라도 항상 옵셔널 값입니다.
이 옵셔널 반환 값을 사용하여 옵셔널 체이닝 호출이 성공했는지(반환된 옵셔널 값에 값이 포함됨) 체인의 nil 값으로 인해 성공하지 못한지(반환된 옵셔널 값은 nil임) 확인할 수 있습니다.
특히, 옵셔널 체이닝 호출의 결과는 예상되는 반환 값과 동일한 유형이지만 옵셔널으로 래핑됩니다.
일반적으로 Int를 반환하는 프로퍼티은 Int? 옵셔널 체이닝을 통해 액세스할 때.
다음 몇 가지 코드 조각은 옵셔널 체이닝이 강제 래핑 해제와 어떻게 다른지 보여주고 성공 여부를 확인할 수 있도록 합니다.
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence 인스턴스에는 numberOfRooms라는 단일 Int property이 있으며 기본값은 1입니다.
Person 인스턴스에는 Residence? 유형의 옵셔널 property property 있습니다.
새 Person 인스턴스를 만드는 경우 해당 거주 프로퍼티은 선택 사항이기 때문에 기본적으로 nil로 초기화됩니다.
아래 코드에서 john의 residence property 값은 nil입니다.
let john = Person()
이 사람의 residenced의 numberOfRoom 프로퍼티에 액세스하려고 하면residenced 뒤에 느낌표를 배치하여 해당 값의 래핑 해제를 강제 실행하면 래핑 해제할 residenced 값이 없기 때문에 런타임 오류가 발생합니다.
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error
위의 코드는 john.residence가 nil이 아닌 값을 가질 때 성공하고 roomCount를 적절한 방 수를 포함하는 Int 값으로 설정합니다. 그러나 이 코드는 위에서 설명한 것처럼 거주가 nil일 때 항상 런타임 오류를 발생시킵니다.
옵셔널 체이닝은 numberOfRooms 값에 액세스하는 대체 방법을 제공합니다. 옵셔널 체이닝을 사용하려면 느낌표 대신 물음표를 사용하세요.
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."
이것은 옵셔널인 거주 프로퍼티에 대해 "연결"하고 거주가 존재하는 경우 numbderOfRooms의 값을 검색하도록 Swift에 지시합니다.
numberOfRooms에 액세스하려는 시도는 실패할 가능성이 있으므로 옵셔널 체이닝 시도는 Int? 또는 "옵셔널 Int" 유형의 값을 반환합니다.
위의 예에서와 같이 거주지가 nil인 경우 numberOfRooms에 액세스할 수 없다는 사실을 반영하기 위해 이 옵셔널 Int도 nil이 됩니다.
옵셔널 Int는 옵셔널 바인딩을 통해 액세스하여 정수를 래핑하고 roomCount 상수에 옵셔널이 아닌 값을 할당합니다.
numberOfRooms가 옵셔널 Int가 아닌 경우에도 마찬가지입니다.
옵셔널 체인을 통해 쿼리된다는 사실은 numberOfRooms에 대한 호출이 항상 Int? 대신에 Int를 리턴한다는걸 의미한다.
Residence 인스턴스를 john.residence에 할당하여 더 이상 nil 값을 갖지 않도록 할 수 있습니다.
john.residence = Residence()
john.residence는 이제 nil이 아닌 실제 Residence 인스턴스를 포함합니다. 이전과 동일한 옵셔널 체이닝로 numberOfRoom에 액세스하려고 하면 이제 Int? 기본 numberOfRooms 값 1을 포함합니다.
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)."
한 수준 이상 깊이 있는 프로퍼티, 메서드 및 첨자에 대한 호출과 함께 옵셔널 체이닝을 사용할 수 있습니다.
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?
}
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 let buildingNumber = buildingNumber, let street = street {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
Forced Unwrapping의 대안으로 Optional Chaining에서 설명한 것처럼 옵셔널 체이닝을 사용하여 옵셔널 값의 프로퍼티에 액세스하고 해당 프로퍼티 액세스가 성공적인지 확인할 수 있습니다.
위에서 정의한 클래스를 사용하여 새 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이기 때문에 이 옵셔널 체이닝 호출은 이전과 같은 방식으로 실패합니다.
옵셔널 체이닝을 통해 프로퍼티 값을 설정하려고 시도할 수도 있습니다.
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
이 예에서 john.residence가 현재 nil이기 때문에 john.residence의 주소 프로퍼티을 설정하려는 시도는 실패합니다.
할당은 옵셔널 체이닝의 일부입니다. 즉, = 연산자의 오른쪽에 있는 코드는 실행되지 않습니다.
앞의 예에서 상수에 접근하면 부작용이 없기 때문에 someAddress가 실행되지 않는 것을 보기가 쉽지 않습니다.
아래 목록은 동일한 할당을 수행하지만 함수를 사용하여 주소를 생성합니다.
이 함수는 값을 반환하기 전에 "Function was 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() 함수가 호출되지 않았음을 알 수 있습니다.
옵셔널 체이닝을 사용하여 옵셔널 값에 대한 메서드를 호출하고 해당 메서드 호출이 성공했는지 확인할 수 있습니다.
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
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."
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."
옵셔널 체이닝을 사용하여 옵셔널 값의 하위 첨자에서 값을 검색 및 설정하고 해당 하위 첨자 호출이 성공적인지 여부를 확인할 수 있습니다.
옵셔널 체이닝을 통해 옵셔널 값의 서브 스크립트에 액세스할 때 물음표는 서브 스크립트의 대괄호 앞이 아니라 뒤에 둡니다.
옵셔널 체이닝 물음표는 항상 표현식의 옵셔널 부분 바로 뒤에 옵니다.
아래 예제는 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."
이 서브 스크립트 호출에서 옵셔널 체이닝 물음표는 john.residence 바로 뒤에 첨자 대괄호 앞에 배치됩니다, john.residence는 옵셔널 체이닝이 시도되는 옵셔널 값이기 때문입니다.
유사하게, 옵셔널 체이닝을 사용하여 서브 스크립트를 통해 새 값을 설정하려고 시도할 수 있습니다.
john.residence?[0] = Room(name: "Bathroom")
이 서브 스크립트 설정 시도도 현재 거주가 0이기 때문에 실패합니다.
실제 Residence 인스턴스를 생성하여 john.residence에 할당하고 rooms 배열에 하나 이상의 Room 인스턴스가 있는 경우 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."
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]
여러 수준의 옵셔널 체이닝을 함께 연결하여 모델 내에서 더 깊은 프로퍼티, 메서드 및 첨자를 드릴다운할 수 있습니다.
그러나 여러 수준의 옵셔널 체이닝은 반환된 값에 더 많은 수준의 선택성을 추가하지 않습니다.
요약:
그러므로
아래 예시는 john의 거주지 프로퍼티의 address 프로퍼티의 street 프로퍼티에 접근을 시도합니다.
여기에는 두 가지 수준의 옵셔널 체이닝이 사용되어 거주 및 주소 프로퍼티을 통해 연결되며 둘 다 선택 유형입니다.
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의 값에는 현재 유효한 레지던스 인스턴스가 포함되어 있습니다.
그러나 john.residence.address의 값은 현재 nil입니다. 이 때문에 john.residence?.address?.street에 대한 호출이 실패합니다.
위의 예에서는 거리 프로퍼티의 값을 검색하려고 합니다. 이 프로퍼티의 유형은 String?입니다.
따라서 john.residence?.address?.street의 반환 값은 프로퍼티의 기본 옵셔널 유형에 추가하여 두 가지 수준의 옵셔널 체이닝이 적용되더라도 String?이기도 합니다.
실제 주소 인스턴스를 john.residence.address의 값으로 설정하고 주소의 거리 프로퍼티에 대한 실제 값을 설정하면 다단계 옵셔널 체이닝을 통해 거리 프로퍼티 값에 액세스할 수 있습니다.
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."
이 예에서 john.residence의 값이 현재 유효한 Residence 인스턴스를 포함하고 있기 때문에 john.residence의 address 프로퍼티을 설정하려는 시도는 성공할 것입니다.
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."
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"."
위의 예에서는 연결하는 옵셔널 값이 buildingIdentifier() 메서드 자체가 아니라 buildingIdentifier() 메서드의 반환 값이기 때문에 괄호 뒤에 옵셔널 체이닝 물음표를 배치합니다.