可能会失败

一个 Swift 库,用于处理异步和同步错误,无需 trycatch 代码块。

文档

安装

Swift 包管理器

将 MightFail 作为依赖项添加到你的 Package.swift 文件

dependencies: [
    .package(url: "https://github.com/might-fail/swift.git", from: "0.1.0")
]

然后将其添加到你的目标依赖项中

targets: [
    .target(
        name: "YourTarget",
        dependencies: ["MightFail"]),
]

导入

在你的源文件中导入 MightFail

import MightFail

用法

MightFail 提供了一种简化的方式来处理 Swift 中的错误,无需传统的 try-catch 代码块。它适用于同步和异步代码。

重要提示

// Good
guard let data else {
    // handle error
}
// Good 
guard success else {
    // handle error
}
// Bad
if let error {
    // handle error
}

基本同步用法

// Returns (error, result, success)
let (error, result, success) = mightFail {
    return "Success"
}

// Check success
if success {
    print(result) // "Success"
}

简化返回类型

// Returns just (error, result)
let (error, result) = mightFail {
    return 42
}

print(result) // 42
print(error) // nil

错误处理

传统错误处理

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).")
}

使用 MightFail

let vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
let (error, _, success) = mightFail {
    try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine)
}
guard success else {
    switch error {
    case VendingMachineError.invalidSelection:
        print("Invalid Selection.")
    case VendingMachineError.outOfStock:
        print("Out of Stock.")
    case VendingMachineError.insufficientFunds(let coinsNeeded):
        print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
    default:
        print("Unexpected error: \(error).")
    }
    return
}
print("Success! Yum.")

异步支持

// Basic async usage
let (error, result, success) = await mightFail {
    try await Task.sleep(nanoseconds: 1_000_000)
    return "Async Success"
}

// Simplified async return
let (error, result) = await mightFail {
    try await Task.sleep(nanoseconds: 1_000_000)
    return 42
}

多个操作

你可以运行多个操作并获取它们的结果

let results = mightFail([
    { 1 },
    { throw TestError.simple },
    { 3 },
])

// Check results
results.forEach { (error, result) in
    guard let result else {
        print("Error: \(error)")
        return
    }
    print("Result: \(result)")
}

或者可能是这样的

guard let imageFiles = await memoryStorage.imageFilesStore[imageId] else {
    return
}
let deleteResults = mightFail([
    { try FileStorage.deleteFile(name: imageFiles.fullSizeName, ext: imageFiles.ext) },
    { try FileStorage.deleteFile(name: imageFiles.thumbnailName, ext: imageFiles.ext) },
    { try FileStorage.deleteFile(name: imageFiles.mediumSizeName, ext: imageFiles.ext) },
])

for deleteResult in deleteResults.filter({ $0.success == false }) {
    print("Failed to delete image file: \(deleteResult.error)")
}

for deleteResult in deleteResults.filter({ $0.success == true }) {
    print("Deleted image file: \(deleteResult.result)")
}

可选值

MightFail 优雅地处理可选值

func returnOptional() throws -> String? {
    return nil
}

let (error, result, success) = mightFail {
    try returnOptional()
}

// success will be true
// result will be nil
// error will be nil

do, try, catch 不好

我认为抛出异常很好,我喜欢异常会中断控制流,我也喜欢异常传播。我唯一不喜欢的是捕获异常。

这通常发生在代码中最“面向用户”的部分,例如 API 端点或 UI 组件,即最外层的函数调用。因此,捕获异常需要通知用户发生了错误,记录错误以进行调试,并停止当前的执行流程。

Guard ✅

Guarding 允许你尽早处理错误并从函数中提前返回,使其更具可读性且更易于理解。

// Generic fetch function that can work with any Codable type
func fetch<T: Codable>(from urlString: String) async throws -> Data {
    guard let url = URL(string: urlString) else {
        throw URLError(.badURL)
    }
    
    // Create and configure the URL session
    let session = URLSession.shared
    
    // Make the network request and await the response
    let (data, response) = try await session.data(from: url)

    guard let data = data else {
        throw URLError(.badServerResponse)
    }
    
    // Verify we got a successful HTTP response
    guard let httpResponse = response as? HTTPURLResponse,
            (200...299).contains(httpResponse.statusCode) else {
        throw URLError(.badServerResponse)
    }
}

