런타임 에러는 프로그램이 실행되는 동안 발생합니다. 보통 발생하면 프로그램이 강제 종료됩니다.
먼저, 에러 형식에 대해서 알아보겠습니다.
// 에러를 전달하는 것을 던진다고 표현합니다, 문법은 다음과 같습니다.
// throws 는 에러를 던질 수 있음을 의미합니다. throw는 에러를 던지는 키워드이므로 잘 구분해야 합니다
throw expression
func name(parameters) throws -> ReturnType {
statements
}
init(parameters) throws {
statements
}
{ (parameters) throws -> ReturnType in
statements
}
// 에러는 필수로 선언해야할 속성이 없어서, 아래와 같은 방식으로 선언만 하면 된다.
enum DataParsingError: Error {
case inivalidType
case invalidField
case minssingRequiredField(String)
}
func parsing(data: [String: Any]) throws {
guard let _ = data["name"] else {
throw DataParsingError.missingRequiredField("name")
}
guard let _ = data["age"] as? Int else {
throw DataParsingError.invalidType
}
}
에러를 리턴하는 함수를 호출할 때는 try 표현식을 사용합니다.
// 문법
try expression
try? expression
try! expression
try? parsing(data: [:]) // 처음 guard문에서 else가 실행되고 nil로 반환후 종료함
에러를 처리하는 방식은 크게 3가지 입니다.
// 문법
do {
try expression
statements
} catch pattern {
statements
} catch pattern where condition {
statements
}
// 위의 예제
// 캐치 블럭을 작성할 때에는 가장 까다로운 에러 케이스 부터 작성해야 한다.
do {
try parsing(data: [:])
} catch DataParsingError.invalidType {
print("Invalid Type Error")
} catch {
pritn("handl error")
}
기본적으로, do-catch문은 do 블럭에서 생길 수 있는 에러를 전부 처리해야만 합니다. 그게 불가능 하다면 다른 코드에 에러를 던지도록 해야 합니다. 따라서 함수 내부에 있는 do-catch문은 해당 에러를 다 처리하는 코드를 작성해야 합니다.
패턴이 없는 catch 블럭에는 error
라는 로컬 상수가 제공됩니다. Error 프로토콜이므로, 사용시 타입 캐스팅이 필요합니다.
func handleError() throws {
do {
try parsing(data: [:])
} catch {
if let error = error as? DataParsingError {
switch error {
case .invalidType:
print("Invalid type")
default:
print("handle error")
}
}
}
}
하나의 catch블록에서 두 개 이상의 에러를 매칭 시킬 수 있습니다.
이 기능이 없을 떄에는 아래와 같이 해결하였습니다.
do {
try parsing(data: [:])
// err에 형식을 바인딩하여 여러 케이스를 살펴봄
} catch let err as DataParsingError {
switch err {
case .invalidType, .invalidField:
// error handling
break
default:
break
}
}
// 새로운 기능이 나온 후에는 콤마로 구분하면 끝입니다.
do {
try parsing(data: [:])
} catch DataParsingError.invalidType, DataParsingError.invalidField {
// error handling
}
defer문은 코드의 실행을 스코프가 종료되는 시점으로 연기시킵니다. 주로 코드에서 사용했던 자원을 정리할 때 사용합니다.
// 문법
defer {
statements
}
// 파라미터로 전달된 경로에서 해당되는 파일을 읽어서 작업 후, 닫는 함수
func processFile(path: String) {
let file = FileHandle(forReadingAtPath: path)
// 아래와 같이 defer문을 사용하지 않는다면
// 파일이 갑자기 닫힐 가능성이 있어서 위험합니다.
// if path.hasSuffix(".jpg") {
// return
// }
// file?.closeFile()
defer {
file?.closefile()
}
if path.hasSuffix(".jpg") {
return
}
}
기존 스위프트의 에러를 처리하는 방식의 코드에는 throws 키워드로 에러를 던지는 것을 알수는 있지만 에러의 형식을 특정하지는 못합니다. 하나의 블럭에서 다양한 형식의 에러를 던질 수 있고, 코드 블럭을 호출하는 부분에서는 어떤 형식이 전달되는지 문법적으로 파악하지 못합니다.
catch 블럭으로 전달되는 시점에는, 에러의 프로토콜 형식이 전달됩니다. 여기서 문제가 생깁니다. 에러를 처리하기 위해서는 함수가 전달하는 에러의 형식을 알고 있어야 합니다.
// 기존 에러를 처리하는 코드
func process(oddNumber: Int) throws -> Int {
guard oddNumber >= 0 else {
throw NumberError.negativeNumber
}
guard !oddNumber.isMultipple(of: 2) else {
throw NumberError.evenNumber
}
}
do {
let result = try process(oddNumber: 1)
print(result)
}// 에러를 던질 때 에러의 형식을 알아야 하므로, 타입 캐스팅이 필요하다.
catch let myErr as NumberError {
switch myErr {
case .negativeNumber:
print("negative number")
case .evenNumber:
print("even number")
}
} catch {
print(error.localizedDescription)
} // 에러가 도중에 추가되는 경우 여기서 처릭된다. 이는
// 에러를 올바르게 처리했다고 볼 수 없다.
이러한 방법을 해결하기 위해 제네릭 열거형인 Result
타입을 사용합니다.
// Result 타입을 사용한 에러처리, 이는 throwing 클로저로 초기화 하는 생성자를 제공합니다
let result = Result { try process(oddNumber: 1) }
switch result {
case .success(let data):
print(data)
case .failure(let error):
print(error.localizedDescription)
}
// 성공시의 리턴형식, 실패시의 리턴형식을 명시해줘야한다.
// 이 타입으로 에러를 처리할 때는 함수에서 에러를 던지지 않고, 연관값으로 저장해서 리턴합니다.
func processResult(oddNumber: Int) -> Result<Int, NubmerError> {
guard oddNumber >= 0 else {
return .failure(.negativeNubmer)
}
guard !oddNumber.isMultipple(of: 2) else {
return .failure(.evenNumber)
}
return .success(oddNubmer * 2)
}
성공과 실패를 열거형 으로 처리하고, 에러는 실제로 결과를 사용하는 시점에서 처리됩니다.
에러를 처리하는 시점이 함수를 호출하는 시점에서, 작업 결과를 사용하는 시점으로 이동한 패턴을 Delayed Error Handling
라고 부릅니다.
컴파일 때 에러 형식을 명확히 인식할 수 있다는 것은 형식 안정성이 보장이 된다는 의미이다. 타입 캐스팅 없이 에러를 처리할 수 있고, 형식 추론을 통해서 코드가 더 단순해집니다.
4가지 고차함수를 제공합니다.
func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
guard data.isMultiple(of: 2) else {
return .failure(MyError.error)
}
return .success(data)
}
let a = doSomethingWithResult(data: 0)
let b = a.map { $0.isMultiple(of:2) ? "even nubmer" : "odd number" }
// faltMap은 Result 인스턴스로 리턴한다.
let c = a.flatMap {$0.isMultiple(of:2) ? .success("even nubmer") : .success("odd number")}
// 아래 코드는 실패값을 반환하는 차이를 빼면 위의 메소드와 차이가 없다.
a.mapError(transform: (MyError) -> Error)
a.flatMapError(transform: (MyError) -> Result<Int, Error>)
코드가 실행 중인 상태에서 입력된 값의 무결성을 확인하거나, 실행 결과를 검증할 때 사용하는 디버깅 도구입니다. 특정 조건이 true이면 진행하고 false이면 중지하고 메시지 출력 후 해당 코드로 이동합니다
assert
는 디버깅 모드에서만 사용되므로, 앱을 출시할 때 코드에 포함되어 있어도 상관이 없다
릴리즈 모드에서 assert를 활용하고 싶다면, precondition
함수를 사용하면 된다. 하지만 이는 종료만 시키고, 디버그 정보를 포함하지 않는다. 이를 사용하는 경우는, 특정 조건을 만족시키지 않을 떄 종료를 시키는 경우가 생긴다면 그 때 활용한다. 예를 들면, 비밀번호를 너무많이 틀린다거나 등등?
// 문법
assert(number > 0, "negative number or zero not allowed")
precondition(number > 0, "negative number or zero not allowed")