Android上的Kotlin Closable和SQLiteDatabase

hlu*_*kyi 6 sqlite android kotlin

我在我的项目中使用此代码

fun getAllData(): List<Data> = writableDatabase.use { db ->
    db.query(...)
}
Run Code Online (Sandbox Code Playgroud)

它在Lollipop设备上成功执行,但在Lollipop之前它会抛出ClassCastException

FATAL EXCEPTION: main
java.lang.ClassCastException: android.database.sqlite.SQLiteDatabase cannot be cast to java.io.Closeable
Run Code Online (Sandbox Code Playgroud)

可能是因为它是Java 1.6并且没有try-with-resources功能,但我不确定.

那么为什么我有这个例外以及它如何修复?

Jay*_*ard 5

该问题与try-with-resources无关,而仅仅是编译后的代码与运行时环境之间的不兼容依赖项问题。

问题是在编译时SQLiteDatabase实现该接口Closeable,然后将其换成另一个没有实现的实现。但是代码已经编译完毕,并且不知道此更改。

调用函数/方法时,将检查所有参数以确保它们是正确的类型,这通常由JVM处理。对于内联和扩展功能,Kotlin编译器会插入类型检查以确保其在正确的类型上运行,对于此特定的内联功能,该函数的接收者定义为Closeable。这是方法签名:

inline fun <T : Closeable, R> T.use(block: (T) -> R): R { ... }
Run Code Online (Sandbox Code Playgroud)

因此,编译器内联writableDatabase如下类型检查(以bytecode表示):

CHECKCAST java/io/Closeable
Run Code Online (Sandbox Code Playgroud)

因此,在这一点上,SQLiteDatabase必须永远实现Closeable接口的实现,否则编译后的代码将失败。通过切换到不再适用的旧版Android,您将违反合同并引发异常。编译器无法做任何不同的事情。就像我在编译代码后将任何JVM应用程序中的JAR换成具有完全不同的实现的JAR一样。更改Android版本基本上会交换所有JAR。

要解决此问题,您可以做的useSQLiteDatabase如果所有版本都有一个close方法(从Kotlin stdlib use函数复制和修改),则专门为该类编写您自己的函数:

inline fun <T : SQLiteDatabase, R> T.use(block: (T) -> R): R {
    var closed = false
    try {
        return block(this)
    } catch (e: Exception) {
        closed = true
        try {
            close()
        } catch (closeException: Exception) {
            // eat the closeException as we are already throwing the original cause
            // and we don't want to mask the real exception
       }
        throw e
    } finally {
        if (!closed) {
            close()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,内联CHECKCAST android/database/sqlite/SQLiteDatabase类型检查将始终成功。