现在,成功情况是唯一没有嵌套在 ifguard 语句中的代码。它也位于函数的底部,使其易于查找。

但由于某些原因,当我们调用此函数时,我们认为可以随意抛弃 guard 语句的所有优点。

do {
    let data = try await networkManager.fetch(
        from: "https://jsonplaceholder.typicode.com/posts/1"
    )
    // success case in the middle somewhere and nested
} catch {
    print("Error fetching post: \(error)")
}

所有代码放在一个 Do/Try/Catch 代码块中 ❌

然后这导致将大量代码放在 do 代码块中。

try {
    let data = try await networkManager.fetch(
        from: "https://jsonplaceholder.typicode.com/posts/1"
    )

    // Decode the JSON data into our Codable type
    let decoder = JSONDecoder()
    let post = try decoder.decode(Post.self, from: data)

    try post.data.write(toFile: "path/to/newfile.txt", atomically: true, encoding: .utf8)

    print("Successfully fetched, decoded, and saved the post")
} catch (error) {
  // handle any errors, not sure which one though 🤷‍♀️
}

// or 

catch let error as URLError {
    print("Network error: \(error.localizedDescription)")
    
} catch let error as DecodingError {
    print("JSON error: \(error.localizedDescription)")
    
} catch let error as CocoaError {
    print("File error: \(error.localizedDescription)")
    
} catch {
    print("Unexpected error: \(error)")
}

这是不好的,因为

  1. 所有成功情况的代码都将发生在 do 代码块内部。嵌套在函数的中间某个位置。
  2. 我们在远离导致错误的代码的地方处理错误,秩序感就丧失了。

多个 Do/Try/Catch 代码块 ❌

// First try-catch for network request
let data: Data
do {
    data = try await networkManager.fetch(
        from: "https://jsonplaceholder.typicode.com/posts/1"
    )
} catch let error as URLError {
    print("Network error: \(error.localizedDescription)")
    return // or throw, or handle error differently
} catch {
    print("Unexpected network error: \(error)")
    return
}

// Second try-catch for JSON decoding
let post: Post
do {
    let decoder = JSONDecoder()
    post = try decoder.decode(Post.self, from: data)
} catch let error as DecodingError {
    print("JSON decoding error: \(error.localizedDescription)")
    return
} catch {
    print("Unexpected decoding error: \(error)")
    return
}

// Third try-catch for file writing
do {
    try post.data.write(toFile: "path/to/newfile.txt", atomically: true, encoding: .utf8)
} catch let error as CocoaError {
    print("File writing error: \(error.localizedDescription)")
    return
} catch {
    print("Unexpected file error: \(error)")
    return
}

// If we get here, everything succeeded
print("Successfully fetched, decoded, and saved the post")

这可能是处理错误的一种更好的方法,但没有人会像这样编写三个 do catch 代码块。而且 catch 不如 guard 好,因为它不会强制你处理错误并提前返回。

正确的方法

Guarding 是好的,错误处理应该在 guard 中紧挨着导致错误的代码进行处理。成功情况的代码应该在 guard 错误处理之后。

我们已经知道这一点了,现在让我们用抛出异常的代码来这样做。

// First try-catch for network request
let (networkError, data) = await mightFail { try await networkManager.fetch(
    from: "https://jsonplaceholder.typicode.com/posts/1")
}
guard let data else {
    switch networkError {
    case URLError.badURL:
        print("Bad URL")
    default:
        print("Network error: \(networkError)")
    }
    return // or throw, or handle error differently
}

// Second try-catch for JSON decoding
let decoder = JSONDecoder()
let (decodingError, post) = await mightFail {
    try decoder.decode(Post.self, from: data)
}
guard let post else {
    switch decodingError {
    case DecodingError.keyNotFound:
        print("Key not found")
    default:
        print("Decoding error: \(decodingError)")
    }
    return
}

// Third try-catch for file writing
let (fileError, _, success) = await mightFail {
    try post.data.write(toFile: "path/to/newfile.txt", atomically: true, encoding: .utf8)
}
guard success else {
    switch fileError {
    case CocoaError.fileWriteNoPermission:
        print("No permission to write file")
    default:
        print("File writing error: \(fileError)")
    }
    return
}

// If we get here, everything succeeded
print("Successfully fetched, decoded, and saved the post")