2021년 7월 3주차
: 타입이 Error
protocol을 채택하여 오류를 표현할 수 있다
- 오류의 종류를 표현하기에 적합함
- associated value를 사용하여 오류에 관한 부가 정보를 제공할 수 있음
enum ValidationError: Error {
case tooShort
case tooLong
case invalidCharacterFound(Character)
}
func validate(username: String) throws {
guard username.count > 4 else {
throw ValidationError.tooShort
}
guard username.count < 11 else {
throw ValidationError.tooLong
}
for character in username {
guard character.isLetter else {
throw ValidationError.invalidCharacterFound(character)
}
}
}
do {
try validate(username: "Lia!!!")
} catch {
print(error) // invalidCharacterFound("!")
}
Error 타입 구분하기
struct CustomErrorStruct : Error {
var message : String
}
class CustomErrorClass : Error { }
do {
let error = CustomErrorStruct(message:"some error")
throw error
} catch let error where error is CustomErrorStruct {
print("error with Struct")
} catch let error where error is CustomErrorClass {
print("eroor with Class")
} catch let error {
print("unknown error", error)
}
Xcode가 보여주는 error 와 비슷한 형태
struct XMLParsingError: Error {
enum ErrorKind {
case invalidCharacter
case mismatchedTag
case internalError
}
let line: Int
let column: Int
let kind: ErrorKind
}
func parse(_ source: String) throws -> XMLDoc {
// ...
throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
// ...
}
do {
let xmlDoc = try parse(myXMLData)
} catch let error as XMLParsingError {
print("Parsing error: \(error.kind) [\(error.line):\(error.column)]")
} catch {
print("Other error: \(error)")
}
// Prints "Parsing error: mismatchedTag [19:5]"
- Error protocol을 채택한 타입을 확장하여
LocalizedError
을 채택하면 된다 - 원하는 에러 메시지 구현 가능
참고) NSLocalizedString의 comment는 이해하기 쉽게 하기 위한 주석으로, 기능 상 이점은 크게 없다
enum ValidationError: Error {
case tooShort
case tooLong
case invalidCharacterFound(Character)
}
extension ValidationError: LocalizedError {
var errorDescription: String? {
switch self {
case .tooShort:
return NSLocalizedString(
"Under length",
comment: "Username needs to be at least 5 characters long"
)
case .tooLong:
return NSLocalizedString(
"Exceeding length",
comment: "Username can't be longer than 10 characters"
)
case .invalidCharacterFound(let character):
let format = NSLocalizedString(
"Invalid character exception : '%@' contained",
comment: "Username can't contain the character '%@'"
)
return String(format: format, String(character))
}
}
}
func validate(username: String) throws {
guard username.count > 4 else {
throw ValidationError.tooShort
}
guard username.count < 11 else {
throw ValidationError.tooLong
}
for character in username {
guard character.isLetter else {
throw ValidationError.invalidCharacterFound(character)
}
}
}
do {
try validate(username: "Lia!!!")
} catch {
print(error.localizedDescription) // Invalid character exception : '!' contained
}
- 함수에서 발생한 오류를 해당 함수를 호출한 코드에 알리는 방법
- throws 키워드를 사용해야 오류를 던질 수 있음
- 함수, 메서드, 이니셜라이져에 throws 키워드 사용 가능
- throws는 함수의 자체 타입에도 영향을 미침
- 같은 이름의 함수 ↔ 같은 이름의 throws 함수 구분
- throws 함수 → 일반 함수 재정의 ❌
- 일반 함수 → throws 함수 재정의 ⭕️
- 에러를 던질 가능성이 있다면, throws 키워드를 붙여줘야함
enum AgeError: Error {
case impossibleAge
case tooYoung
case tooOld
}
func validate(age: Int) throws {
guard age > 0 && age < 130 else {
throw AgeError.impossibleAge
}
guard age > 16 else {
throw AgeError.tooYoung
}
guard age < 60 else {
throw AgeError.tooOld
}
print("나이 \(age)세, 기준을 충족합니다")
}
try validate(age: 22) // "나이 22세, 기준을 충족합니다"
try validate(age: 7) // error!
// Fatal error: Error raised at top level: AgeError.tooYoung
- 에러 발생을 전달받으면, 에러를 처리해줌
- do 절 내부에서 코드 오류를 던지고, catch 절에서 에러를 전달받아 처리해줌
func applyForPosition(age: Int) {
do {
try validate(age: age)
} catch {
print(error)
}
}
applyForPosition(age: 22) // "나이 22세, 기준을 충족합니다"
applyForPosition(age: 7) // tooYoung
- try? 는 호출한 함수가 에러를 던질 때, 리턴값이 nil이 된다
- 에러가 아닐 경우는 그냥 optional 로 리턴
optional 값으로 에러 처리
func someThrowingFunction(shouldThrowError: Bool) throws -> Int {
if shouldThrowError {
enum SomeError: Error {
case justSomeError
}
throw SomeError.justSomeError
}
return 100
}
let x: Optional = try? someThrowingFunction(shouldThrowError: true)
print(x) // nil
let y: Optional = try? someThrowingFunction(shouldThrowError: false)
print(y) // Optional(100)
optional 반환 타입과 optional 값으로 에러 처리하는 방법의 결합
func fetchData() -> Data? {
if let data = try? fetchDataFromDisk() {
return data
}
if let data = try? fetchDataFromServer() {
return data
}
return nil
}
- try! 는 에러가 발생하지 않을 것이라 확신하고 쓰는 방법
- 에러 발생 시, 런타임 오류
- try! 는 지양하자
func someThrowingFunction(shouldThrowError: Bool) throws -> Int {
if shouldThrowError {
enum SomeError: Error {
case justSomeError
}
throw SomeError.justSomeError
}
return 100
}
let y: Optional = try! someThrowingFunction(shouldThrowError: false)
print(y) // 100
let x: Optional = try! someThrowingFunction(shouldThrowError: true)
print(x) // Fatal error: 'try!' expression unexpectedly raised an error
rethrow
키워드를 통해, 매개변수로 받은 함수가 오류를 던진다는 것을 명시- 에러를 다시 받아서 던지는지(rethrowing), 그냥 자신의 에러를 던지는지(throwing) 명시하는 효과
func someThrowingFunc() throws {
enum SomeError: Error {
case justSomeError
}
throw SomeError.justSomeError
}
func someFunction(callback: () throws -> Void) rethrows {
enum AnotherError: Error {
case justAnotherError
}
do {
try callback()
} catch {
throw AnotherError.justAnotherError
}
do {
try someThrowingFunc()
} catch {
// 에러 발생!!
throw AnotehrError.justAnotherError
}
// catch 절 외부에서 단독으로 오류 던질 수 없음
// 에러 발생!!
throw AnotherError.justAnotherError
}
protocol SomeProtocol {
func someThrowingFunctionFromProtocol(callback: () throws -> Void) throws
func someRethrowingFunctionFromProtocol(callback: () throws -> Void) rethrows
}
class SomeClass: SomeProtocol {
func someThrowingFunction(callback: () throws -> Void) throws { }
func someRethrowingFunction(callback: () throws -> Void) rethrows { }
// rethrow 메서드는 throw 메서드를 요구하는 프로토콜의 요구사항에 부합
func someThrowingFunctionFromProtocol(callback: () throws -> Void) rethrows { }
// throw 메서드는 rethrow 메서드를 요구하는 프로토콜의 요구사항을 충족할 수 없음
// 컴파일 에러!
func someRethrowingFunctionFromProtocol(callback: () throws -> Void) throws { }
}
class SomeChildClass: SomeClass {
// 부모클래스의 throw 메서드는 자식클래스에서 rethrow 메서드로 재정의 가능
override func someThrowingFunction(callback: () throws -> Void) rethrows { }
// 부모 클래스의 rethrow 메서드는 자식클래스의 throw 메서드로 재정의 불가
// 컴파일 에러!
override func someRethrowingFunction(callback: () throws -> Void) throws { }
}
- throw → rethrow ⭕️
- rethrow → throw ❌
func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]
사용 예시
enum MyError: Error {
case cannotDivide
}
func divideNumber(first: Float, second: Float) throws -> Float {
if second == 0 {
throw MyError.cannotDivide
}
return first/second
}
func calculateFunction(function: (Float, Float) throws -> Float) rethrows {
print(try function(2, 0))
}
do {
try calculateFunction(function: divideNumber)
} catch {
print(error, ": 0으로 나눌 수 없음!")
}
- rethrow 를 throw 라고 적어도 문제 없음
: 미루다, 연기하다는 뜻
- 현재 코드 블록을 나가기 전에 꼭 특정 코드를 실행해야 하는 경우 사용
- 에러 처리 외에도 사용될 수 있음
- 실행 순서
- 맨 마지막에 작성된 구문부터 역순으로 실행됨
- 오류가 던져지면, 그 직전 defer 까지 실행됨
- 현재 코드 블록을 나가기 전에 무조건 실행됨
- defer 내부에는 break, return 같이 구문을 빠져나가는 코드나, 오류 던지는 코드를 작성해서는 안됨
func someThrowingFunction(shouldThrowError: Bool) throws -> Int {
print("1")
defer {
print("2")
}
do {
defer {
print("3")
}
print("4")
}
defer {
print("5")
}
if shouldThrowError {
enum SomeError: Error {
case justSomeError
}
throw SomeError.justSomeError
}
defer {
print("6")
}
print("7")
return 100
}
try? someThrowingFunction(shouldThrowError: true)
// 1
// 4
// 3
// 5
// 2
try? someThrowingFunction(shouldThrowError: false)
// 1
// 4
// 3
// 7
// 6
// 5
// 2
- 주의 사항 :
throw
보다 앞에 작성해야 실행됨
func someThrowingFunction() throws {
defer {
print("finished")
}
throw CustomErrorStruct(message: "struct error~")
}
try someThrowingFunction()
// finished
// Swift/ErrorType.swift:200:
// Fatal error: Error raised at top level
// : study_swift.CustomErrorStruct(message: "struct error~")
NSLock
을 이용해 데드락을 피할 방법으로 쓸 수 있음
let myLock: NSLock = .init()
func fetchData() {
myLock.lock()
defer { myLock.unlock() }
if data == nil { return }
//이후 작업
}
: 함수가 종료되는 경우
func test(isError: Bool) throws -> Void{
defer {
print("1")
}
if isError {
enum TestError: Error {
case error
}
throw TestError.error
}
defer {
print("2")
}
print("3")
}
try? test(isError: true)
// 1
try? test(isError: false)
// 3 2 1
- throw 로 인해 함수가 종료될 경우, 아래 선언된
defer
에 도달하지 못해 호출되지 않는다
func test(string: String?){
defer {
print("1")
}
guard let str = string else {
return
}
defer {
print("2")
}
print("3")
}
test(string: nil)
// 1
test(string: "test")
// 3 2 1
throw
와 마찬가지로defer
에 도달하기 전에 함수가 종료되어 호출되지 않는다
Never : 정상적으로 끝나지 않은 함수
- 오류를 보고하는 일을 하고, 프로세스를 종료한다
func test() -> Never {
defer {
print("test 1")
}
defer {
print("test 2")
}
defer {
print("test 3")
}
abort() // test 중일 때 앱을 종료하는 함수
}
- 에러가 발생하면서 함수를 반환하지 않고 실행을 종료하기 때문에 defer가 호출되지 않는다
var a: String? = nil
func b() -> String {
a = "Hello world"
defer { a = nil }
return a!
}
print(b()) // Hello world
- force unwrapping에서 crash가 날 것이라 예상했으나, 잘 출력됨
Hopper 를 이용해 수도 코드 분석하기
수도 코드에서 색칠된 부분과 함수 코드 매칭
-
앱이 crash 나지 않는 이유 : a값을 읽는 작업 후에
a = nil
이 실행되기 때문 -
이를 통해
defer
가 로직 이전이나 이후가 아니라 로직 중간에 나타남을 알 수 있음 -
리턴 후에 defer 호출
- 스위프트 프로그래밍 3판 (야곰 저)
Validation
예제 코드 - Localized Error 포함- 구조체 에러 예제 - Xcode 에러
- rethrow 왜 쓰는지 알 수 있는 블로그
- defer 참고 블로그
- defer가 호출되지 않는 경우 (뀔뀔의 개발새발기 블로그)
- defer의 동작 원리 (FLIP.LOG)