Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Optional Chaining 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
옵셔널 체이닝Optional chaining은 Optional 프로퍼티, 메서드, 서브스크립트에 대고 질의querying하거나 호출하는 프로세스를 가리킨다. 옵셔널은 값이 있거나 nil이거나 둘 중 하나일텐데, 옵셔널이 값을 가지고 있다면 호출은 성공하고, 옵셔널이 nil이라면 호출은 nil을 리턴한다. 여러개의 쿼리가 한 번에 묶일chained 수 있으며 전체 체인 중 하나라도 nil이 되면 전체 체인이 실패한다.
<note> Swift에서의 옵셔널 체이닝은 Objective-C에서 nil에 메세지를 보내는 것과 유사하다. 다른 점은 Swift에서는 모든 타입에 대해 옵셔널 체이닝이 가능하며, nil에 대고 호출했을 시 nil을 리턴해주기 때문에 호출 실패 여부를 알 수 있다는 것이다.
강제 언랩핑의 대안이 되는 옵셔널 체이닝
옵셔널 타입의 프로퍼티, 메서드, 스크립트를 non-nil로 간주할 경우, 호출 시에 뒤에 물음표(?)를 붙여서 옵셔널 체이닝이라는 것을 명시할 수 있다. 이것은 느낌표(!)를 붙여서 강제 언랩핑 하는 것과 유사하다. 그러나 옵셔널 체이닝의 경우에는 만약 옵셔널 값이 nil이라도 그냥 nil을 리턴하며 실패만 할 뿐, 런타임 에러를 내지 않는다.
옵셔널 체이닝 호출의 결과는 항상 옵셔널 타입의 값이다. 기대되는 리턴 타입과 동일한 타입인데, 단지 옵셔널로 감싸져있을 뿐이다. nil에 대고 호출했을 때 nil이 리턴되어야 하기 때문에 리턴 타입은 옵셔널 타입이 되어야 하는 것이 당연하다. 리턴된 옵셔널 값이 nil이라면 옵셔널 체이닝 호출이 실패한 것이고, 값이 들어있다면 성공한 것이다.
그럼 예제를 통해 살펴보자.
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
let john = Person() // 이때 john.residence 는 nil
let roomCount = john.residence!.numberOfRooms
// 위 라인은 강제언랩핑. runtime error
if let roomCount = john.residence?.numberOfRooms {
// numberOfRooms는 옵셔널 타입이 아니지만
// 옵셔널 체이닝을 사용하고 있으므로
// john.residence?.numberOfRooms 의 리턴값은 옵셔널 Int.
print("john의 주거지에는 \(roomCount)개 방이 있다")
} else {
print("Jone의 주거지의 방 개수를 얻어올 수 없다")
}
let john = Person()
john.residence = Residence()
// 이제 john.residence?.numberOfRooms 의 리턴값은
// 1이라는 값을 가지는 옵셔널 Int이다.
if let roomCount = john.residence?.numberOfRooms {
print("john의 주거지에는 \(roomCount)개 방이 있다")
} else {
print("Jone의 주거지의 방 개수를 얻어올 수 없다")
}
옵셔널 체이닝을 위한 모델 클래스를 정의해보자.
프로퍼티, 메서드, 서브스크립트를 한 단계 더 깊게 호출할 수 있는 옵셔널 체이닝을 사용할 수 있다. 이것을 이용하면 서로 연관되어 있는 복잡한 모델들에서, 각 서브프로퍼티의 프로퍼티, 메서드, 서브스크립트에 접근할 수 있는지 체크해볼 수 있을 것이다.
예제를 통해 알아보자.
Person 클래스 (사람) : 위에서 정의한 것과 같다. Residence 인스턴스를 가진다.
Room 클래스 (방) : 방 이름을 가진다.
Address 클래스 (주소) : 옵셔널 스트링 타입의 빌딩이름, 빌딩번호, 도로이름을 가지고 있다.
Residence 클래스 (거처) : Room 인스턴스들의 배열을 가지고 있다. 인덱스를 통해 해당 Room 인스턴스에 접근할 수 있는 서브크립트를 구현하고 있다.
class Person {
var residence: Residence?
}
class Residence {
var rooms = [Room]()
var address: Address?
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("\(numberOfRooms)개")
}
}
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 buildingNumber != nil && street != nil {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
옵셔널 체이닝을 통해 프로퍼티에 접근해보자.
let john = Person() // 이때 john.residence 는 nil이다
if let roomCount = john.residence?.numberOfRooms {
print("john의 거처에는 \(roomCount)개의 방이 있다")
} else {
print("방 개수를 알 수 없음")
}
// 옵셔널 체이닝을 통해 프로퍼티의 값을 set하려고 "시도"할 수도 있다
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
// residence가 nil이니까 실패한다.
// 이런 케이스에서 someAddress는 never evaluated
func createAddress() -> Address {
print("function was called")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
// 아무것도 print되지 않는다.
// john.residence?.address의 리턴값이 nil이기 때문에
// createAddress() 메서드 자체가 호출될 일이 없다 (never evaluated)
옵셔널 체이닝을 통해 메서드를 호출해보자.
옵셔널 체이닝을 통해 메서드를 호출하면 성공/실패 여부를 알 수 있다. 해당 메서드가 리턴 타입이 없더라도 말이다. (리턴 타입이 없는 메서드라도 사실은 Void 리턴 타입을 가지며 리턴되는 것은 빈 튜플이다)
예제에서는 Residence 클래스에 다음과 같은 리턴 타입 없는 메서드가 있다.
func printNumberOfRooms() {
print("방의 개수는 \(numberOfRooms)개")
}
옵셔널 체이닝을 통해 이 메서드를 호출할 경우 리턴 타입은 Void가 아니라 Void? 가 될 것이다.
if john.residence?.printNumberOfRooms() != nil {
print("방 개수를 알 수 있음")
} else {
print("방 개수를 알 수 없음")
}
// 옵셔널 체이닝을 통해 프로퍼티를 set하려고 "시도"할 때도
// 성공하든 실패하든 Void? 가 리턴된다.
// 따라서 다음과 같이 프로퍼티 셋팅이 성공했는지를 체크할 수 있다
if (john.residence?.address = someAddress) != nil {
print("프로퍼티 셋팅 성공")
} else {
// john.residence가 nil이라면 set을 실패해서 여기로 들어옴
print("프로퍼티 셋팅 실패")
}
옵셔널 체이닝을 통해 서브스크립트에 접근해보자.
서브스크립트의 경우에도 크게 다르지 않다. 옵셔널체이닝을 통해 get/set을 시도할 수 있고 성공/실패 여부를 체크해볼 수 있다.
// get 시도
// <주의> 물음표(?)는 항상 옵셔널 표현식의 뒤에 위치해야 한다
// 아래는 john.residence?[0].name 이므로
// john의 residence가 nil인 경우를 처리하고자 하는 것이다
if let firstRoomName = john.residence?[0].name {
print("첫 번째 방은 \(firstRoomName)")
} else {
print("첫 번째 방 가져올 수 없음")
}
// set 시도
john.residence?[0] = Room(name: "욕실")
// 이 시점에서는 john.residence 가 nil이므로 시도는 둘 다 실패한다
let johnHouse = Residence()
johnHouse.rooms.append(Room(name: "거실"))
john.residence = johnHouse
// get 시도가 이때는 성공한다
if let firstRoomName = john.residence?[0].name {
print("첫 번째 방은 \(firstRoomName)")
} else {
print("첫 번째 방 가져올 수 없음")
}
<옵셔널 타입의 서브스크립트에 접근해보자>
만약 서브스크립트가 옵셔널 타입을 리턴한다면(ex: 딕셔너리의 key를 받아서 value를 돌려주는 서브스크립트. key에 해당하는 value가 없을 수도 있다), 물음표(?)를 서브스크립트의 괄호 뒤에 위치시켜서 옵셔널 체이닝을 해보자.
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// 마지막 set 시도는 실패하지만 crash는 나지 않는다. (옵셔널 체이닝의 장점)
체이닝을 여러 레벨로 연결하기Linking Multiple Levels of Chaining
옵셔널 체이닝을 여러 개 연결할 수 있다. 그러나 여러 개 연결한다고 해서 옵셔널 타입이 몇 겹으로 감싸지는 건 아니다. 옵셔널이 아닌 값은 옵셔널로 반환이 되고, 이미 옵셔널인 값은 그냥 그대로 옵셔널로 반환이 된다. (옵셔널을 한 번 더 옵셔널로 감싸서 반환되는 게 아니라는 말이다)
if let johnsStreet = john.residence?.address?.street {
// 두 번 옵셔널 체이닝이 걸렸더라도
// john.residence?.address?.street 의 반환값은 옵셔널 String
print("john이 사는 거리이름은 \(johnsStreet)")
} else {
print("거리이름 가져올 수 없음")
}
옵셔널 리턴 값을 이용해서 메서드 체이닝를 해보자Chaining on Methods with Optional Return Values
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
// john.residence?.address?.buildingIdentifier() 의 리턴 값은
// 옵셔널 String이다.
// 이 리턴값에 대해 또 한번 옵셔널 체이닝을 걸고 싶다면 아래처럼 하면 된다
print("John의 빌딩 identifier는 \(buildingIdentifier)")
}
if let buildingIdentifier = john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
print("John의 빌딩 identifier는 \(buildingIdentifier)")
}
// 만약 buildingIdentifier()가 반환하는 값이 nil이 될 경우에
// (빌딩 이름도 번호도 없으면 nil 반환한다)
// 다음과 같이 강제 언랩핑을 하면 런타임 에러가 날 것이기 때문에
// 위와 같이 하는 것임
if let buildingIdentifier = john.residence?.address?.buildingIdentifier()!.hasPrefix("The") {
print("John의 빌딩 identifier는 \(buildingIdentifier)")
}
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Type Casting (0) | 2017.03.23 |
---|---|
Error Handling (0) | 2017.03.18 |
Automatic Reference Counting (ARC) (1) | 2017.03.14 |
Deinitialization (0) | 2017.03.13 |
Initialization (3/3) (0) | 2017.03.13 |