Swift - 옵셔널 체이닝

32502 단어 swiftswift

옵셔널 체이닝


옵셔널 체이닝은 nil일 수도 있는 프로퍼티, 메소드 그리고 서브스크립트에 질의를 하는 과정을 말한다. 만약 옵셔널이 프로퍼티나 메소드 혹은 서브스크립트에 대한 값을 갖고 있다면 그 값을 반환하고 만약 값이 nil이면 nil을 반환한다. 여러 질의를 연결해서 할 수도 있는데, 연결된 질의에서 어느 하나라도 nil이면 전체 결과는 nil이 된다.


강제 언래핑의 대체로써의 옵셔널 체이닝

옵셔널 체이닝은 옵셔널 값 뒤에 물음표(?)를 붙여서 표현 가능하다. 옵셔널을 사용할 수 있는 값에는 프로퍼티, 메소드 그리고 서브스크립트가 포함된다. 옵셔널 값을 강제 언래핑 하기 위해서 뒤에 느낌표(!)를 붙이는 것과 문법이 비슷한데, 가장 큰 차이는 강제 언래핑을 했는데 만약 그 값이 없으면 런타임 에러가 발생하지만, 옵셔널 체이닝을 사용하면 런타임 에러 대신 nil이 반환 된다는 점이다.

옵셔널 체이닝에 의해 nil 값이 호출 될 수 있기 때문에 옵셔널 체이닝의 값은 항상 옵셔널 값이 된다. 옵셔널 값을 반환하지 않는 프로퍼티, 메소드 혹은 서브스크립트를 호출하더라도 옵셔널 체이닝에 의해 옵셔널 값이 반환된다. 이 옵셔널 리턴 값을 이용해 옵셔널 체이닝이 성공적을 실행 됐는지 아니면 nil을 반환 했는지 확인할 수 있다.

보다 구체적으로 옵셔널 체이닝에 의해 호출되면 반환 값과 같은 타입에 옵셔널이 붙어 반환 된다. 예를 들어, Int를 반환하는 메소드 경우 옵셔널 체이닝이 성공적으로 실행되면 Int?를 반환한다.

아래 코드는 옵셔널 체이닝이 강제 언래핑과 어떻게 다른지 보여준다.

class Person {
	var residence: Residence?
}

class Residence {
	var numberOfRooms = 1
}

Residence 인스턴스는 numberOfRooms 이라는 Int 프로퍼티 하나를 소유하고 있고 Person 인스턴스는 옵셔널 residence 프로퍼티를 Residence? 로 소유합니다. 만약 Person 이라는 인스턴스를 생성하면 residence 프로퍼티는 옵셔널의 기본값인 nil로 초기화 됩니다. 아래 코드에서 johnresidence 프로퍼티가 nil인 값을 소유합니다.

let john = Person()

만약 이 Person 클래스의 residencenumberOfRooms 프로퍼티에 접근하기 위해 느낌표(!)를 이용해 강제 언래핑을 한다면 residence 가 언래핑할 값이 없기 때문에 런타임에러가 발생한다.

let roomCount = john.residence!.numberOfRooms
// 런타임에러 발생!

numberOfRooms 의 값에 접근하기 위해 강제 언래핑 대신 옵셔널 체이닝을 사용할 수 있다. 옵셔널 체이닝을 사용하기 위해 느낌표 대신 물음표를 사용한다.

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// "Unable to retrieve the number of rooms." 출력

옵셔널 체이닝의 체인이라는 단어처럼 이 residence 프로퍼티들은 묶여 있다. 그래서 위 코드에서 numberOfRooms 프로퍼티는 옵셔널이 아니지만 이 값에 접근하기 위해 사용했던 인스턴스의 프로퍼티 residence 가 옵셔널이기 때문에 최종값은 옵셔널 값이 된다.
nil이었던 residence 값에 Residence 인스턴스를 생성해 추가할 수 있다.

let john.residence = Residence()

