[Swift] Optional Chaining

LEEHAKJIN-VV·2022년 5월 23일
0

Study-Swift 5.6

목록 보기
18/22

참고사이트:
English: The swift programming language
Korean: The swift programming language


Optional Chaining (옵셔널 체이닝)

Optional chaining은 nil 일 수도 있는 프로퍼티, 메소드 그리고 서브스크립트를 호출하고 query(질의) 하는 과정이다. 만약 optional이 값을 가지고 있으면 프로퍼티, 메소드, 서브스크립트의 호출은 성공한다. 그러나 값을 가지고 있지 않다면 nil을 반환한다. 여러 개의 질의를 연결하여 사용할 수 있는데, 연결된 질의에서 optional 중 1개라도 nil이라면 전체 결과는 nil을 반환한다. (john.residence?.address?)

NOTE
Swift에서 optional chaining은 Objective-C의 nil 메시징과 비슷하다. 그러나 Swift에서는 모든 타입에서 사용이 가능하고 값을 가져오는데 성공했는지 실패했는지 확인이 가능하다.

Optional Chaining as an Alternative to Forced Unwrapping (옵셔널 체이닝은 강제 언래핑의 대안)

Optional Chaining은 호출하고자 하는 프로퍼티, 메소드 그리고 서브스크립트의 뒤에 물음표(?)을 붙여 사용한다. 이는 optional 값을 강제적 언래핑 하는 방식과 유사하다. 이 두 가지 방법의 차이점은 강제적 언래핑은 optional 값이 nil 일때 runtime error를 trigger하고, optional chaning은 error 없이 nil을 반환한다.

Optional Chaining의 결과는 nil이 될 수 있기 때문에 query 하는 메소드, 프로퍼티 및 서브스크립트의 타입과 상관없이 optional 타입을 반환한다. 이 반환 값으로 optional chaining의 성공 여부를 확인할 수 있다.

특히, optional chaning의 반환 값은 호출하고자 하는 프로퍼티, 메소드 그리고 서브스크립트의 타입의 값을 wrapping 한 것이다. 에를 들어 Int를 반환하는 프로퍼티는 optional chaining이 성공하면 Int? 을 반환한다.

다음 몇 개의 예제들은 강제적 언래핑과 optional chaining의 차이점과 optional chaning 성공 여부를 보여준다.

우선 2개의 클래스 PersonResidence을 구현한다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence 인스턴스는 1개의 Int 프로퍼티 numberOfRooms를 가지고 있고 Person 인스턴스는 1개의 Residence 타입의 optional 프로퍼티를 가진다.

Person의 인스턴스를 생성하면 residence 프로퍼티는 optional 타입이기 때문에 nil로 초기화된다. 아래 코드에서 john 인스턴스는 값이 nil인 residence 프로퍼티를 가진다.

let john = Person()

만약 강제적 언래핑을 이용하여 residence의 numberOfRooms 프로퍼티에 접근하면 wrapping이 되어있는 값이 없기 때문에 rutime error가 trigger된다.

let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

강제적 언래핑을 사용하지 않고 optional chaning을 사용하여 numberOfRooms 프로퍼티에 접근할 수 있다. optional chaning을 사용하는 방법은 프로퍼티 뒤에 물음표(?)을 작성한다.

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

위 코드를 보면 Swift는 optional 프로퍼티인 residence에 "chain(연결)"하고, residence의 값이 존재한다면 값을 검색하는 것을 알 수 있다.

위 예제에서 residence는 nil이므로 반환값도 nil이 된다. 만약 nil이 아닐 경우 반환 타입이 optioanl Int이므로 optional binding을 통해 값을 unwrap 하고 roomCount 상수에 할당한다.

numberOfRooms의 타입이 optional이 아니지만, optional chain을 통해 값을 접근하였으므로 Int가 아닌 Int? 을 반환한다.

john 인스턴스의 residence프로퍼티에 nil 대신 새로운 인스턴스를 할당할 수 있다.

john.residence = Residence()

이제 위와 같은 방법으로 optional chaining을 사용하여 numberOfRooms 프로퍼티에 접근하면 default 값인 1이 포함된 Int?를 반환하는 것을 확인할 수 있다.

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

Defining Model Classes for Optional Chaning (옵셔널 체이닝을 위한 모델 클래스 정의)

optional chaining을 한 레벨로 사용하는 것이 아닌 여러 레벨로 연결하여 사용할 수 있다. (multilevel optional chaining)

