协程内部的通用泛型参数不起作用

Mic*_*551 6 java generics coroutine kotlin

我正在创建http json客户端。我将Volley与协程结合使用。我想创建通用的HTTP客户端,以便可以在任何地方使用它。

我创建了通用扩展方法来将JSON字符串解析为对象。

inline fun <reified T>String.jsonToObject(exclusionStrategy: ExclusionStrategy? = null) : T {
val builder = GsonBuilder()

if(exclusionStrategy != null){
    builder.setExclusionStrategies(exclusionStrategy)
}

return builder.create().fromJson(this, object: TypeToken<T>() {}.type)
Run Code Online (Sandbox Code Playgroud)

}

问题是,当我调用此方法时,无法得到预期的结果。第一次通话会给出正确的结果。对象已初始化。但是第二次调用(我使用传递给方法的通用参数)以异常“ LinkedTreeMap无法转换为令牌”结束。

    protected inline fun <reified T>sendRequestAsync(endpoint: String, data: Any?, method: Int, token: Token?): Deferred<T> {
    return ioScope.async {
        suspendCoroutine<T> { continuation ->
            val jsonObjectRequest = HttpClient.createJsonObjectRequest(
                endpoint,
                data?.toJsonString(),
                method,
                Response.Listener {
                    //this call is successful and object is initialized
                    val parsedObject : HttpResponse<Token> = it.toString().jsonToObject()

                    //this call is not successful and object is not initialized properly
                    val brokenObject : HttpResponse<T> = it.toString().jsonToObject()
                    continuation.resume(brokenObject.response)
                },
                Response.ErrorListener {
                    continuation.resumeWithException(parseException(it))
                },
                token)
            HttpClient.getInstance(context).addToRequestQueue(jsonObjectRequest)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

泛型方法的调用。

fun loginAsync(loginData: LoginData): Deferred<Token> {
    return sendRequestAsync("/tokens/", loginData, Request.Method.POST, null)
}
Run Code Online (Sandbox Code Playgroud)

这就是httpresponse数据类的外观。

data class HttpResponse<T> (
val response: T
)
Run Code Online (Sandbox Code Playgroud)

我在这里看到了使用Type :: class.java的解决方法,但是我不喜欢这种方法,我想使用经过修饰的关键字和内联关键字。 Kotlin中的reified关键字如何工作?

更新 这是我得到的例外。

java.lang.ClassCastException:com.google.gson.internal.LinkedTreeMap无法转换为com.xbionicsphere.x_card.entities.Token

可能的解决方法 我发现了可能的解决方法。如果我创建了一种方法,该方法将从响应中解析Token并在executeRequestAsync中使用此方法,那么一切都会开始工作,但是我不喜欢这种解决方案,因为我必须为每个请求添加其他参数。

新的loginAsync

fun loginAsync(loginData: LoginData): Deferred<Token> {
    val convertToResponse : (JSONObject) -> HttpResponse<Token> = {
        it.toString().jsonToObject()
    }

    return executeRequestAsync("/tokens/", loginData, Request.Method.POST, null, convertToResponse)
}
Run Code Online (Sandbox Code Playgroud)

新的executeRequestAsync

    protected inline fun <reified T>executeRequestAsync(endpoint: String, data: Any?, method: Int, token: Token?, crossinline responseProvider: (JSONObject) -> HttpResponse<T>): Deferred<T> {
    return ioScope.async {
        suspendCoroutine<T> { continuation ->
            val jsonObjectRequest =
                HttpClient.createJsonObjectRequest(
                    endpoint,
                    data?.toJsonString(),
                    method,
                    Response.Listener {
                        val response: HttpResponse<T> = responseProvider(it)
                        continuation.resume(response.response)
                    },
                    Response.ErrorListener {
                        continuation.resumeWithException(parseException(it))
                    },
                    token
                )
            HttpClient.getInstance(
                context
            ).addToRequestQueue(jsonObjectRequest)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新 我可能已经找到了可行的解决方案。executeRequestAsync需要通过通用参数提供的最终类型定义,因此我增强了方法的声明。现在方法声明如下所示:

    protected inline fun <reified HttpResponseOfType, Type>executeRequestAsync(endpoint: String, data: Any?, method: Int, token: Token?) : Deferred<Type> where HttpResponseOfType : HttpResponse<Type> {
    val scopedContext = context

    return ioScope.async {
        suspendCoroutine<Type> { continuation ->
            val jsonObjectRequest =
                HttpClient.createJsonObjectRequest(
                    endpoint,
                    data?.toJsonString(),
                    method,
                    Response.Listener {
                        val response: HttpResponseOfType = it.toString().jsonToObject()
                        continuation.resume(response.response)
                    },
                    Response.ErrorListener {
                        continuation.resumeWithException(parseException(it))
                    },
                    token
                )
            HttpClient.getInstance(
                scopedContext
            ).addToRequestQueue(jsonObjectRequest)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

感谢这个复杂的函数声明,我可以通过此调用执行请求:

fun loginAsync(loginData: LoginData): Deferred<Token> {
    return executeRequestAsync("/tokens/", loginData, Request.Method.POST, null)
}
Run Code Online (Sandbox Code Playgroud)

Qua*_*fel 6

为了理解为什么第二个调用的行为有点奇怪,以及为什么像Leo Aso所建议的那样,删除关键字inline并且reified(需要一个不可插入的函数)也会中断第一个调用,您必须了解类型擦除以及如何reified 首先启用类型化。

注意:以下代码是用Java编写的,因为我比对Kotlin的语法更熟悉Java。此外,这使得类型擦除更容易解释。

类型参数 a的通用功能是不是在运行时可用; 泛型仅是“编译时技巧”。这适用于Java和Kotlin(因为Kotlin能够在JVM上运行)。删除通用类型信息的过程称为类型擦除,发生在编译过程中。那么泛型函数在运行时如何工作?考虑以下函数,该函数返回任意集合中最有价值的元素。

<T> T findHighest(Comparator<T> comparator, Collection<? extends T> collection) {
    T highest = null;
    for (T element : collection) {
        if (highest == null || comparator.compare(element, highest) > 0)
            highest = element;
    }

    return highest;
}
Run Code Online (Sandbox Code Playgroud)

由于可以使用许多不同种类的集合等调用此函数,因此类型变量 的值T可能会随时间变化。为了确保它们全部兼容,在类型擦除期间对函数进行了重构。类型擦除完成后,该函数看起来将与此类似:

Object findHighest(Comparator comparator, Collection collection) {
    Object highest = null;
    for (Object element : collection) {
        if (highest == null || comparator.compare(element, highest) > 0)
            highest = element;
    }

    return highest;
}
Run Code Online (Sandbox Code Playgroud)

在类型擦除期间,类型变量将替换为其边界。在这种情况下,绑定类型为Object。参数化通常不会保留其泛型类型信息。

但是,如果编译擦除的代码,则会出现一些问题。考虑以下代码(未擦除),该代码调用已擦除的代码:

Comparator<CharSequence> comp = ...
List<String> list = ...
String max = findHighest(comp, list);
Run Code Online (Sandbox Code Playgroud)

由于#findHighest(Comparator, Collection)现在的回报Object,在第3行转让是非法的。因此,编译器在类型擦除期间在其中插入强制类型转换。

...
String max = (String) findHighest(comp, list);
Run Code Online (Sandbox Code Playgroud)

由于编译器始终知道必须插入哪个强制类型转换,因此在大多数情况下,类型擦除不会引起任何问题。但是,它有一些限制:instanceof不起作用,catch (T exception)是非法的(throws T允许,因为调用函数知道它必须期望什么样的异常),等等。必须克服的限制是缺乏可修改的限制( =常规类型信息(在运行时可用);泛型类型(有一些例外,但在此情况下无关紧要)。


但是,等等,Kotlin支持格式化类型,对吗?是的,但是正如我前面提到的,这仅适用于不可移植的函数。但是为什么呢?

inline调用包含关键字的签名的函数时,调用代码将替换为该函数的代码。由于“复制”代码不再必须与所有类型兼容,因此可以针对使用的上下文对其进行优化。

一种可能的优化方法是在完成类型擦除之前,在“复制的代码”中替换类型变量(幕后发生了很多事情)。因此,类型信息将保留并在运行时可用。它与其他任何非通用代码都没有区别。


尽管您#jsonToObject(ExclusionStrategy?)和的两个函数#sendRequestAsync(String, Any?, Int, Token?)都标记为inlinable且具有可更改的类型参数,但仍然缺少一些东西:T至少在您调用时#toJsonObject(ExclusionStrategy?),不可更改。

原因之一是您致电#suspendCoroutine(...)。要理解为什么这是一个问题,我们必须首先查看其声明:

suspend inline fun <T> suspendCoroutine(
    crossinline block: (Continuation<T>) -> Unit
): T
Run Code Online (Sandbox Code Playgroud)

crossinline-keyword是有问题的,因为它从内联是内声明的代码将停止编译器block。因此,您传递给的lambda #suspendCoroutine将被转移到匿名内部类中。从技术上讲,这是在运行时进行的。

因此,通用类型信息不再可用,至少在运行时不可用。在调用时#jsonToObject(...),类型变量T被擦除为ObjectTypeToken因此,Gson生成如下所示:

suspend inline fun <T> suspendCoroutine(
    crossinline block: (Continuation<T>) -> Unit
): T
Run Code Online (Sandbox Code Playgroud)

更新:这是我经过进一步研究后发现的,并非如此。crossinline不会阻止编译器内联lambda,而只是禁止它们影响函数的控制流。我可能将它与关键字混合使用了noinline,顾名思义,它实际上禁止内联。

但是,我非常确定以下部分。但是,我仍然必须找出为什么Gson无法正确确定和/或反序列化类型。我会在了解更多信息后立即更新。


这使我们进入最后一部分,试图解释您收到的奇怪异常。为此,我们必须看看Gsons的内部。

在内部,GSON有两个主要类型,负责反射序列化和反序列化:TypeAdapterFactoryTypeAdapter<T>.

TypeAdapter<T>只适应(=提供的(缩小)串行化逻辑)一种特定的类型。这意味着IntegerDoubleList<String>并且List<Float>都是由不同的处理TypeAdapter<T>秒。

TypeAdapterFactory正如其名称所暗示的那样,s负责提供匹配的TypeAdapter<T>s。TypeAdapter<T>s和TypeAdapterFactorys 之间的区别非常有用,因为一个工厂可能会为例如集合类型创建所有适配器,就像List它们都以类似方式工作一样。

为了确定您需要哪种适配器,Gson希望您TypeToken<T>在调用应处理通用类型的(反)序列化函数时传递a 。TypeToken<T>使用“技巧”来访问传递给其type参数的类型信息。

调用后Gson#fromJson(this, object: TypeToken<T>() {}.type),Gson会遍历所有可用TypeAdapterFactory的,直到找到可以提供合适的TypeAdapter<T>。Gson带有各种TypeAdapterFactory,包括用于原始数据类型,包装器类型,基本集合类型,日期等的工厂。除此之外,Gson还提供了两个特殊工厂:

  • ReflectiveTypeAdapterFactory顾名思义,该工厂尝试以反射方式访问对象的数据。为了适当地适应每个字段的类型,它为每个字段请求一个匹配的TypeAdapter。这是为(反)序列化HttpRequest选择的工厂。
  • ObjectTypeAdapter.Factory此工厂仅返回ObjectTypeAdapter。下面的代码片段显示了它在对象反序列化方面的作用(分别是HttpRequest对象中的字段):
TypeToken<HttpResponse<Object>>
Run Code Online (Sandbox Code Playgroud)

这就是为什么你得到了ClassCastException一个com.google.gson.internal.LinkedTreeMap