john.residence 는 이제 nil이 아니라 Residence 인스턴스를 갖는다. 그래서 옵셔널 체이닝으로 numberOfRooms 값에 접근 했을 때 nil 대신 Int? 값인 1을 리턴한다.

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// "John's residence has 1 room(s)." 출력

옵셔널 체이닝을 위한 모델 클래스 정의

옵셔널 체이닝을 프로퍼티. 메소드 그리고 서브스크립트에서 사용할 수 있는데, 한 단계가 아닌 여러 level로 사용할 수 있다.

아래 코드는 위의 Person , Residence 모델을 확장해 Room , Address 클래스를 추가한 4가지 모델을 정의한다. Person 클래스는 전과 같다.

class Person {
	var residence: Residence?
}

Residence 는 전보다 더 복잡해졌다. 이번에 Residencerooms 라는 프로퍼티를 소유한다. 이 프로퍼티는 [Room] 타입의 빈 배열로 초기화 된다.

class Residecne {
	var rooms = [Room]()
    var numverOfRooms: Int {
    	return rooms.count
    }
    subscript(i: Int) {
    	get {
        	return rooms[i]
        }
        set {
        	rooms[i] = newValue
        }
    }
    func printNumberOfRooms() {
    	print("The number of rooms is \(numberOfRooms)
    }
    var address: Address?
}

이번 버전의 Residence 클래스가 Room 인스턴스의 배열을 소유하고 있기 때문에 numberOfRooms 프로퍼티는 계산된 프로퍼티로 선언된다. rooms 배열에 접근하기 위한 단축으로 서브스크립트를 선언한다.

rooms 배열의 Room 클래스는 이름 하나를 초기화할 때 인자로 받는 간단한 클래스이다.

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

마지막 클래스는 Address 이다. 이 클래스는 3개의 String? 옵셔널 프로퍼티를 갖는다. 처음 2개는 buildingName , buildingNumber 이고 빌딩 주소를 구분하는 대체 수단이다.
마지막 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
        }
    }
}

Address 클래스는 buildingIdentifier() 라는 String? 타입의 메소드를 지원한다. 이 메소드는 buildingNumberstreet 를 확인해 값이 있으면 buildingNumber 와 결합된 street 값을 반환하고 값이 없는 경우 nil을 반환한다.


옵셔널 체이닝을 통한 프로퍼티의 접근

옵셔널 체이닝을 이용해 프로퍼티에 접근 할 수 있다.

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

위 코드의 경우 residence? 가 nil이기 때문에 옵셔널 체이닝 결과 nil을 호출한다. 옵셔널 체이닝을 사용해 값을 할당할 수 있다.

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

위 예제에서는 Address() 인스턴스를 생성해 john.residenceaddress 로 할당하는 코드이다. john.residence? 가 nil이기 때문에 address 할당은 실패하고 할당도 할당받는 왼쪽 항이 nil이면 아예 오른쪽 항이 실행되지 않는다.


옵셔널 체이닝을 통한 메소드 호출

옵셔널 체이닝을 이용해 메소드를 호출 할 수 있다.

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

위 메소드는 리턴값이 명시되지 않았다. 하지만 함수나 메소드는 리턴값이 없는 경우 암시적으로 Void라는 값을 갖는다. 그래서 이 메소드가 옵셔널 체이닝에서 호출되면 반환값은 Void가 아니라 Void? 타입이 반환된다.

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

위 코드는 john.residence?.printNumberOfRooms() 메소드 호출의 결과가 nil인지 아닌지 비교를 하고 그 결과에 대한 처리를 한다. 위에서 언급했던 것처럼 printNumberOfRooms() 에는 직접적인반환값이 명시돼 있지 않지만 암시적으로 Void를 반환하고 이 메소드 호출이 옵셔널 체이닝에서 이루어 지기 때문에 Void? 가 반환되서 nil 비교를 할 수 있다.

