[Swift] Optional Chaining

상 원·2022년 7월 24일
0

Swift

목록 보기
27/31
post-thumbnail

Optional Chaining은 프로퍼티, 메소드, 서브스크립트를 현재 nil일수도 있는 optional로 검색하고 호출하는 것을 의미함.
옵셔널이 값이 있다면 프로퍼티, 메소드, 서브스크립트 호출이 성공하지만
nil이라면 요 호출은 nil을 리턴함!

다수의 쿼리(조회)가 chain될 수 있고(엮일 수 있고), 그 중 하나라도 실패하면 전체 체인이 실패해버림!

Optional Chaining as an Alternative to Forced Unwrapping

옵셔널이 nil이 아닌 프로퍼티, 메소드, 서브스크립트 뒤에 물음표(?)를 붙여서 옵셔널 체이닝이라는 것을 알려줄 수 있음!

느낌표를 써주는 강제 언래핑이랑 상당히 비슷하지만 강제 언래핑은 값이 nil이면 런타임에러를 뱉어내는 반면 옵서널 체이닝을 사용하면 우아하게(?) 종료된다고 한다.

옵셔널 체이닝이 nil값에 쓰일 수도 있기 때문에 요걸 인식해서 옵셔널 체이닝의 결과는 항상 옵셔널 값임! non-optional인 거에도 마찬가지로 옵셔널 값을 리턴해줌.
그래서 요 리턴된 옵셔널 값이 nil이면 옵셔널 체이닝이 실패한 거고, nil이 아니라 값이 들어가 있으면 성공한 것!

옵셔널 체이닝으로 리턴된 값의 타입은 원래 타입과 같긴 하지만 옵셔널으로 싸인 타입임!
Int 타입의 프로퍼티를 옵셔널 체이닝으로 접근했다면 Int? 타입이 반환된당.

다음 예제로 강제 언래핑과 옵셔널 체이닝의 차이점을 알아봅시다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

// Person 인스턴스를 새로 만들면, residence 프로퍼티는 nil로 초기화된다.
let john = Person()

// ***강제 언래핑***
// residence 프로퍼티가 옵셔널이기 때문에 강제 언래핑(!)을 통해서 
// 접근하면 언래핑할 값이 없기 때문에 런타임 에러가 남.
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

// ***옵셔널 체이닝***
// 강제 언래핑을 사용했던 ! 자리에 ? 를 집어넣어서 옵셔널 체이닝으로 만든다.

// Swift는 옵셔널 값인 residence 프로퍼티를 "체이닝" 하고 값이 존재한다면 
// 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."

numberOfRooms 로의 접근이 실패할 수도 있기 때무네 옵셔널 체이닝은 Int? 의 타입을 리턴함!

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)."

이런식으로 Residence 인스턴스를 john.residence에 할당해 주고 다시 옵셔널 체이닝을 통해 접근하면 1의 값을 가진 Int? 타입을 반환해 준다.

Defining Model Classes for Optional Chaining

한 단계 더 밑에 있는 프로퍼티, 메소드, 서브스크립트에도 옵셔널 체이닝을 쓸 수 있다. 무슨 개소리?
이를 통해 서로 연관된 타입들이 있는 복잡한 모델 안의 서브프로퍼티에 접근할 수 있고, 그 서브프로퍼티들의 프로퍼티, 메소드, 서브스크립트에 접근할 수 있는지 체크할 수도 있다.


진짜 공식 문서 정의는 무슨말인지 하나도 모르겠다...
예제로 한번 이해해 보자!

class Person {
    var residence: Residence?
}

class Residence {
    var rooms: [Room] = []
    var numberOfRooms: Int {
        return rooms.count
    }
    // rooms 배열에 인덱스로 접근할 수 있는 서브스크립트
    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
        }
    }
}

이렇게 여러 클래스가 엮여 있음!
이 예제는 4개의 model class를 정의하는데, 멀티레벨 옵셔널 체이닝의 예제도 포함돼 있음.

모델 클래스가 뭐임??

모델 클래스는 실제의 형태를 클래스로 만드는 것!
즉 Address 클래스라면 주소를 표현하는 데 필요한 건물 이름, 건물 번호, 거리 이름 등의 정보를 갖고 있을 수 있음.

