이상하게 옵셔널 체이닝이 너무 너무 헷갈려서 공식문서를 보고 정리해보았다.
옵셔널 체이닝이란, 옵셔널의 프로퍼티, 메서드, 서브스크립트를 불러올 때 사용되는 방법이다. 옵셔널이 값을 가지면 옵셔널의 프로퍼티, 메서드, 서브스크립트 불러오기 성공, 옵셔널이 nil이라면, nil을 리턴한다. 복수의 queries 가 체인처럼 이어져서 그 중 하나라도 nil이라면 nil을 리턴한다.
프로퍼티, 메서드, 서브스크립트를 가져오고 싶은 옵셔널값 뒤에 ? 를 붙여서 사용한다. 이것은 강제 언래핑 ! 이랑 비슷하다. (! 자리에 ? 쓴다)
하지만 중요한 차이점은 옵셔널이 nil일 때 강제 언래핑의 경우 강제로 벗기면 런타임 에러가 나지만,
옵셔널 체이닝을 사용하여 벗기면 우아하게 nil을 리턴하며 실패를 한다는 것이다.
옵셔널 체이닝 실패시 nil을 리턴한다는 점에서, 옵셔널 체이닝은 항상 옵셔널값을 리턴한다는 것을 알 수 있다.
→ 심지어 옵셔널값의 프로퍼티,메서드 등이 옵셔널값이 아니라도 옵셔널값으로 리턴한다
예) Int를 리턴하는 프로퍼티에 옵셔널 체이닝으로 접근시 Int? 리턴
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
let john = Person()
현재 옵셔널 타입으로 선언된 residence에 값이 없는 상태
//방법1) 강제 언래핑 !
let roomCount = john.residence!.numberOfRooms
print(roomCount) //residence가 nil이라면 런타임 에러
//방법2) 옵셔널 체이닝 ?
if let roomCount = john.residence?.numberOfRooms {
print(roomCount)
} else {
print("값 가져오기 실패")
}
: residence 값을 강제로 벗겨서 프로퍼티인 roomCount 값 가져와라
→ 강제로 벗겨서 만약 값이 없다면 런타임 에러가 일어나는 대참사
: 만약 residence 값이 있다면 프로퍼티인 roomCount 값 가져오고, 없다면 nil을 리턴해라 → 값이 없어도 우아하게 실패해서 괜찮음
: nil리턴 가능성이 있으니 항상 옵셔널 값을 리턴함
그런데 왜 print(roomCount)는 옵셔널 값이 아닌걸까?
옵셔널 체이닝이 성공하여 값이 리턴되면, let roomCount에 할당되어 옵셔널 바인딩 형태로 옵셔널을 까준다.
따라서 옵셔널 체이닝 성공시 → 바인딩도 성공해서 최종적으로는 옵셔널값이 아닌 값이 나오는 것이다.
john.residence = Residence()
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("방개수: \(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
}
}
}
헷갈려서 그림을 그려보았다
john의 address는 1) 옵셔널 체이닝으로 할당해줄 수도 없고, 2) 옵셔널 체이닝으로 가져올 수 없다.
그 이유는 현재 john.residence 가 nil이기 때문이다.
john의 address를 꺼내오려면 꺼내오는 길목에 residence라는 단계를 거쳐야하는데 nil이라서 꺼내기를 실패하게 된다.
let john = Person()
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "사과로"
john.residence?.address = someAddress //john의 주소를 할당해주기 실패
if let johnsAddress = john.residence?.address {
print("주소: \(johnsAddress)")
} else {
print("주소 가져오기 실패")
}
//주소 가져오기 실패
john의 residence에 johnsHouse 할당해준 후, room 프로퍼티도 추가해주고 나서
옵셔널 체이닝으로 numberOfRooms를 꺼내올 수 있다.
let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "거실"))
johnsHouse.rooms.append(Room(name: "거실"))
john.residence = johnsHouse
if let roomCount = john.residence?.numberOfRooms {
print("방 개수: \(roomCount)")
} else {
print("방 개수 가져오기 실패")
}
Address 클래스의 인스턴스 johnsAddress가 있을 때 비로소 john의 street를 꺼내올 수 있다.
let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "거실"))
johnsHouse.rooms.append(Room(name: "거실"))
john.residence = johnsHouse
let johnsAddress = Address()
johnsAddress.buildingName = "사과빌딩"
johnsAddress.street = "사과로"
john.residence?.address = johnsAddress
if let johnsStreetAddress = john.residence?.address?.street {
print("도로명주소: \(johnsStreetAddress)")
} else {
print("도로명주소 가져오기 실패")
}
위 코드는 사실 아래의 코드처럼 옵셔널 바인딩을 중첩으로 사용해야하는데 너무 번거로워서 옵셔널 체이닝이 만들어진 것이다.
if let johnsHouse = john.residence {
if let johnsAddress = johnsHouse.address {
if let johnsStreet = johnsAddress.street {
print("도로명주소: \(johnsStreet)")
} else {
print("도로명주소 가져오기 실패")
}
}
}