옵셔널 체이닝을 통해 값을 할당하면 Void? 값을 반환한다. 그래서 이 값이 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.")
}
// "It was not possible to set the address." 출력

옵셔널 체이닝을 통한 서브스크립트 접근

옵셔널 체이닝을 이용해 옵셔널 값을 서브스크립트로 접근할 수 있다.

아래 예제는 서브스크립트를 이용해 rooms 에서 첫 roomsname 을 요청하는 코드이다.

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") 이 코드는 0번째 index room 값을 할당하는 코드인데 residence 가 현재 nil이기 때문에 room 을 할당하지 못하고 fail이 발생한다.

아래 예제와 같이 Residence 인스턴스를 할당하면 residence 서브스크립트를 사용할 수 있다.

let johnHouse = Residence()
johnHouse.rooms.append(Room(name: "Living Room"))
johnHouse.rooms.append(Room(name: "Kitchen"))
john.residence = john.House

if let firstRoomName = john.residence?[0].name {
	print("The first room name is \(firstRoomName).")
} else {
	print("TUnable to retrieve the first room name.")
}
// "The first room name is Living Room" 출력

옵셔널 타입의 서브스크립트 접근

만약 Swift의 Dictionary 타입 같이 서브스크립트의 결과로 옵셔널을 반환한다면 그 뒤에 물음표를 붙여준다. Dictionary 타입은 key-value로 동작해서 항상 존재한다는 보장이 없기 때문에 옵셔널 값을 반환한다.

var testScore = ["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]

위 예제는 testScores 라는 특정 key에 대한 값을 get 혹은 set하는 코드이다. Dave , Bev 와 관련한 값은 처리가 되고 Brian 은 등록된 key가 없기 때문에 관련한 아무런 일도 처리도 일어나지 않는다.


체이닝의 다중 레벨 연결

옵셔널 체이닝이 여러 단계에 걸쳐 연결 될 수 있다. 상위 값이 옵셔널 값인 경우 하위 레벨도 옵셔널 값이라고 해서 더 옵셔널 해지진 않는다.

  • 옵셔널 체이닝의 상위 레벨 값이 옵셔널인 경우 현재 값이 옵셔널이 아니더라도 그 값은 옵셔널 값이 된다.

  • 옵셔널 체이닝의 상위 레벨 값이 옵셔널이고 현재 값이 옵셔널이라고 해서 더 옵셔널하게 되진 않는다.

  • 옵셔널 체이닝을 통해 값을 검색하거나 메소드를 호출하면 몇 단계를 거치는지 상관없이 옵셔널을 반환한다.

if let johnStreet = john.residence?.address?.street {
	print("John's street name is \(johnStreet).")
} else {
	print("Unable to retrieve the address.")
}
// "Unable to retrieve the address." 출력

john.residence 는 valid한 Residence 인스턴스를 갖고 있지만 address 가 nil이어서 이 옵셔널 체이닝은 fail이 된다. 비록 상위의 옵셔널 값인 john.residence 가 true여도 하위 값이 nil 이면 체이닝은 fail이 된다.

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.")
}
// "John's street name is Laurel Street." 출력

Residence 인스턴스와 Address 인스턴스가 모두 존재하기 때문에 옵셔널 체이닝은 success가 된다.


체이닝에서 옵셔널 값을 반환하는 메소드

아래 예제와 같이 옵셔널 체이닝에서 반환값이 있는 메소드를 호출할 수 있다.

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

만약 메소드의 반환값을 갖고 추가적인 행동을 더 하고 싶다면 메소드 호출 표현 뒤에 물음표(?)를 붙이고 그 행동을 적어주면 된다. 앞에서 언급 했던 것 처럼 옵셔널 체이닝에 물려 있기 때문에 메소드의 반환값도 옵셔널이 돼서 그 표시를 해줘야 한다.

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\".")
}
}
// "John's building identifier begins with "The"." 출력

좋은 웹페이지 즐겨찾기