아래 코드는 다음 multilevel optional chaining의 예제에서 사용할 4가지 모델 클래스를 정의한다.

Person 클래스는 위 챕터와 같다.

class Person {
    var residence: Residence?
}

Residence 클래스는 전보다 복잡해졌다. 이 클래스는 [Room] 타입인 rooms 프로퍼티를 선언하였다.

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?
}

numberOfRooms는 computed property로 저장된 방의 수를 나타낸다. 이 프로퍼티는 getter만 구현하였다. 그리고 rooms 배열에 접근하기 위해 read-write 서브스크립트를 구현하였다. 또한 optional 프로퍼티인 addres을 선언하였다.

Room 클래스는 1개의 프로퍼티를 가지는 간단한 클래스로, 이 프로퍼티는 방의 이름을 나타낸다.

class Room {
    let name: String
    init(name: String) { self.name = name }
}

마지막 클래스로 Address를 정의한다. 이 클래스는 3개의 optional 프로퍼티를 가진다. 처음 프로퍼티 buildingNamebuildingNumber은 주소의 일부로 건물을 식별할 때 사용된다. street 프로퍼티는 주소의 부분인 거리 이름을 나타낸다.

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
        }
    }
}

이 클래스는 또한 buildingIdentifier() 메소드를 구현하는데, 반환 타입은 String?이다. 메소드는 주소의 프로퍼티를 확인하여 buildingNumberstreet의 값이 있으면 이를 문자열로 결합하여 반환하고, buildingName의 값이 있으면 반환하고 나머지 다른 경우는 nil을 반환한다.


Accessing Properties Trough Optional Chaining (옵셔널 체이닝을 사용한 프로퍼티 접근)

Optional Chaining as an Alternative to Forced Unwrapping에서 기술한 것 처럼 optional chaining을 optional 프로퍼티를 접근하는 데 사용할 수 있고, 성공 여부를 확인할 수 있다.

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이기 때문에 optional chaining의 호출도 nil을 반환한다.

또한 아래와 같이 optional chaining을 통해 프로퍼티의 값 할당을 할 수 있다.

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

위 예제의 경우 john 인스턴스의 residence 프로퍼티가 nil이기 때문에 값 할당은 실패한다.

할당은 왼쪽항이 nil이면 오른쪽 항이 실행되지 않는다. 즉 예를 들면 a=b에서 b의 값을 a 프로퍼티에 할당해야 하는데 a자체가 nil이기 때문에 값을 할당할 수 없으므로 오른쪽 항 자체가 실행이 되지 않는다 이를 눈으로 직접 확인하기 위해 다음 함수를 구현하고 실험해 본다.

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

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

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

콘솔 로그에 아무것도 출력되지 않는 것을 통해 createAddress() 함수가 호출되지 않는 것을 확인할 수 있다.


Calling Methods Through Optional Chaining (옵셔널 체이닝을 이용한 메소드 호출)

optional value에서 optional chaining을 이용하여 메소드를 호출할 수 있고, 호출 성공 여부를 확인할 수 있다. 반환 타입이 정의되어 있지 않은 메소드도 호출할 수 있다.

Residence 클래스의 printNumberOfRooms() 메소드는 numberOfRooms값을 출력한다.

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

위 코드에서 메소드는 특정한 반환 타입을 명시하지 않았다. 그러나 이 메소드는 Void 타입을 암시적으로 반환한다. 즉 빈 튜플을 반환하는 것이다. Functions Without Return Values 참조

위 예제에서 optional chaining을 이용하여 optional value에서 메소드를 호출하면, optional chaining을 이용한 호출은 항상 optional 타입을 반환하기 때문에 Void가 아닌 Void? 타입을 반환한다. 이는 if문을 사용하여 nil과의 비교 연산자를 통해 메소드 호출 여부를 확인할 수 있게 한다. 아래 예제를 살펴보자.

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

optional chaining을 이용한 프로퍼티에 값 할당도 마찬가지이다. optional chaining을 이용한 프로퍼티에 값 할당은 Void? 타입을 반환하기 때문에 if 문을 사용하여 할당 성공 여부를 확인할 수 있다.

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 (서브스크립트를 이용한 옵셔널 체이닝)

optional value에서 optional chaining을 이용하여 서브스크립트로부터 값을 할당 및 검색할 수 있고, 호출 성공 여부를 확인할 수 있다.

