티스토리 뷰
옵셔널 체이닝 (Optional Chaining)
옵셔널(optional)일 수 있는 인스턴스 내부의 프로퍼티, 메소드, 서브스크립트를 매번 nil 체크를 하지 않고 최종적으로 원하는 값 또는 nil 인지 판단하는 방법 입니다.
여러 쿼리들을 연결할 수 있으며, 링크중 하나라도 nil일 경우 최종값은 nil이 반환 됩니다.
(참고링크)
강제 언래핑 대체용도의 옵셔널 체이닝
(Optional Chaining as an Alternative to Forced Unwrapping)
옵셔널 체이닝은 옵셔널 값 뒤에 물음표(?)를 붙여서 표현 합니다.
강제 언래핑할때 뒤에 느낌표(!)를 붙이는것과 문법이 비슷하지만,
가장 큰 차이점은
강제 언래핑을 할때 값이 없으면 런타임 에러가 발생하고,
옵셔널 체이닝을 사용하면 런타임 에러 대신 nil을 반환합니다.
옵셔널 체이닝에 의해 nil값이 호출될 수 있기 때문에 옵셔널 체이닝의 값은 항상 옵셔널 값이 됩니다.
예를들어, Int를 반환하는 프로퍼티에서 옵셔널체이닝이 성공적으로 실행되면 Int? 를 반환하게 됩니다.
아래 예제는 옵셔널 체이닝과 강제 언래핑의 다른점을 보여줍니다.
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence 인스턴스는 numberOfRooms 라는 Int 프로퍼티를 하나 갖고 있고,
Person 인스턴스는 옵셔널 residence 프로퍼티를 Residence? 로 갖고 있습니다.
Person 인스턴스를 새로 생성하면, residence 프로퍼티는 옵셔널의 기본값인 nil로 초기화가 됩니다.
아래 코드에서 john은 residence 프로퍼티가 nil인 값을 가집니다.
let john = Person()
아래 코드처럼 person.residence.numberOfRooms에 접근하기 위해 강제 언래핑을 하게 되면 residence의 값이 없기 때문에 런타임 에러가 발생 합니다.
let roomCount = john.residence!.numberOfRooms
// runtime 에러
위의 코드의 numberOfRooms에 접근하기 위해 강제 언래핑 대신 옵셔널 체이닝을 사용합니다.
if let roomCount = john.residence?.numberOfRooms {
print("john네 객실이 \(roomCount)개야.")
} else {
print("객실을 검색할 수 없습니다.")
}
// 객실을 검색할 수 없습니다.
numberOfRooms 프로퍼티는 옵셔널이 아니지만 numberOfRooms 값에 접근하기 위해 사용했던 residence 프로퍼티가 옵셔널이기 때문에 최종값은 옵셔널 값이 됩니다.
아래와 같이 nil이었던 residence 값에 Residence 인스턴스를 생성해서 추가할 수 있습니다.
john.residence = Residence()
john.residence는 이제 Residence 인스턴스를 가지므로 더이상 nil이 아닙니다.
numberOfRooms에 접근하면 nil 대신, 값 1을 갖는 Int? 를 반환 합니다.
john.residence = Residence()
if let roomCount = john.residence?.numberOfRooms {
print("john네 객실이 \(roomCount)개야.")
} else {
print("객실을 검색할 수 없습니다.")
}
// john네 객실이 1개야.
옵셔널 체이닝을 위한 모델 클래스 정의
(Defining Model Classes for Optional Chaining)
옵셔널 체이닝을 한 단계 이상 (여러 레벨로)의 프로퍼티 메소드, 서브스크립트에서 사용할 수 있습니다.
아래 코드는 위에서 했던 Person, Residence 모델을 확장해 Room과 Address 클래스를 추가한 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?
}
Residence는 rooms 라는 프로퍼티를 소유하며, 이 프로퍼티는 [Room] 타입의 빈 배열로 초기화 됩니다.
(질문) 여기서의 Residence 가 Room인스턴스의 배열을 소유하고 있기 때문에 numberOfRooms 프로퍼티는 계산된 프로퍼티로 선언됩니다.
rooms 배열에 접근하기 위해 서브스크립트를 선언합니다.
rooms 배열의 Room 클래스는 name을 초기화 때 인자로 받는 간단한 클래스 입니다.
class Room {
let name: String
init(name: String) { self.name = name }
}
Address 클래스는 3개의 String? 옵셔널 프로퍼티를 갖습니다. (buildingName, buildingNumber, street)
String? 타입을 반환하는 buildingIdentifier() 메소드는 buildingNumber와 street를 확인해
값이 있으면 buildingNumber와 결합된 street 값을 반환하고, 값이 없는 경우 nil을 반환 합니다.
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifir() -> String? {
if let buildingNumber = buildingNumber,
let street = street {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
옵셔널 체이닝을 통한 프로퍼티의 접근
(Accessing Properties Through Optional Chaining)
위의 코드에서 옵셔널 체이닝을 이용해 프로퍼티에 접근하는걸 봤을겁니다.
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("john네 객실이 \(roomCount)개야.")
} else {
print("객실을 검색할 수 없습니다.")
}
위의 코드의 경우 residence? 가 nil이기 때문에 옵셔널 체이닝 결과 nil을 호출하게 됩니다.
옵셔널 체이닝을 값을 할당하는데 사용할 수도 있습니다.
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
Address() 인스턴스를 생성해 john.residence.address로 할당하는 코드 입니다.
john.residence?가 nil이기 때문에 address 할당은 실패합니다.
할당받는 왼쪽 항이(john.residence?.address) nil이면 오른쪽 항은 실행도 되지 않습니다.
아래 메소드를 만들어 실행 해보면 creatAddress()에 print가 불려지지도 않는걸 볼 수 있습니다.
(왼쪽 항에서 nil이기 때문에 실행이 더이상 되지 않기 때문)
func createAddress() -> Address {
print("Function was called")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress() // nil
옵셔널 체이닝을 통한 메소드 호출
(Calling Methods Through Optional Chaining)
옵셔널 체이닝을 이용해 메소드를 호출할 수 있으며, 메소드가 성공적으로 호출됐는지 확인도 할 수 있습니다.
Residence 클래스 안에 있는 printNumberOfRooms() 메소드는 numberOfRooms의 값을 print 합니다.
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
위 메소드는 반환값이 명시 되어 있지 않지만, Void? 타입이 반환 됩니다.
(함수나 메소드는 리턴 값이 없는 경우 암시적으로 Void 라는 값을 갖습니다.)
아래 코드는 john.residence?.printNumberOfRooms() 메소드 호출 결과가 nil인지 아닌지 비교하고 그 결과에 대한 처리를 합니다.
let john = Person()
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."
위에서 언급한것 처럼 printNumberOfRooms() 는 직접적인 반환 값이 명시되어 있지 않지만 암시적으로 Void를 반환하고 이 메소드 호출이 옵셔널 체이닝에서 이루어지기 때문에
Void? 가 반환되어 nil 비교를 할 수 있습니다.
옵셔널 체이닝을 통해 값을 할당하면 Void? 값을 반환 하기 때문에, 해당 값이 nil인지 아닌지 비교할 수 있습니다.
let john = Person()
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
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)
옵셔널 체이닝을 이용해 서브스크립트로 접근할 수 있습니다.
NOTE
옵셔널 값을 서브스크립트로 접근 하기 위해서 괄호 [ ] 전에 물음표(?) 기호를 붙여서 사용합니다
아래 예제는 서브스크립트를 이용해 rooms에서 첫 rooms의 name을 요청하는 코드 입니다.
john.residence가 nil이기 때문에 서브스크립트 호출에는 실패합니다.
let john = Person()
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
// Unable to retrieve the first room name.
옵셔널 체이닝에서 서브스크립트로 값을 가져오는 것과 유사한 형태로 값을 할당할 수 있습니다.
john.residence?[0] = Room(name: "Bathroom")
이 코드는 첫 room 값을 할당하는 코드인데 room을 할당 하지 못하고 fail 이 발생합니다. (residence가 nil이기 때문)
아래 코드와 같이 Residence 인스턴스를 할당하면 residence 서브스크립트를 사용할 수 있습니다.
let john = Person()
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.")
}
// The first room name is Living Room.
옵셔널 타입의 서브스크립트 접근
(Accessing Subscripts of Optional Type)
Dictionary Type 같이 서브스크립트의 결과로 옵셔널을 반환 하면 괄호 [ ]뒤에 물음표 (?) 를 붙여줍니다.
var testScores = ["Dave": [86, 82], "Bev": [79, 94]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72 // nil
print(testScores)
// ["Bev": [80, 94], "Dave": [91, 82]]
체이닝의 다중 레벨 연결
(Linking Multiple Levels of Chaining)
옵셔널 체이닝이 여러단계에 걸쳐 연결 될 수 있습니다.
- 아래와 같이 상위 값(residence)도 옵셔널, 하위 값(address)도 옵셔널이라고 해서 Optional(Optional(value)) 이런식으로 되진 않습니다.
john.residence?.address?.street
- 옵셔널 체이닝의 상위 레벨 값이 옵셔널인 경우 현재 값이 옵셔널이라도 그 값은 옵셔널이 됩니다.
아래 예제는 address가 nil이기 때문에 옵셔널 체이닝은 fail이 됩니다.
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 인스턴스 생성 후 할당한 다음 다시 옵셔널 체이닝을 해보면
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."
Residence, Address 인스턴스가 모두 존재하기 때문에 옵셔널 체이닝은 아래 값을 print 하게 됩니다.
"John's street name is Laurel Street."
체이닝에서 옵셔널 값을 반환하는 메소드
(Chaining on Methods with Optional Return Values)
옵셔널 체이닝에서 반환 값이 있는 메소드를 호출할 수 있습니다.
Address에 있는 buildingIdentifier() 메소드를 사용한 예제를 보겠습니다.
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."
풀이
buildingIdentifier() 메소드를 보면,
func buildingIdentifier() -> String? {
if let buildingNumber = buildingNumber,
let street = street {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
buildingNumber, street 값이 있으면 buildingNumber, street를 반환하고,
buildingName이 nil이 아니면 buildingName을 반환하게 되어있습니다.
우리는 좀전에 buildingName과 street에 값을 넣어줬습니다.
let johnsAddress = Address() // 인스턴스 생성
// 값 할당
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
buildingNumber가 없으니 다음 else if 부분으로 넘어가서 buildingName 조건문으로 갈거고,
buildingName의 값이 있으니 buildingName을 return 합니다.
만약 메소드의 반환 값을 갖고 추가적인 구현을 하고 싶으면 메소드 호출 뒤에 물음표(?)를 붙인후 이어주면 됩니다.
john.residence?.address?.buildingIdentifier()?.hasPrefix("The")
아래는 메소드의 반환값을 갖고 .hasPrefix() 를 사용한 예 입니다.
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 does not begin with \"The\".")
}
}
// Prints "John's building identifier begins with "The"."
NOTE
옵셔널이 메소드의 반환 값에 있기 때문에 메소드 괄호 뒤에 "?" 를 붙여줍니다.
(메소드 자체에 옵셔널이 있는게 아니기 때문)
아래는 추가로 알고 있으면 좋을 내용들 입니다.
옵셔널 체이닝의 마지막 표현식이 옵셔널이더라도 ?를 생략합니다.
let email = person.contacts.email? (x)
let email = person?.contacts.email (o)
옵셔널 체이닝의 표현식중 하나라도 nil이면, 이어지는 표현식은 평가하지 않고 nil을 반환합니다.
person = nil
let email = person?.contacts.email
// person 까지만 접근하고 nil을 반환 해버립니다.
객체지향 생활체조
옵셔널체이닝 여러개 하지말아라 (체이닝을 끊어서 하자)
나쁜 예
let phone = person?.contact.phone
좋은 예
let contact = person?.contact
let phone = contact.phone
why?
하나의 상수가 너무 깊게 관여하게 되어
추후 유지보수에 어려움이 있게되고,
코드 한 줄에 "." 하나만 사용하여 코드의 가독성을 늘릴 수 있기 때문입니다.
디미터의 법칙: '친구하고만 말하라'
'iOS' 카테고리의 다른 글
[iOS] ViewController LifeCycle (생명주기) (0) | 2022.05.27 |
---|---|
[iOS] Reactive Programming 이란? (0) | 2022.05.25 |
[iOS] TableView, CollectionView 차이점 (0) | 2022.05.20 |
[iOS] class의 성능을 향상 시키는 방법 (0) | 2022.05.18 |
[Swift] 소멸자 (Deinitialization) (0) | 2022.05.15 |
- Total
- Today
- Yesterday
- Class
- 2023년 회고
- Swift Error Handling
- swift programmers
- Swift 알고리즘
- RTCCameraVideoCapturer
- swift reduce
- swift 고차함수
- Swift joined()
- swift protocol
- 원티드 프리온보딩
- Swift inout
- Swift 내림차순
- Swift 프로그래머스
- Swift
- Swift init
- RIBs tutorial
- CS 네트워크
- Swift 프로퍼티
- removeLast()
- swift (programmers)
- Swift joined
- swift property
- Swift RIBs
- Swift Leetcode
- Combine: Asynchronous Programming with Swift
- Swift ModernRIBs
- Swift final
- iOS error
- ios
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |