- vừa được xem lúc

Anh Trai Say Bye "try-catch", Anh Trai Say Hi "runCatching"

0 0 6

Người đăng: Kẻ hai mặt

Theo Viblo Asia

Một cách xử lý ngoại lệ "sạch hơn" trong Kotlin với runCatching

image.png

Giới thiệu

Xử lý ngoại lệ là một khía cạnh quan trọng trong việc xây dựng các ứng dụng mạnh mẽ. Tuy nhiên, không có một giải pháp duy nhất phù hợp cho tất cả khi nói đến xử lý lỗi. Các mô hình và trường hợp sử dụng khác nhau yêu cầu các cách tiếp cận khác nhau.

Roman Elizarov, một nhân vật chủ chốt trong thiết kế của Kotlin, đã nhấn mạnh rằng chúng ta không nên bắt ngoại lệ một cách không cần thiết, đặc biệt là trong các ứng dụng có cấu trúc, và thay vào đó để framework xử lý chúng khi có thể.

Tuy nhiên, trong các ứng dụng dựa trên JVM tích hợp với thư viện Java, việc tránh bắt ngoại lệ thường không thực tế. Không giống như Kotlin, Java sử dụng các ngoại lệ kiểm tra, không phải lúc nào cũng chỉ ra lỗi lập trình mà là các điều kiện có thể phục hồi. Trong Kotlin, thường ưu tiên xử lý các lỗi có thể phục hồi một cách rõ ràng bằng cách trả về một giá trị, chẳng hạn như sử dụng Result<T>, thay vì ném ngoại lệ. Điều này làm cho việc xử lý lỗi rõ ràng hơn và tránh các gián đoạn không mong muốn trong luồng thực thi. Một thư viện lập trình hàm phổ biến, Arrow, cung cấp Either, tương tự bao bọc thành công và thất bại. Arrow cung cấp nhiều cấu trúc hàm hơn để làm việc với lỗi một cách an toàn về kiểu.

Ngoài ra, ngay cả khi một ngoại lệ nên được truyền đi, nền tảng bạn đang chạy có thể không xử lý nó chính xác như bạn muốn. Ví dụ, một số nền tảng có thể không ghi lại lỗi với ngữ cảnh cần thiết, chẳng hạn như các thông số tùy chỉnh cho việc giám sát và cảnh báo. Trong những trường hợp như vậy, việc bắt ngoại lệ một cách rõ ràng và xử lý chúng theo cách có cấu trúc có thể cung cấp quyền kiểm soát tốt hơn đối với việc ghi nhật ký và giám sát.

Để đơn giản hóa việc xử lý ngoại lệ, thư viện chuẩn của Kotlin cung cấp hàm runCatching, tự động bắt ngoại lệ và bao bọc chúng trong một Result.

Trong bài viết này, chúng ta sẽ khám phá cách runCatching tích hợp với Result bằng cách tự động bắt ngoại lệ và bao bọc chúng trong trạng thái Failure. Chúng ta cũng sẽ xem xét một số tác dụng phụ của việc sử dụng runCatching.

Trả về Result để có code an toàn hơn

Vì tất cả các ngoại lệ trong Kotlin đều unchecked, khi bạn ném ngoại lệ từ một hàm thay vì trả về kết quả, người gọi có thể không biết điều gì sẽ xảy ra và không bị buộc phải xử lý ngoại lệ. Điều này có thể dẫn đến hành vi không thể đoán trước và các lỗi không được bắt tại thời gian chạy. Bằng cách luôn trả về một Result<T>, bạn có thể đảm bảo rằng việc xử lý lỗi là rõ ràng và được thực thi, dẫn đến code an toàn và dễ bảo trì hơn.

Thay vì ném một ngoại lệ, chúng ta bao bọc thao tác trong runCatching:

fun parseNumber(input: String): Result<Int> { return runCatching { input.toInt() }
}

Bây giờ, gọi n thôi

val result = parseNumber("123")
result.onSuccess { println("Parsed number: $it") } .onFailure { println("Failed to parse number: ${it.message}") }

runCatching như một Scope Function

