[Swift] Typed throws
Xcode 16 릴리스 노트를 보다가...새롭게 알게 된 내용이 있어서 간단히 정리!!
# Typed throws
그냥 한마디로
enum MyError: Error {
case invalid
}
func foo() ➡️throws(MyError)⬅️ -> String { ... }
이런식으로 throws할 때 에러 타입을 지정할 수 있게 되는 것 같다.
위 foo메소드는 String을 리턴하거나, 오직 MyError타입의 에러만 throw할 수 있다.
enum OtherError: Error {
case 저쩌구
}
func foo() throws(MyError) -> String {
do {
try ~~~~~~
} catch {
throw OtherError.저쩌구 // 🔴 Thrown expression type 'ViewController.OtherError' cannot be converted to error type 'ViewController.MyError'
}
}
만약 MyError가 아닌 타입을 throw하려고 하면 컴파일 에러가 발생하게 된다.
(위 예제에서는 MyError가 아닌 OtherError를 throw하려고함)
기존에 사용하던것 처럼 타입을 지정하지 않으면
func bar() throws -> String { }
throws(any Error)와 동일하다. (모든 에러타입을 넘기기 가능)
그리고..Typed throws추가되면서 do throws문도 추가된 것 같다..?!
# do throws(에러타입)
enum MyError: Error {
case invalid
}
do throws(MyError) {
if true {
throw .invalid
}
} catch {
// error는 MyError로 추론됨
}
do throws(Error타입) ⬅️ 이런식으로 쓸 수 있는 것 같다.
catch블록 내의 error는 당연히 해당 에러타입이 된다!
# 에러타입 추론
그리고 do 블록 내의 모든 throw문에서 발생하는 에러타입이 동일할경우
do /*infers throws(MyError)*/ {
try foo() // throws MyError
if true {
throw MyError.invalid // throws MyError
}
} catch {
// implicit 'error' value has type MyError
}
catch블록에서 error는 해당 에러타입으로 추론된다고 한다.
(do throws(동일하게 발생하는 에러타입)으로 추론하는듯)
⚠️ 근데 내 Xcode 16베타 && catch블록에서 error가 any Error로 나온다.. swift 6모드 켜도!! 흠 이건 수정될듯
만약 do 블록 내에서 발생하는 에러타입이 다를경우 catch블록내에서 error의 타입은 any Error로 추론된다.
do /*infers throws(any Error)*/ {
try callCat() // throws CatError
try callKids() // throw KidError
} catch {
// implicit 'error' variable has type 'any Error'
}
아래에는 이 Typed throws의 필요성?에 대해 Proposal에 나와있는 내용인데,
아주 간단히 왜 Typed throws가 필요하고 어떤 경우에 유용한지 정리해봤다.
Proposal에 더 정확히 나와있으니 참고하길!!
아래와 같은 에러타입이 있다고 가정할때
enum CatError: Error {
case sleeps
case sitsAtATree
}
기존의 throws는 Result와 Task에 비해서 더 적은 에러 정보를 전달하게 되는데,
func callCat() -> Result<Cat, CatError>
func callFutureCat() -> Task<Cat, CatError>
func callCatOrThrow() throws -> Cat
throws 메소드를 호출하는 코드에서 에러를 Result나 Task로 변환할 때 타입 정보가 손실되고,
이 손실된 정보는 명시적인 캐스팅을 통해서만 복원할 수 있기 때문이다.
이게 무슨소리냐!!
enum CatError: Error {
case sleeps
case sitsAtATree
}
func callCatOrThrow() throws -> Cat {
if Bool.random() {
return Cat()
} else {
throw CatError.sitsAtATree
}
}
뭐 이런 코드가 있다고 했을때
func callAndFeedCat1() -> Result<Cat, CatError> {
do {
return Result.success(try callCatOrThrow())
} catch {
return Result.failure(error) // 🔴 won't compile, because error type guarantee is missing in the first place
}
}
do 에서 callCatOrThrow()를 호출해도 callCatOrThrow는
func callCatOrThrow() throws -> Cat { ... }
에러타입이 정해져있지 않고 모든 에러를 throw할 수 있기 때문에 (any Error)
return Result.success(try callCatOrThrow())
이 시점에서 에러 타입의 정보가 손실되게 된다.
근데!!
func callAndFeedCat1() -> Result<Cat, CatError> {
callAndFeedCat1는 Error타입이 CatError로 지정되어있기 때문에
func callAndFeedCat1() -> Result<Cat, CatError> {
do {
return Result.success(try callCatOrThrow())
} catch {
return Result.failure(error) // 🔴 won't compile, because error type guarantee is missing in the first place
}
}
CatError에 대한 정보가 손실된 상태(any Error) 캐스팅 없이 error를 던지면 컴파일에러가 나게 된다.
아 ㅇㅋ....;;; 하고 CatError로 캐스팅을 아래와 같이 했다고 해보자
func callAndFeedCat2() -> Result<Cat, CatError> {
do {
return Result.success(try callCatOrThrow())
} catch let error as CatError {
// compiles
return Result.failure(error)
} catch {
return Result.failure(error)
// 🔴 won't compile, because exhaustiveness can't be checked by the compiler
// so what should we return here?
}
}
이렇게 되도 마지막 catch문에서 컴파일에러가 나게 된다(any Error타입이기 때문)
마지막 catch문에서 CatError중 하나를 아무거나 리턴하게 해도 되고
enum CatError: Error {
case sleeps
case sitsAtATree
case unknownError(Error)
}
이런식으로 case를 하나 추가해서.....처리해도 되지만
아무튼 any Error를 throw하는 메소드의 에러타입이 손실된다는 점이 포인트!!!
하지만 Typed throws를 사용하면
enum CatError: Error {
case sleeps
case sitsAtATree
}
func callCatOrThrow() throws(CatError) -> Cat { // ⭐️ throws(CatError)로 명시함
if Bool.random() {
return Cat()
} else {
throw CatError.sitsAtATree
}
}
func callAndFeedCat2() -> Result<Cat, CatError> {
do {
return Result.success(try callCatOrThrow())
} catch {
return Result.failure(error) // ✅
}
}
에러타입의 정보가 손실되지 않게 되며 Result로 변환하는 과정에서 별도의 캐스팅 없이 컴파일이 잘 된다.
아무튼 이렇게 구체적인 에러타입을 지정할 수 있게 되면서
1. 에러 처리의 명확성을 높히고, 특정 에러 타입에 대해 철저히 처리할 수 있는 이점이 생김
2. 기존의 any Error는 런타임에 타입 정보를 확인해야하기 떄문에 메모리/성능측면에서 오버헤드가 발생할 수 있는데, Typed throws는 컴파일타임에 에러타입이 결정되므로 이러한 오버헤드가 개선될 수 있다고 한다.
Typed throws가 도입되더라도 대부분의 Swift코드에서는 기존의 throws가(any Error) 여전히 더 나은 기본 에러 처리 메커니즘으로 여겨지므로 Typed throws는 특정상황에서 사용될 때 가장 적합하다고 한다.
e.g. 상대적으로 에러 조건이 고정되어있는경우. 같은 모듈이나 패키지에서 발생하는 에러 / 독립적인 라이브러리에서 발생하는 에러를 다루는 경우에 유용
# 참고