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

时间:2019-09-16 20:11:11

标签: java generics kotlin coroutine

我正在创建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)
}

1 个答案:

答案 0 :(得分:6)

为了理解为什么第二个调用的行为有点奇怪,以及为什么按照Leo Aso的建议,删除关键字inlinereified(需要一个不可插入的函数)也会中断第一个调用,您必须了解类型擦除以及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具有负责反射序列化和反序列化的两种主要类型:TypeAdapterFactoryTypeAdapter<T>.

TypeAdapter<T>仅适应一种特定类型(=为该类型提供(反序列化)逻辑。这意味着IntegerDoubleList<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还提供了两个特殊工厂:

  • ReflectiveTypeAdapterFactory顾名思义,该工厂尝试以反射方式访问对象的数据。为了适当地适应每个字段的类型,它为每个字段请求一个匹配的TypeAdapter。将为(反序列化HttpRequest)选择该工厂。
  • ObjectTypeAdapter.Factory此工厂仅返回ObjectTypeAdapter。下面的代码片段显示了它在对象反序列化方面的作用(分别是HttpRequest对象中的字段):
@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;   // <-----
    ...
  }

这就是为什么您得到ClassCastExceptioncom.google.gson.internal.LinkedTreeMap的原因。