扩展函数与高阶函数在文件与IO中的应用

说实话,Kotlin 的扩展函数在文件 IO 这块,是我用得最爽的地方之一。你想想看,Java 里写个文件读写,又是 BufferedReader 又是 try-catch-finally,代码量不小。但 Kotlin 几行就搞定了。

我个人习惯把这类工具函数放在一个叫 FileExt.kt 的文件里。项目里谁要用,直接 import 就行。今天我们就来聊聊文件 IO、SharedPreferences 和 ContentProvider 这三个场景,看看扩展函数和高阶函数怎么帮我们省事。

File 的扩展函数:读写从未如此简单

Kotlin 标准库已经给 File 加了不少好东西。比如 readText()writeText(),这两个函数我几乎天天用。

// 读取文件内容
val content = File("config.json").readText()

// 写入文件内容
File("output.txt").writeText("Hello, Kotlin!")

// 追加内容
File("log.txt").appendText("New log entry\n")

你看,没有流,没有缓冲区,没有异常处理。为什么?因为 readText() 内部已经帮你处理好了。它自动打开流、读取全部内容、关闭资源。如果出错了,会抛出 IOException,你可以在调用方处理。

核心原理:readText() 内部调用了 BufferedReader 的 readText() 扩展,最终调用了 readBytes()。整个过程是同步的,适合小文件。

我在项目中遇到过一个问题:用 readText() 读了一个 50MB 的日志文件,结果直接 OOM 了。嗯,这里要注意——大文件千万别这么干。大文件要用 useLines() 逐行处理。

// 逐行处理大文件
File("large_log.txt").useLines { lines ->
    lines.filter { it.contains("ERROR") }
         .forEach { println(it) }
}

useLines() 是个高阶函数。它接收一个 lambda,lambda 的参数是一个 Sequence<String>。这个 Sequence 是懒加载的,不会一次性把所有行加载到内存。而且 useLines() 内部会保证流被正确关闭,哪怕 lambda 里抛了异常。

小技巧:如果你需要批量处理文件,可以自己写一个扩展函数。比如我写过一个 File.batchProcess(),每次读 1000 行,处理完再读下一批。这样既不会 OOM,又能利用批处理的性能优势。

SharedPreferences 的扩展:告别 Editor

SharedPreferences 的 API 设计,说实话,有点啰嗦。每次读写都要调用 edit(),然后 putXxx(),最后 apply()。用扩展函数封装一下,清爽多了。

// 定义扩展函数
fun SharedPreferences.put(key: String, value: Any?) {
    when (value) {
        is String? -> edit().putString(key, value).apply()
        is Int -> edit().putInt(key, value).apply()
        is Boolean -> edit().putBoolean(key, value).apply()
        is Float -> edit().putFloat(key, value).apply()
        is Long -> edit().putLong(key, value).apply()
        else -> throw IllegalArgumentException("Unsupported type")
    }
}

fun SharedPreferences.get(key: String, default: Any?): Any? {
    return when (default) {
        is String -> getString(key, default)
        is Int -> getInt(key, default)
        is Boolean -> getBoolean(key, default)
        is Float -> getFloat(key, default)
        is Long -> getLong(key, default)
        else -> null
    }
}

// 使用
val prefs = context.getSharedPreferences("config", Context.MODE_PRIVATE)
prefs.put("username", "KotlinFan")
prefs.put("isLoggedIn", true)

val name = prefs.get("username", "") as String

我曾经在项目里看到有人每个页面都写一遍 edit().putString().apply(),代码重复得不行。后来我封装了一个 SharedPreferencesManager 单例,配合扩展函数,整个项目里 SharedPreferences 的代码量减少了 70%。

注意:上面的 get() 函数用了 as 强制转换,如果类型不匹配会抛 ClassCastException。更安全的做法是用泛型 + reified 关键字,或者返回 Any? 让调用方自己处理。

ContentProvider 的简化:用高阶函数封装 CRUD

ContentProvider 的查询操作,写起来也挺繁琐的。Cursor 要手动管理,还要处理 null 安全。用高阶函数封装一下,可以省不少事。

// 封装查询操作
fun ContentResolver.query(
    uri: Uri,
    projection: Array<String>? = null,
    selection: String? = null,
    selectionArgs: Array<String>? = null,
    sortOrder: String? = null,
    block: (Cursor) -> Unit
) {
    val cursor = query(uri, projection, selection, selectionArgs, sortOrder)
    cursor?.use {
        block(it)
    }
}

// 使用
contentResolver.query(
    uri = ContactsContract.Contacts.CONTENT_URI,
    projection = arrayOf(ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME)
) { cursor ->
    while (cursor.moveToNext()) {
        val id = cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID))
        val name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
        println("Contact: $id - $name")
    }
}

你看,cursor?.use { } 这个写法很关键。use() 是 Kotlin 标准库提供的扩展函数,它会在 lambda 执行完毕后自动调用 close()。哪怕 lambda 里抛了异常,资源也会被正确释放。

我个人习惯把这种封装放在一个叫 ContentResolverExt.kt 的文件里。项目里所有 ContentProvider 的查询都走这个函数,既统一又安全。

避坑指南:我曾经在 ContentProvider 的查询里忘记关 Cursor,导致内存泄漏。后来用 use() 之后,再也没出过这个问题。记住:凡是实现了 CloseableAutoCloseable 的对象,都可以用 use() 来管理生命周期。

知识体系总览

下面这张图总结了本章的核心内容。你可以看到,扩展函数和高阶函数在文件 IO、SharedPreferences 和 ContentProvider 这三个场景中,分别解决了什么问题。

扩展函数与高阶函数在文件与IO中的应用 File 扩展函数 • readText() / writeText() • appendText() • useLines() 逐行处理 • readBytes() / writeBytes() 特点:自动管理资源 适合小文件快速读写 大文件用 useLines() SharedPreferences 扩展 • put(key, value) 统一写入 • get(key, default) 统一读取 • 支持 String/Int/Boolean Float/Long 类型 特点:告别 Editor 模板代码 类型安全需注意 建议配合泛型使用 ContentProvider 简化 • query() 高阶函数封装 • cursor.use() 自动关闭 • 支持投影、条件、排序 • 统一异常处理 特点:Cursor 生命周期管理 避免内存泄漏 代码更简洁 核心思想:用扩展函数消除模板代码,用高阶函数管理资源生命周期

说白了,这三个场景的核心思路是一样的:把重复的模板代码封装到扩展函数里,把资源管理交给高阶函数。你不需要每次都写 try-catch-finally,不需要手动关 Cursor,不需要调用 edit().apply()。Kotlin 的扩展函数和高阶函数帮你把这些脏活累活都干了。

嗯,最后说一句:这些扩展函数写好了,不只是自己用着爽,整个团队的代码质量都会提升。因为统一的封装意味着统一的错误处理、统一的资源管理、统一的代码风格。这就是 Kotlin 的魅力所在。