Kotlin cung cấp nhiều scope functyion (run, let, apply, v.v.) cho phép các thao tác ngắn gọn và dễ đọc trên các đối tượng. runCatching là một phiên bản chuyên biệt của run bao bọc kết quả thực thi trong một đối tượng Result, làm cho việc xử lý lỗi có cấu trúc hơn.

Không giống như một khối run tiêu chuẩn, trả về kết quả trực tiếp, runCatching đảm bảo rằng nếu xảy ra ngoại lệ, nó được bắt bên trong một Result.Failure thay vì lan truyền như một ngoại lệ không được xử lý.

Ví dụ:

data class Order(val id: String, val quantity: Int)
val order = Order(id = "123", quantity = 0) val result = order.runCatching { 100 / quantity // This will cause a division by zero
}.onFailure { e -> println("An error occurred: ${e.message}")
}.onSuccess { value -> println("Computation successful: $value")
}
println("Result object: $result")

Sử dụng runCatching như một scope function bao bọc cả tính toán và xử lý lỗi trong một khối duy nhất, giảm mã lặp. Thay vì ném ngoại lệ, các lỗi có thể được xử lý an toàn bằng cách sử dụng onFailure, recover, hoặc getOrElse.

Thay thế Try-Catch bằng một Khối Catch Rỗng

Ngoài việc chỉ trả về kết quả, runCatching có thể thay thế try-catch trong nhiều trường hợp, ngay cả khi bạn muốn hành vi vẫn không thay đổi. Nó cho phép xử lý lỗi một cách rõ ràng trong khi giữ cho code dễ đọc và súc tích hơn.

Xem xét một ví dụ mà chúng ta muốn bỏ qua một ngoại lệ, trong đó try-catch chứa một khối catch rỗng, điều này triệt tiêu các ngoại lệ:

