我正在创建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)
}
问题是,当我调用此方法时,没有得到预期的结果。第一次通话会给出正确的结果。对象已初始化。但是第二次调用(我使用传递给方法的通用参数)以“ LinkedTreeMap无法转换为Token”结束。
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)
}
}
}
调用泛型方法。
fun loginAsync(loginData: LoginData): Deferred<Token> {
return sendRequestAsync("/tokens/", loginData, Request.Method.POST, null)
}
这是httpresponse数据类的外观。
data class HttpResponse<T> (
val response: T
)
我在这里看到使用Type :: class.java的解决方法,但是我不喜欢这种方法,我想使用经过修饰的关键字和内联关键字。 How does the reified keyword in Kotlin work?
更新 这是我得到的例外。
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)
}
新的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)
}
}
}
更新 我可能找到了可行的解决方案。 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)
}
}
}
感谢这个复杂的函数声明,我可以通过以下调用执行请求:
fun loginAsync(loginData: LoginData): Deferred<Token> {
return executeRequestAsync("/tokens/", loginData, Request.Method.POST, null)
}
答案 0 :(得分:6)
为了理解为什么第二个调用的行为有点奇怪,以及为什么按照Leo Aso的建议,删除关键字inline
和reified
(需要一个不可插入的函数)也会中断第一个调用,您必须了解类型擦除以及reified
如何首先启用类型确定。
注意:以下代码是用Java编写的,因为我对Java的了解比对Kotlin的语法更熟悉。此外,这使得类型擦除更容易解释。
泛型函数的类型参数在运行时不可用;泛型仅是“编译时技巧”。这适用于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;
}
由于可以使用许多不同种类的集合等调用此函数,因此 type变量 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;
}
在擦除类型期间,类型变量将替换为其边界。在这种情况下,绑定类型为Object
。参数化通常不会保留其泛型类型信息。
但是,如果您编译擦除的代码,则会出现一些问题。考虑以下代码(未擦除),该代码调用已擦除的代码:
Comparator<CharSequence> comp = ...
List<String> list = ...
String max = findHighest(comp, list);
由于#findHighest(Comparator, Collection)
现在返回Object
,因此第3行中的分配是非法的。因此,编译器会在类型擦除期间在其中插入强制类型转换。
...
String max = (String) findHighest(comp, list);
由于编译器始终知道必须插入哪个强制类型转换,因此在大多数情况下,类型擦除不会引起任何问题。但是,它有一些限制:instanceof
不起作用,catch (T exception)
是非法的(而throws T
是允许的,因为调用函数知道它必须期望什么样的异常),等等。您必须克服的限制是缺少 reifiable (=运行时可用的完整类型信息)通用类型(有一些例外,但在此情况下无关紧要)。
但是,等等,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
crossinline
关键字是有问题的,因为它阻止了编译器内联block
中声明的代码。因此,您传递给#suspendCoroutine
的lambda将被转移到匿名内部类中。从技术上讲,这是在运行时进行的。
因此,通用类型信息不再可用,至少在运行时不可用。
在您调用#jsonToObject(...)
时,类型变量T
被擦除为Object
。因此TypeToken
Gson生成如下:
TypeToken<HttpResponse<Object>>
更新:根据我的进一步研究,这是不正确的。 crossinline
不会阻止编译器内联lambda,而只是禁止它们影响函数的控制流。我可能将其与关键字noinline
混合使用,顾名思义,该关键字实际上禁止内联。
但是,我非常确定以下部分。但是,我仍然必须找出为什么Gson无法正确确定和/或反序列化类型。我会在了解更多信息后立即更新。
这使我们进入最后一部分,试图解释您收到的奇怪异常。为此,我们必须看看Gsons的内部。
内部,Gson具有负责反射序列化和反序列化的两种主要类型:TypeAdapterFactory
和TypeAdapter<T>.
TypeAdapter<T>
仅适应一种特定类型(=为该类型提供(反序列化)逻辑。这意味着Integer
,Double
,List<String>
和List<Float>
均由不同的TypeAdapter<T>
处理。
TypeAdapterFactory
负责提供匹配的TypeAdapter<T>
。 TypeAdapter<T>
和TypeAdapterFactory
之间的区别非常有用,因为一个工厂可能会创建所有适配器,例如像List
这样的集合类型,因为它们都以相似的方式工作。
为了确定您需要哪种适配器,Gson希望您在调用应处理通用类型的(反)序列化函数时传递TypeToken<T>
。 TypeToken<T>
使用“技巧”来访问传递给其type参数的类型信息。
一旦您致电Gson#fromJson(this, object: TypeToken<T>() {}.type)
,Gson就会遍历所有可用的TypeAdapterFactory
,直到找到可以提供适当TypeAdapter<T>
的地址为止。 Gson带有各种TypeAdapterFactory
,包括用于原始数据类型,包装器类型,基本集合类型,日期等的工厂。除此之外,Gson还提供了两个特殊工厂:
@Override public Object read(JsonReader in) throws IOException {
JsonToken token = in.peek();
switch (token) {
...
case BEGIN_OBJECT:
Map<String, Object> map = new LinkedTreeMap<String, Object>(); // <-----
in.beginObject();
while (in.hasNext()) {
map.put(in.nextName(), read(in));
}
in.endObject();
return map; // <-----
...
}
这就是为什么您得到ClassCastException
和com.google.gson.internal.LinkedTreeMap
的原因。