扩展函数与高阶函数在文件与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() 之后,再也没出过这个问题。记住:凡是实现了 Closeable 或 AutoCloseable 的对象,都可以用 use() 来管理生命周期。
知识体系总览
下面这张图总结了本章的核心内容。你可以看到,扩展函数和高阶函数在文件 IO、SharedPreferences 和 ContentProvider 这三个场景中,分别解决了什么问题。
说白了,这三个场景的核心思路是一样的:把重复的模板代码封装到扩展函数里,把资源管理交给高阶函数。你不需要每次都写 try-catch-finally,不需要手动关 Cursor,不需要调用 edit().apply()。Kotlin 的扩展函数和高阶函数帮你把这些脏活累活都干了。
嗯,最后说一句:这些扩展函数写好了,不只是自己用着爽,整个团队的代码质量都会提升。因为统一的封装意味着统一的错误处理、统一的资源管理、统一的代码风格。这就是 Kotlin 的魅力所在。