Kotlin文件读取:如何优雅处理use块中的文件不存在异常
Great question! It's totally understandable to want to keep that clean Kotlin chaining while handling file absence gracefully. Let's break down why your initial approach caused an NPE, then look at a few elegant solutions.
Why Your Initial Approach Crashed
When your inputStreamOrNull() returns null (for a missing file), the standard use function passes this null directly to your block. If you try to call methods on that null stream inside the block, you’ll trigger a NullPointerException. The use function’s catch block does handle exceptions thrown in the block, but NPEs here are just regular exceptions that get re-thrown, leading to crashes.
Solution 1: Use takeIf for Null-Safe Chaining
This is the simplest approach using only standard Kotlin functions, no custom extensions needed:
val cacheDir = externalCacheDir File(cacheDir, "missingfile.dat") .takeIf { it.exists() } ?.inputStream() ?.use { stream -> // Process the stream safely here }
- Pros: Clean, leverages Kotlin’s null-safe chaining to skip the
useblock entirely if the file is missing. - Cons: A tiny race condition exists: the file could be deleted right after
exists()returnstruebut beforeinputStream()is called. This is rare in most app scenarios, but worth noting.
Solution 2: Pair inputStreamOrNull with a Null-Safe use Extension
If you want to avoid the race condition (by trying to open the stream directly instead of checking existence first), create a custom useOrNull extension that handles null gracefully:
// Refine your extension to only catch the specific exception we care about fun File.inputStreamOrNull(): FileInputStream? { return try { FileInputStream(this) } catch (e: FileNotFoundException) { null } } // Custom use extension for nullable Closeables fun <T : Closeable?, R> T.useOrNull(block: (T) -> R?): R? { var result: R? = null try { this?.let { result = block(it) } } finally { this?.close() // Safely close if the stream was opened } return result }
Now call it like this:
File(cacheDir, "missingfile.dat") .inputStreamOrNull() .useOrNull { stream -> stream?.let { // Process the stream without NPE risk } }
- Pros: No race condition—we attempt to open the stream directly, which is the most reliable check for file existence.
- Cons: Requires two reusable custom extensions, but they’re easy to maintain across your codebase.
Solution 3: Wrap with runCatching (Cleanest Standard Library Approach)
Kotlin’s runCatching is made for this scenario—it wraps your entire chain, catches exceptions, and lets you handle failures without breaking the flow:
runCatching { File(cacheDir, "missingfile.dat") .inputStream() .use { stream -> // Process the stream normally } }.onFailure { exception -> when (exception) { is FileNotFoundException -> { // Handle missing file case here } else -> { // Handle other IO exceptions if needed } } }
- Pros: Uses only standard library functions, no custom code needed. Keeps your original chain intact and handles all exceptions from stream opening and processing.
- Cons: If you only care about
FileNotFoundException, you’ll need to check the exception type inonFailure.
All three approaches keep your code concise and idiomatic Kotlin. My personal pick is Solution 3 because it’s the most maintainable and avoids race conditions or custom extensions unless you specifically need them.
内容的提问来源于stack exchange,提问作者Ersen Osman