NOTE
optional chaining을 이용하여 optional value에 서브스크라입트로 접근할 때 물음표를 "[ ]" 앞에 작성해야 한다.

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가 optional value를 가지고 있기 때문에 바로 뒤에 물음표를 사용하였다.

유사하게 optional chaining와 서브스크립트를 이용하여 새로운 값을 할당할 수 있다.

john.residence?[0] = Room(name: "Bathroom")

위 코드의 할당 또한 john.residence 프로퍼티가 nil이므로 실패한다. john.residence 프로퍼티에 새로운 인스턴스를 만들어 할당하고, rooms 배열에 몇 개의 Room 인스턴스를 추가하면, optional chainging을 통해 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 (옵셔널 타입의 서브스크립트 접근)

만약 Swift의 딕셔너리 타입의 key 서브스크립트와 같이 서브스크립트가 optional 타입을 반환한다면, "[]" 뒤에 물음표를 작성한다.

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]

위 예에서 딕셔너리를 선언하였다. 딕셔너리의 key를 이용한 서브스크립트 접근은 key가 존재하지 않을 수 있기 때문에 optional 타입을 반환한다. 그렇기 때문에 딕셔너리에 키가 존재하는 "Dave" 와 "Bev"의 새로운 값 할당은 성공하나, "Brian"은 딕셔너리에 키가 없으므로 nil을 반환하여 결국 할당에 실패한다.


Linking Mutiple Levels of Chaining (체이닝의 다중 레벨 연결)

여러 level에 걸쳐 optional chaining을 연결하여 더 깊은 프로퍼티, 메소드 그리고 서브스크립트에 접근할 수 있다. 그러나 여러 level에 걸친 optional chaining은 optionality를 더 추가하지 않는다.(옵셔널 타입을 한 번더 wrapping 하지 않는다고 생각)

이를 다음과 같이 정리한다.

  • optional chaining의 멀티 레벨에서 상위 레벨의 타입이 optional이 아닌 경우 하위 level의 타입은 optional이 된다. (optional chaining을 사용하기 때문)

  • 상위 레벨의 타입이 이미 optional인 경우 더 optional 되지 않는다. (wrapping 하지 않는다.)

이를 실제 적용시켜 보면

  • optional chaining을 통해 Int 값을 검색하는 경우, 아무리 많은 레벨로 연결되어 있더라도 Int?을 반환한다.

  • 이와 유사하게, optional chaining을 통해 Int? 값을 검색하는 경우, 아무리 많은 레벨로 연결되어 있더라도 Int? 을 반환한다.

아래 예제는 residence 프로퍼티의 address 프로퍼티의 street 프로퍼티에 접근하는 예제이다. 이는 2레벨의 optional chaining이 residence 프로퍼티와 address 프로퍼티에 의해 연결되어있다.

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

join.residence는 값을 갖고 있지만 john.residence.address가 nil이기 때문에 이 호출은 실패한다. 그리고 street의 타입이 String? 이기 때문에 optional chaining이 2단계로 연결되어도 String? 이 반환된다.

만약 Adress의 인스턴스에 속하는 프로퍼티에 값을 할당하고, 이를 john.residence.address에 할당한다면 다중 레벨 optional chaining으로 값 접근이 가능하다.

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

Chaining on Methods with Optional Return Values (옵셔널 값을 반환하는 메소드 체이닝)

이전 예제는 optional chaining을 이용하여 optioanl 타입의 프로퍼티 값을 어떻게 검색하는지 설명하였다. optioan 타입을 반환하는 메소드 호출에 optional chaining을 사용할 수 있고, 필요한 경우 메소드의 반환 값을 chain(연결) 할 수 있다.

아래 예제는 optional chaining을 이용하여 Address 클래스의 buildingIdentifier() 메소드를 호출한다. 이 메소드의 반환 타입은 String?이다. 위에서 기술한 것처럼 optional chaing이 메소드를 호출한 후에 궁극적인 반환 타입은 String?이다.

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."

이 메소드의 반환 값에서 추가적으로 optional chaining을 수행하고 싶으면, 메소드의 괄호 뒤에 물음표를 추가 작성한다.

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

NOTE
위 예제에서 optional chaing은 메소드 자체에 사용되는 것이 아닌 메소드가 반환하는 값에 사용되기 때문에 메소드의 괄호 뒤에 물음표를 작성한다.

0개의 댓글