fun fireAndForget() { try { riskyFunction() } catch (t: Throwable) { // Ignore }
}

Sử dụng runCatching, chúng ta có thể đạt được kết quả tương tự một cách súc tích hơn:

fun fireAndForget() { runCatching { riskyFunction() }
}

Quay lại Giá trị Mặc định (Falling Back to a Default Value)

Một mẫu phổ biến trong các khối try-catch truyền thống là cung cấp một giá trị mặc định khi có ngoại lệ xảy ra:

fun parseNumberWithDefault(input: String): Int { return try { input.toInt() } catch (t: Throwable) { 0 // Default value for invalid numbers }
}

Với runCatching, điều này có thể được đơn giản hóa bằng cách sử dụng getOrElse, hàm này cung cấp một giá trị mặc định trong trường hợp thất bại:

fun parseNumberWithDefault(input: String): Int { return runCatching { input.toInt() }.getOrElse { 0 }
}

Điều này loại bỏ sự cần thiết của các khối try-catch rõ ràng trong khi đảm bảo một giá trị dự phòng được trả về trong trường hợp thất bại.

Ném lại các Ngoại lệ Gốc với getOrThrow

Trong một số trường hợp, bạn vẫn có thể muốn ném ra ngoại lệ. Ví dụ, trong một số thư viện, framework hoặc chức năng cloud serverless, ngoại lệ dẫn đến các cơ chế xử lý lỗi tự động, chẳng hạn như thử lại và dead lettering. Một ví dụ khác là khi refactor một ứng dụng từ try-catch sang runCatching, nhưng bạn muốn giữ nguyên cách xử lý lỗi hiện có. Trong những trường hợp này, bạn có thể sử dụng getOrThrow.

Trước đây

fun parseNumberWithExceptions(input: String): Int { return try { input.toInt() } catch (e: NumberFormatException) { logger.error(e){"Failed parsing integer"} // Log error throw e // Unexpected exceptions are rethrown }
}

Sau này:

fun parseNumberWithExceptions(input: String): Int { return runCatching { input.toInt() } .onFailure { e -> logger.error(e){"Failed parsing integer"}} // Log error .getOrThrow()
}

Xử lý các Ngoại lệ Lồng nhau với runCatching

Trong nhiều ứng dụng, nhiều thao tác phụ thuộc có thể thất bại theo nhiều cách khác nhau. Đọc một tệp có thể thất bại nếu tệp bị thiếu, phân tích nội dung của nó có thể thất bại nếu định dạng không hợp lệ và xử lý dữ liệu đã phân tích cú pháp có thể thất bại nếu các giá trị bắt buộc bị thiếu. Xử lý các lỗi này bằng các khối try-catch lồng nhau thường dẫn đến code khó đọc và khó bảo trì.

Trước đây: try-catch lồng nhau

fun processFile(path: String): ProcessedData { return try { val content = File(path).readText() try { val json = parseJson(content) try { processData(json) } catch (e: Exception) { logger.error(e) { "Failed to process data" } throw e } } catch (e: Exception) { logger.error(e) { "Failed to parse JSON" } throw e } } catch (e: Exception) { logger.error(e) { "Failed to read file: $path" } throw e }
}

Trước đây: try-catch dài dòng đã được refactor

fun processFile(path: String): ProcessedData { val content = try { File(path).readText() } catch (e: Exception) { logger.error(e) { "Failed to read file: $path" } throw e } val json = try { parseJson(content) } catch (e: Exception) { logger.error(e) { "Failed to parse JSON" } throw e } return try { processData(json) } catch (e: Exception) { logger.error(e) { "Failed to process data" } throw e }
}

Sau này: Súc tích với runCatching

fun processFile(path: String): ProcessedData { return runCatching { File(path).readText() } .onFailure { e -> logger.error(e) { "Failed to read file: $path" } } .mapCatching { content -> parseJson(content) } .onFailure { e -> logger.error(e) { "Failed to parse JSON" } } .mapCatching { json -> processData(json) } .onFailure { e -> logger.error(e) { "Failed to process data" } } .getOrThrow()
}

Với runCatching kết hợp với mapCatching, việc xử lý lỗi trở nên có cấu trúc và súc tích hơn.

Xử lý Nhiều Kiểu Ngoại lệ (Handling Multiple Exception Types)

Trong một số trường hợp, các ngoại lệ khác nhau yêu cầu cách xử lý khác nhau, điều này thường được thực hiện bằng cách sử dụng nhiều khối catch trong cấu trúc try-catch. runCatching không cung cấp cú pháp tích hợp sẵn cho nhiều ngoại lệ, nhưng bạn có thể đạt được logic tương tự bằng cách kết hợp runCatching với when.

Trước đây:

fun readFile(path: String): String { return try { File(path).readText() } catch (e: FileNotFoundException) { logger.error(e) { "File not found: $path" } throw e } catch (e: IOException) { logger.error(e) { "Failed to read file: $path" } throw e }
}

Sau này:

fun readFile(path: String): String { return runCatching { File(path).readText() } .onFailure { e -> when (e) { is FileNotFoundException -> logger.error(e) { "File not found: $path" } is IOException -> logger.error(e) { "Failed to read file: $path" } } }.getOrThrow()
}

Thay thế các Khối Try-Catch-Finally

Nếu chúng ta cần thực thi code dọn dẹp bất kể thành công hay thất bại, một khối try-catch-finally truyền thống sẽ thực thi code trong khối finally. Giống như xử lý nhiều ngoại lệ, runCatching không cung cấp hỗ trợ tích hợp sẵn cho việc này. Tuy nhiên, chúng ta có thể sử dụng hàm use tích hợp sẵn của Kotlin để dọn dẹp các tài nguyên có thể đóng (Closable resources).

Đối với các tài nguyên như tệp, stream hoặc kết nối cơ sở dữ liệu, use được ưu tiên vì nó tự động đóng tài nguyên. try-catch-finally truyền thống trông như thế này:

fun readFileWithTryCatch(path: String): String { val reader = File(path).bufferedReader() return try { reader.readText() } catch (e: Exception) { logger.error(e){"Failed to read file: ${e.message}"} throw e } finally { reader.close() // Must be manually closed }
}

Sử dụng runCatching với use thay thế để loại bỏ việc quản lý tài nguyên thủ công:

fun readFileWithUse(path: String): String { return runCatching { File(path).bufferedReader().use { it.readText() } // Auto-closes reader }.onFailure { logger.error(it){"Failed to read file: ${it.message}" }} .getOrThrow()
}

Xử lý Coroutines và Lỗi với runCatching

Mặc dù runCatching đơn giản hóa việc xử lý lỗi, điều quan trọng cần lưu ý là nó bắt tất cả Throwable, bao gồm cả ngoại lệ (Exception) và lỗi (Error). Điều này bao gồm CancellationException, được coroutines sử dụng để báo hiệu việc hủy bỏ. Việc bắt nó một cách vô ý có thể ngăn chặn việc hủy coroutine đúng cách, dẫn đến các tác dụng phụ như UI không phản hồi hoặc rò rỉ tài nguyên.

Tránh các Vấn đề Hủy bỏ trong Coroutines

Valerii Popov đã chỉ ra rằng runCatching bắt CancellationException, điều này có thể vô ý triệt tiêu việc hủy coroutine. Vì CancellationException không phải là một lỗi điển hình mà là một cơ chế kiểm soát, nó nên được ném lại để cho phép hành vi coroutine phù hợp.

Để đảm bảo CancellationException được truyền bá chính xác trong khi vẫn xử lý các ngoại lệ khác, bạn có thể sử dụng một hàm extension tùy chỉnh:

inline fun <reified E : Throwable, T> Result<T>.onFailureOrRethrow(action: (Throwable) -> Unit): Result<T> { return onFailure { if (it is E) throw it else action(it) }
} inline fun <T> Result<T>.onFailureIgnoreCancellation(action: (Throwable) -> Unit): Result<T> { return onFailureOrRethrow<CancellationException, T>(action)
}

Sau đó, bạn có thể áp dụng nó cho runCatching như sau:

val result = runCatching { apiService.fetchDataFromServer()
}.onFailureIgnoreCancellation { println("Handled non-cancellation error: ${it.message}")
}

Điều này đảm bảo rằng CancellationException được truyền đúng cách, cho phép coroutines hủy bỏ như mong đợi trong khi vẫn xử lý các thất bại khác

Tránh các Lỗi Cấp Hệ thống (Avoiding System-Level Errors)

Trong hầu hết các ứng dụng, bạn chỉ cần bắt các ngoại lệ có thể phục hồi (Exception) thay vì các lỗi cấp hệ thống (Error). Nếu bạn đang bắt và không truyền nó đi, thì bạn muốn đảm bảo rằng runCatching không triệt tiêu các lỗi nghiêm trọng, bạn có thể lọc chúng ra theo cách tương tự như CancellationException, ví dụ:

inline fun <T> Result<T>.onFailureIgnoreErrors(action: (Throwable) -> Unit): Result<T> { return onFailureOrRethrow<Error, T>(action)
}

Cách dùng:

val result = runCatching { riskyOperation()
}.onFailureIgnoreErrors { println("Handled exception: ${it.message}")
}

Static Analysis để Xử lý Ngoại lệ Tốt hơn

Để đảm bảo các thực hành tốt nhất trong xử lý ngoại lệ, các công cụ Static Analysis như Detekt có thể giúp phát hiện các mẫu có vấn đề. coroutines ruleset có thể gắn cờ việc xử lý CancellationException không chính xác, và exceptions ruleset có thể cảnh báo về việc triệt tiêu ngoại lệ. Cấu hình Detekt cho phù hợp với nhu cầu dự án của bạn giúp bạn tránh các cạm bẫy phổ biến.

Bằng cách ghi nhớ những cân nhắc này, bạn có thể sử dụng runCatching một cách hiệu quả trong khi vẫn duy trì hành vi coroutine phù hợp và tránh triệt tiêu không chủ ý các lỗi cấp hệ thống.

Kết luận

runCatching là một công cụ mạnh mẽ khi bạn muốn luôn trả về một kết quả — dù thành công hay thất bại — mà không có các ngoại lệ bất ngờ làm gián đoạn quá trình thực thi. Tuy nhiên, lợi ích của nó không chỉ dừng lại ở việc trả về kết quả. Trong nhiều trường hợp, nó cũng có thể thay thế các khối try-catch và try-catch-finally truyền thống, làm cho việc xử lý lỗi có cấu trúc, súc tích và dễ theo dõi hơn.

Bằng cách sử dụng runCatching, chúng ta duy trì một mẫu xử lý lỗi nhất quán trong toàn bộ codebase, cho dù chúng ta cần luôn trả về kết quả, xử lý nhiều ngoại lệ hay vẫn truyền bá ngoại lệ khi cần thiết. Cách tiếp cận này dẫn đến code sạch hơn, dễ đọc hơn và dễ bảo trì hơn.

⚠️ Thận trọng: Khi sử dụng runCatching, hãy lưu ý rằng nó bắt Throwable, bao gồm cả Error và CancellationException. Cân nhắc sử dụng các kỹ thuật như ném lại ngoại lệ khi cần thiết.

Túm cái váy lại thì:

Bằng cách thay thế các khối try-catch-finally truyền thống bằng runCatching, bạn có thể làm cho code của mình có cấu trúc và dễ hiểu hơn:

  • Sử dụng runCatching khi bạn muốn các hàm của mình luôn trả về một kết quả có ý nghĩa thay vì ném ra ngoại lệ.

  • Thay thế các khối catch rỗng bằng runCatching súc tích hơn.

  • Sử dụng getOrElse để cung cấp các giá trị mặc định trong trường hợp thất bại.

  • Sử dụng getOrThrow khi bạn cần ném lại ngoại lệ trong khi vẫn giữ cấu trúc xử lý lỗi.

  • Xử lý nhiều kiểu ngoại lệ bằng cách sử dụng when bên trong recover, recoverCatching hoặc onFailure.

  • Tận dụng use để xử lý đúng cách việc dọn dẹp tài nguyên, thay thế các khối finally một cách hiệu quả.

  • Nếu sử dụng trong coroutines, đảm bảo xử lý phù hợp để tránh triệt tiêu việc hủy bỏ không chủ ý.

    Bài viết hôm nay đến đây là hết ,cảm ơn mọi người đã theo dõi

    Nguồn: https://proandroiddev.com/kotlin-tips-and-tricks-you-may-not-know-7-goodbye-try-catch-hello-trycatching-7135cb382609

Bình luận

Bài viết tương tự

- vừa được xem lúc

Học Flutter từ cơ bản đến nâng cao. Phần 1: Làm quen cô nàng Flutter

Lời mở đầu. Gần đây, Flutter nổi lên và được Google PR như một xu thế của lập trình di động vậy.

0 0 303

- vừa được xem lúc

Học Flutter từ cơ bản đến nâng cao. Phần 3: Lột trần cô nàng Flutter, BuildContext là gì?

Lời mở đầu. Màn làm quen cô nàng FLutter ở Phần 1 đã gieo rắc vào đầu chúng ta quá nhiều điều bí ẩn về nàng Flutter.

1 1 365

- vừa được xem lúc

[Android] Hiển thị Activity trên màn hình khóa - Show Activity over lock screen

Xin chào các bạn, Hôm nay là 30 tết rồi, ngồi ngắm trời chờ đón giao thừa, trong lúc rảnh rỗi mình quyết định ngồi viết bài sau 1 thời gian vắng bóng. .

0 0 121

- vừa được xem lúc

Tìm hiểu Proguard trong Android

1. Proguard là gì . Cụ thể nó giúp ứng dụng của chúng ta:. .

0 0 113

- vừa được xem lúc

Làm ứng dụng học toán đơn giản với React Native - Phần 6

Chào các bạn một năm mới an khang thịnh vượng, dồi dào sức khỏe. Lại là mình đây Đây là link app mà các bạn đang theo dõi :3 https://play.google.com/store/apps/details?id=com.

0 0 89

- vừa được xem lúc

20 Plugin hữu ích cho Android Studio

1. CodeGlance. Plugin này sẽ nhúng một minimap vào editor cùng với thanh cuộn cũng khá là lớn. Nó sẽ giúp chúng ta xem trước bộ khung của code và cho phép điều hướng đến đoạn code mà ta mong muốn một cách nhanh chóng.

0 0 326