일단 여기서는 클래스만 정의해두고 아래쪽에서 사용하는 걸 보여줌.

Accessing Properties Through 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."

Person 의 인스턴스를 만들고, 이때 바로 residence 에 접근하면 당연히 접근이 불가능하다. else 파트가 실행됨.

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

Address 의 인스턴스를 만들고 요걸 만들어둔 residence의 address에 넣어주면? 실패한다.
아직도 john.residence? 의 값은 nil이기 때문!
요 할당은 옵셔널 체이닝의 일부임. 등호(=)의 오른쪽 부분은 아예 평가되지 않음!
위 코드에서는 someAddress 가 상수라 접근에 아~무 문제가 없어서 평가되는지 아닌지 보기가 어렵기 때문에 아래 수정된 코드로 다시 살펴보면,

func createAddress() -> Address {
    print("Function was called.")

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"

    return someAddress
}
john.residence?.address = createAddress()
// Prints nothing.

이런식으로 옵셔널 체이닝의 오른쪽에 함수를 넣어둠. 위랑 하는 역할은 같지만 요 함수가 실행된다면 당연히 "Function was called." 가 프린트돼야 하지만!
실행창을 보면 아무것도 프린트되지가 않는다.

요 예제에서는 옵셔널 체이닝이 실패한다면(nil이라면) 등호 오른쪽은 보지도 않고 고냥 실패를 찍어버린다는 것을 알 수 있었삼.

Calling Methods Through Optional Chaining

메소드 호출이 성공적인지 보기 위해서 메소드를 옵셔널 값으로 호출할 수 있음!
메소드가 반환하는 값이 없더라도 요런 옵셔널 체이닝을 사용할 수 있다.

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

printNumberOfRooms 메소드는 현재 numberOfRooms 를 출력해준다.

특별한 반환값이 없는 것 같지만 이렇게 반환값을 명시해두지 않은 메소드는 암시적으로 Void형을 반환하는 것이고, 비어있는 튜플 ( )을 반환하는 거임!

이 메소드를 옵셔널 체이닝을 사용해 옵셔널 값으로 호출하면, 메소드의 반환값은 Void 형태가 아니다. 옵셔널 체이닝을 사용하면 반환값은 항상 옵셔널 타입이기 때문! 그래서 메소드가 반환값을 정의하지 않더라도 if 구문을 사용해서 요 메소드를 호출할 수 있는지 알 수 있다.

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 를 써서 요 함수의 호출결과가 nil인지 아닌지 판단할 수 있음!

위에서 봤던 것과 마찬가지로 프로퍼티의 값을 세팅할 때도 얘를 세팅할 수 있는지 없는지를 if 구문을 사용해서 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."

요런식으로 address 프로퍼티를 Address 클래스의 인스턴스인 someAddress로 초기화할 수 있는지 없는지를 nil이랑 비교해서 알 수 있음!

Accessing Subscripts Through Optional Chaining

서브스크립트를 통해 값에 접근해 세팅하는 것이 성공적인지 아닌지도 옵셔널 체이닝을 통해 알아볼 수 있따.

서브스크립트를 옵셔널 체이닝을 통해 옵셔널 값으로 접근하려면 ? 을 서브스크립트 괄호 앞에 써야 함! 요 ? 는 옵셔널인 표현 바로 뒤에 따라와야 하는거니까!

요 아래 코드는 john.residence 프로퍼티의 rooms 배열의 첫 번째 원소에 접근하는 거임. 이때 이용하는 서브스크립트는 Residence 클래스에 정의된 걸 씀!
지금 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?[0] = Room(name: "Bathroom")

이런식으로 초기화해보려고 해도 john.residence가 nil이기 때문에 안먹힘!

그니까 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."

Accessing Subscripts of Optional Type

딕셔너리 타입의 key 서브스크립트는 옵셔널 타입의 반환값을 가짐!
이런 경우에는 얘네의 반환값에 chain하기 위해 서브스크립트의 [ ] 뒤에 물음표를 달아준다.

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

profile
ios developer

0개의 댓글