Error handling : 프로그램에서 error 에 대응하고 recovering 하는 것.
Swift 에서 에러는 Error 프로토콜을 conform 하는 type 에 의해 표현됨. 빈 Error 프로토콜을 통해 에러 핸들링에 사용될 수 있는 타입을 나타냄.
주로 Swift enum 은 관련된 에러 조건을 모델링하는데 적합함.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
Error 를 throw 해서 플로우 상 더 이상 진행할 수 없는 예상하지 못한 무언가가 발생했음을 나타냄. throw 문을 통해 에러를 던짐.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
에러가 던져지면, 해당 에러를 처리해야 하는 곳이 필요함.
do-catch 문함수가 에러를 던지는 경우, 프로그램의 플로우를 바꾸기 때문에 에러를 던질 수 있는 곳을 명시하는 것이 중요함. try / try? / try! 를 에러를 던질 수 있는 function/method/initializer 전에 사용
Swift 의 error handling 은 다른 언어의 exception handling 과 비슷함. 다른 언어와 다른 점은 Swift 의 error handling 은 연산이 많이 필요할 수 있는 call stack 을 unwinding 하는 작업을 포함하지 않음. 따라서 throw 문의 성능은 return 문과 거의 비슷할 정도
Function 정의 다음에 throw 키워드를 써서 function/method/initializer 가 error 를 던지는 것을 표현 가능. throws 로 마킹된 함수는 throwing function 이라고 불림. Return type 이 있는 function 은 return arrow (->) 전에 throws 키워드를 붙임
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
Throwing function 은 내부에서 호출된 scope 로 error 를 전파함
struct Item {
var price: Int
var count: Int
}
class VendingMachine {
var inventory = [
"Candy Bar": Item(price: 12, count: 7),
"Chips": Item(price: 10, count: 4),
"Pretzels": Item(price: 7, count: 11)
]
var coinsDeposited = 0
func vend(itemNamed name: String) throws {
guard let item = inventory[name] else {
throw VendingMachineError.invalidSelection
}
guard item.count > 0 else {
throw VendingMachineError.outOfStock
}
guard item.price <= coinsDeposited else {
throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
}
coinsDeposited -= item.price
var newItem = item
newItem.count -= 1
inventory[name] = newItem
print("Dispensing \(name)")
}
}
let favoriteSnacks = [
"Alice": "Chips",
"Bob": "Licorice",
"Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
let snackName = favoriteSnacks[person] ?? "Candy Bar"
// vend(itemNamed:) method 가 던진 error 는 buyFavoriteSnack() 가 호출된 곳으로 propagate 됨
try vendingMachine.vend(itemNamed: snackName)
}
// throwing initializers
struct PurchasedSnack {
let name: String
init(name: String, vendingMachine: VendingMachine) throws {
try vendingMachine.vend(itemNamed: name)
self.name = name
}
}
do-catch 문을 사용해서 에러를 handling 할 수 있음
do {
try <#expression#>
<#statements#>
} catch <#pattern 1#> {
<#statements#>
} catch <#pattern 2#> where <#condition#> {
<#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
<#statements#>
} catch {
<#statements#>
}
catch 문은 가능한 모든 에러를 핸들링해야 할 필요는 없음. 만약 매칭되는 catch 가 없다면 상위 scope 로 propagate. 에러가 handling 되지 않고 top-level scope 로 propagate 됐다면 runtime error 발생
func nourish(with item: String) throws {
do {
try vendingMachine.vend(itemNamed: item)
} catch is VendingMachineError {
print("Couldn't buy that from the vending machine.")
}
// catch 안 된 에러는 어디선가 handling 되어야 함
}
do {
try nourish(with: "Beet-Flavored Chips")
} catch {
print("Unexpected non-vending-machine-related error: \(error)")
}
// Prints "Couldn't buy that from the vending machine."
try? 를 써서 optional 값으로 바꿔서 error 를 handling 하는 방법도 있음. 만약 에러가 try? 문에서 던져졌다면 nil 값이 리턴됨.
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
// x 와 같은 동작
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
Throwing function 이 error 를 runtime 에 던지지 않을 것이라는 것이 확실한 경우도 있음. 이런 경우에는 try! 를 통해 호출을 runtime assertion 에 감싸서 에러가 throw 되지 않을 것임을 나타내는 방법도 있음.
// 항상 error 를 던지지 않을 것임을 알 때 사용
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
위의 예제들에서 throw 된 에러들은 Error 프로토콜을 conform 하는 어떤 타입이든 될 수 있음. 이런 접근 방식은 코드가 실행되고 있을 때, 특히 error 가 다른 어딘가에서 propagate 됐을 때 모든 에러를 알 수 없는 실제 상황을 반영함. 또한 에러가 시간에 따라 달라질 수 있음을 반영하기도 함. (e.g. 의존하고 있는 라이브러리의 버전이 업그레이드 돼서 새로운 에러가 throw 될 수 있음)
대부분의 Swift code 는 던지는 에러의 타입을 명시하지 않음. 하지만 특별한 경우에 throw 되는 에러가 특정 타입을 따르도록 제약을 추가할 수 있음.
Typed throw 문법 : 에러의 타입을 throw 뒷부분에 명시
enum StatisticsError: Error {
case noRatings
case invalidRating(Int)
}
// 오직 `StatisticsError` 만 throw 할 것임을 명시
func summarize(_ ratings: [Int]) throws(StatisticsError) {
guard !ratings.isEmpty else { throw .noRatings }
var counts = [1: 0, 2: 0, 3: 0]
for rating in ratings {
guard rating > 0 && rating <= 3 else { throw .invalidRating(rating) }
counts[rating]! += 1
}
print("*", counts[1]!, "-- **", counts[2]!, "-- ***", counts[3]!)
}
Never 로 절대로 return 하지 않는 함수를 작성할 수 있음. 아래 함수는 Never 타입의 값을 생성하는 것이 불가능하기 때문에 throw 를 할 수 없음
func nonThrowingFunction() throws(Never) {
// ...
}
함수의 error type 을 명시하는 것에 추가로 do-catch 문에도 특정 error type 을 쓸 수 있음
let ratings = []
do throws(StatisticsError) { // 오직 StatisticsError 만 throw 할 것임을 명시함
try summarize(ratings)
} catch {
switch error {
case .noRatings:
print("No ratings available")
case .invalidRating(let rating):
print("Invalid rating: \(rating)")
}
}
// Prints "No ratings available".
// 이미 특정 타입의 에러만 throw 하고 있다면 typed throw 를 추론하고, 더 간결하게 작성 가능
let ratings = []
do {
try summarize(ratings)
} catch {
switch error {
case .noRatings:
print("No ratings available")
case .invalidRating(let rating):
print("Invalid rating: \(rating)")
}
}
// Prints "No ratings available".
현재 코드 block 실행이 끝나기 전에 실행할 코드들을 defer 문을 사용해서 실행함. defer 는 코드가 어떤 방식으로 (error thrown/return/break) 끝나든 항상 실행해야 하는 cleanup 코드를 실행할 수 있게 해줌.
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// Work with the file.
}
// close(file) is called here, at the end of the scope.
}
}