[Swift] 16. Optional Chaining

도윤·2021년 7월 29일
0

Swift

목록 보기
9/21

Optional Chainging은 nil일 수 있는 옵셔널 타입의 프로퍼티,메서드,서브스크립트를 호출하는 프로세스이다. 만약 옵셔널이 값을 가지고 있다면 호출에 성공하지만 nil이라면 nil을 반환한다. 여러 개의 쿼리를 함께 연결할 수 있으며 전체 체인 중 하나라도 nil이라면 실패하게 된다.


Optional Chaining as an Alternative to Forced Unwrapping

옵셔널 타입의 프로퍼티,메서드,서브 스크립트 값이 nil이 아니라면 이름 뒤에 ?를 배치하여 옵셔널 체이닝을 할 수 있다. 강제로 언래핑하기 위해 !를 붙ㅇ텨주는 것과 비슷하다. 차이점은 nil일 때 옵셔널 체이닝은 실패하지만 강제 언래핑은 에러가 발생한다.

옵셔널 체이닝이 nil을 호출할 수 있어서 return값이 옵셔널 값이 아니더라도 옵셔널 체이닝의 결과는 항상 옵셔널 값이다. 옵셔널 체이닝 호출이 성공인지 실패인지를 옵셔널 반환값으로 확인할 수 있다.

특히, 옵셔널 체이닝 호출의 결과는 예상한 반환값과 동일한 타입이지만 옵셔널로 래핑된 상태로 나타난다. 일반적으로 Int를 반환하는 프로펕티가 옵셔널 체이닝을 통해 접근한다면 Int?로 반환된다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}
let john = Person()
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

Person클래스의 프로퍼티는 default값이 선언되지 않은 옵셔널 타입이므로 nil값이 할당되고, 위와 같이 강제 언래핑을 한다면 런타임에러가 발생한다. 만약 John의 resident 프로퍼티에 적절한 값을 할당 시킨 뒤 다시 실행한다면 오류가 발생하지는 않는다.

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.

만약 residence가 존재한다면 numbersOfRooms 값을 반환하고 그렇지 않다면 실패 메세지가 호출된다.
만약 값이 존재하여 roomCount에 할당되더라도 Int형이 아닌 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).

Defining Model Classes for Optional Chaining

한 레벨 이상의 깊이에 존재하는 메서드,프로퍼티,서브 스크립트에도 옵셔널 체이닝을 사용할 수 있다. 복잡한 구조를 가진 모델에서도 하위 프로퍼티들에 접근해서 값이 있는지 확인할 수 있다. 이때 체인 중 하나라도 nil을 반환하면 실패하게 된다.

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

Address는 String?타입의 프로퍼티를 가지고 있다. buildingIdentifier() 함수를 호출하면 둘다 반환하며 없다면 nil을 반환한다.


Accessing Properties Through Optional Chaning

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 클래스의 resident 프로퍼티는 옵셔널 타입이지만 값이 할당되지 않아서 nil값이 반환된다.

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

여전히 residence는 nil이어서 address를 할당하려는 시도는 실패할 것이다.

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

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

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

아래에서 함수가 정상정으로 호출된다면 print가 나와야하는데 나오지 않으므로 실패한 것임을 알 수 있다.


Calling Methods Through Optional Chaining

메소드 호출이 정상적으로 됐는지 확인하거나 옵셔널 값에 대한 메서드를 호출하기 위해 옵셔널 체인을 사용할 수 있다. 심지어 메서드가 return value를 정의하지 않아도 가능하다.

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

만약 옵셔널 체이닝으로 이 함수를 호출하게 된다면 형태는 Void?가 될것이다. 옵셔널 체이닝으로 호출하면 return value는 항상 optional type이다. 이렇게 메서드가 반환값이 없더라도 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.

위와 같이 사용하면 된다.


Accessing Subscripts Through Optional Chaining

옵셔널 체이닝을 통해 옵셔널 값의 서브스크립트를 검색하고 할당하여 해당 서브스크립트가 성공적으로 호출됐는지 확인할 수 있다. 옵셔널 체이닝을 통해 서브스크립트에 접근할 땐 ?는 대괄호 앞에 붙는다.

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.

Residence 클래스에 정의된 서브 스크립트를 사용해서 john의 residence 프로퍼티의 rooms Array에서 첫 번째 방의 이름을 검색하려고 한다. 하지만 현재 john의 residence 프로퍼티는 nil 이므로 서브스크립트 호출이 실패한다. 여기서 볼 수 있는 것은 ?의 위치이다. 서브스크립트 대괄호 앞에 ?가 위치한 것을 볼 수 있다.

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

위 처럼 옵셔널 체이닝과 서브스크립트를 통해 새로운 값을 할당할 수 있다.

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.

위 코드처럼 residence 인스턴스를 할당하고 optional chaining을 통해 아이템에 접근할 수 있다.

Accessing Subsript of Optional Chaining

서브스크립트가 딕셔너리 타입같은 옵셔널 타입을 반환하는 경우 ?마크를 []뒤에 둔다

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]

2개의 key-value가 있는데 dictionary는 return type이 optional 타입이므로 ?를 붙여 인덱스에 접근하는 것을 볼 수 있다. 만약 key가 존재하지 않더라도 에러가 아닌 nil값만 반환한다.


Linking Multiple Levels of Chaining

multiple level의 옵셔널 체이닝을 함께 연걸하여 모델 내에서 프로퍼티,메서드,서브 스크립트로 접근할 수 있다.
하지만 여러 수준의 옵셔널 체이닝은 반환된 값에 더 많은 수준의 옵셔널을 추가하지 않는다.

  • 검색하려는 타입이 옵셔널이 아니라면 옵셔널 체이닝때문에 옵셔널이 된다.
  • 검색하려는 타입이 옵셔널이라면 옵셔널 체이닝때문에 더이상 옵셔널이 되지 않는다.

따라서

  • 옵셔널 체이닝을 통해 Int 값을 얻고자 한다면, 항상 Int?가 반환될 것이다.
  • 옵셔널 체이닝을 통해 Int? 값을 검색하려고 한다면, 항상 Int?가 반환된다.
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.

john의 residence는 올바른 residence가 할당되어 nil이 아니지만 address가 nil이므로 실패하게 된다.

street의 타입은 String?이다. 만약 address 인스턴스가 할당되고 street이 할당된다면 여러개의 optional chaning을 값을 얻을 수 있다.

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

이전의 예는 multiple optional chaining을 통해 옵셔널 프로퍼티의 값을 얻는 방법을 확인했다. 옵셔널 체이닝을 이용하여 옵셔널 ㅌ타입의 값을 반환하는 메소드를 호출할 때도 사용할 수 있다.

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

위의 코드는 String?을 반환하는 buildingIdentifier() 메서드를 옵셔널 체이닝을 통해 호출한 것이다. 물론 옵셔널 체이닝을 사용하여 호출해도 반환값의 타입은 Stirng?이다

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

반환값에 대하여 옵셔널 체이닝을 하고싶다면 괄호 뒤에 ?을 붙여주면 된다.

0개의 댓글