많은 언어들에서 런타임에 발생하는 오류를 처리하는 기능을 제공한다.
에러 처리 없이 런타임에 오류가 발생하면 프로그램은 바로 멈추게 되기 때문에 에러 처리를 통해서 특정에러를 처리하고 에러 하나로 프로그램이 종료되지 않도록 하기 위해서 사용하게 된다.
에러는 열거형이다.
또한 swift에서 에러는 Error프로토콜을 따르는 열거형으로 선언해야 한다.
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
↳ 다음과 같은 형태로 에러를 처리한다.
throw VendingMachineError.insufficientFunds(coinsNeeded: 5)
↳ throw 키워드를 이용해서 임의로 에러를 발생시키는것도 가능하다.
앞서 말했던 것처럼 그냥 에러만 발생해서는 프로그램의 동작에 큰 문제를 불러일으킨다 에러처리의 목적이 특정부분의 에러를 처리하는 과정을 설계하는것이다.
에러를 처리하는 방법에는 4가지가 있다.
swift에서 에러를 처리할 부분이 있다는것을 미리 명시하는 부분이 있을때 함수의 경우 반환타입을 명시하는 -> 앞부분에 throws를 붙이는것으로 명시한다, 이러한 함수 형태를 Throwing Function이라고 한다(애플 문서 명칭)
//에러 열거형
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
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)")
}
}
(앞서 생성했던 VendingMachineError를 기반으로 에러를 처리하는 클래스)
함수의 -> 앞부분에 throws 키워드를 붙이는것으로 Throwing Function을 선언할 수 있다.
함수 내부에서 throw키워드를 이용해서 Error프로토콜을 따르는 열거형 타입의 에러를 발생시킬 수 있다.
do {
try <#expression#>
<#statements#>
} catch <#pattern 1#> {
<#statements#>
} catch <#pattern 2#> where <#condition#> {
<#statements#>
} catch <#pattern 3#>, <#pattern 4#> where <#condition#> {
<#statements#>
} catch {
<#statements#>
}
do안에 오류가 발생할 가능성이 있는 코드를 포함한 일정단위의 코드를 담고 try에 에러가 발생 할 수 있는 코드를 실행시킨다.
에러 종류별로 catch에서 처리하면된다.
//에러 열거형
enum VendingMachineError: Error {
case invalidSelection
case insufficientFunds(coinsNeeded: Int)
case outOfStock
}
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
print("Success! Yum.")
} catch VendingMachineError.invalidSelection {
print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
print("Unexpected error: \(error).")
}
// Prints "Insufficient funds. Please insert an additional 2 coins."
try에서 에러가 발생하면 그 즉시 catch로 이동한다.
catch 에서 do안에서 발생할 수 있는 모든 에러를 처리할 필요는 없다. 하지만 catch에서 에러를 처리하지 않으면 에러는 주변에게 에러처리가 가능한곳을 찾을때까지 에러를 전파하고 다닌다. 그러다 만약
에러가 처리되지 않고 범위의 최상위로 전파되면 런타임 에러가 발생하게 된다.
그래서 기본적으로는 일반 함수에서는 do-catch 구문에서 에러를 처리해야 하고 던지기 함수(throwing fucntion)에서는 do-catch 구문이나 호출자가 에러를 처리해야 한다
앞서 do -catch에서 try가 에러 가능성이 있는 코드를 직접실행하는 키워드로 보여졌는데 이렇게 try로 에러가능성있는 코드를 다루는 방법에는 2가지가 있다.
이 방법은 에러를 옵셔널로 변환시켜서 처리하는 방법이다.
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
위 코드를 실행키긴다면 x와 y의 값은 정수nil이 될것이다.
이처럼 에러를 옵셔널로 처리하면 런타입에러의 위험을 없애면서 형태도 간단하게 에러 처리가 가능해 진다.
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() { return data }
if let data = try? fetchDataFromServer() { return data }
return nil
}
try!는 에러를 전파하지 않게 만들어 준다.
전파의 개념은 위에서 잠깐 이야기 했는데 에러는 발생한 순간부터 에러를 처리하는곳을 찾을때까지 코드의 상위로 전파된다.
try!는 이러한 에러의 전파를 비활성화하는 키워드라고 생각하면된다.
try!를 사용하는 경우 에러가 발생하지 않는다는 확신이 있는경우 사용하면 된다. (만약 에러가 나게 된다면 바로 런타임 에러를 유발 할 수 있으므로 주의해서 사용할 것!)
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
앞서 에러는 Error프로토콜을 따르는 열거형이라고 했는데 열거형이나 클래스, 구조체는 하나의 타입으로 부르기도 한다고 했었다.
이와 마찬가지로 던지기함수에서 throws를 명시할때 발생할 수 있는 에러의 타입을 지정해 줄수 있다.
-> 어떤 에러들이 발생할 수 있는 함수인지 미리 명시해두는 방법이라고 생각하면된다.
//사용자 정의 에러
enum StatisticsError: Error {
case noRatings
case invalidRating(Int)
}
//에러 타입지정 던지기 함수
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]!)
}
사실 우리가 앞서 이야기한 던지기 함수도 이러한 에러 타입지정이 생략된 형태라고 할 수 있다.
func someThrowingFunction() -> throws {
let ratings = [1, 2, 3, 2, 2, 1]
try summarize(ratings)
}
func someThrowingFunction() -> throws(any Error) {
let ratings = [1, 2, 3, 2, 2, 1]
try summarize(ratings)
}
위 두 함수와 같이 던지기 함수에서 에러타입이 지정되지 않았다는 것은 에러 타입으로 (any Error)를 에러 타입으로 명시한것과 동일하다고 생각하면 된다.
에러를 던지지않는 함수? 우리가 throws를 사용하지 않는 함수가 에러를 던지지 않는 함수이다
하지만 이것도 생략된 형태였다는 사실을 알아두자 에러를 던지지 않는 함수는 Never타입으로 에러타입을 지정해 주면 된다.
func nonThrowingFunction(){
// ...
}
func nonThrowingFunction() throws(Never) {
// ...
}
위 두 함수는 문법적으로 동일한 함수임을 알 수 있다.
에러타입은 던지기함수 뿐만아니라 do-catch에서도 사용할 수 있다.
do throws(any Error){
try someThrowingFunction()
}
catch{
//error handling code here...
}