协程内部的通用泛型参数不起作用 [英] Reified generic parameter inside coroutine is not working

查看:80
本文介绍了协程内部的通用泛型参数不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am creating http json client. I am using Volley in combination with coroutines. I wanted to create generic http client so I can use it everywhere.

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

I have created generic extension method to parse JSON string into object.

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无法转换为令牌"结束.

Problem is that when I call this method I don't get expected result. First call gives proper result. Object is initialized. But second call, where I use generic parameter which is passed to method, ends with exception "LinkedTreeMap can not be cast into 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数据类的外观.

This is how httpresponse data class looks.

data class HttpResponse<T> (
val response: T
)

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

I saw a workaround here using Type::class.java but I don't like this approach and I would like to use reified and inline keywords. How does the reified keyword in Kotlin work?

更新这是我得到的例外.

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

java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xbionicsphere.x_card.entities.Token

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

POSSIBLE WORKAROUND I found possible workaround. If I create method which will parse Token into from response and use this method in executeRequestAsync everything starts working but I don't like this solution since I have to add additional parameter for each request.

新的登录异步

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需要通过通用参数提供的最终类型定义,因此我增强了方法的声明.现在方法声明如下:

UPDATE I probably have found working solution. executeRequestAsync needs final type definition provided through generic parameters so I enhanced declaration of method. Now method declaration looks like this:

    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)
        }
    }
}

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

Thanks this complicated function declaration I can execute request with this call:

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

推荐答案

为了理解第二个呼叫为什么表现得有些奇怪,以及为什么按照Leo Aso的建议,删除了关键字 inline reified (需要一个不可插入的函数)也会中断第一个调用,您必须了解类型擦除以及 reified 首先如何启用类型化.

In order to understand why the second call behaves kind of strangely and why, as proposed by Leo Aso, removing the keywords inline and reified (which requires an inlinable function) also breaks the first call, you have to understand type erasure and how reified enables type reification first.

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

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

The type parameter of a generic function isn't available at runtime; Generics are a "compile-time trick" only. This applies to both, Java and Kotlin (since Kotlin is able to run on the JVM). The process in which the generic type information is removed is called type erasure and happens during compilation. So how do generic functions work at runtime? Consider the following function which returns the most valuable element of an arbitrary collection.

<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 的值可能会随时间变化.为了确保它们全部兼容,在擦除类型时会对该函数进行重构.完成类型擦除后,该函数看起来将与此类似:

As this function can be invoked with many different kinds of collections etc, the value of the type variable T might vary over time. To ensure compatiblity with all of them, the function gets refactored during type erasure. After type erasure has completed, the function will look somehow similar to this:

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 .参数化通常不会保留其泛型类型信息.

During type erasure, type variables are replaced with their bound. In this case, the bound type is Object. Parameterized don't keep their generic type information generally.

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

However, if you'd compile the erased code, some problems will occur. Consider the following code (unerased) which calls the erased one:

Comparator<CharSequence> comp = ...
List<String> list = ...
String max = findHighest(comp, list);

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

As #findHighest(Comparator, Collection) now returns Object, the assignment in line 3 would be illegal. The compiler therefore inserts a cast there during type erasure.

...
String max = (String) findHighest(comp, list);

由于编译器始终知道必须插入哪个强制类型转换,因此在大多数情况下,类型擦除不会引起任何问题.但是,它有一些限制: instanceof 不起作用, catch(T异常)是非法的(而自 thing T 调用函数知道它必须期望什么样的异常),等等.您必须克服的限制是缺少 reifiable (=运行时可用的完整类型信息)通用类型(有一个很少有例外,但在此情况下无关紧要).

As the compiler always knows which cast it has to insert, type erasue doesn't cause any problems in most cases. However, it comes with a few restrictions: instanceof doesn't work, catch (T exception) is illegal (whereas throws T is allowed since the calling function knows what kind of exception it has to expect), etc. The restriction you had to fight with is the lack of reifiable (= full type information available at runtime) generic types (there are a few exceptions, but they do not matter in this context).

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

But wait, Kotlin has support for reified types, right? That's true, but as I've mentioned earlier, this is only true for inlinable functions. But why is that?

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

When a function which signature contains the keyword inline is invoked, the invoking code is replaced with the code of this function. As the "copied" code no longer has to be compatible with all kinds of types, it can be optimized for the context it is used in.

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

One possible optimization is to replace the type variables in the "copied code" (there's a lot more happening under the hood) before type erasure is done. The type information is therefore preserved and also available at runtime; it is indistinguishable from any other non-generic code.

尽管您的两个函数 #jsonToObject(ExclusionStrategy?) #sendRequestAsync(String,Any ?, Int,Token?)难以理解且具有可更改的类型参数,您仍然缺少一些东西: T 是,至少在您对 #toJsonObject(ExclusionStrategy?)的调用中,不可更改.

Although both of your functions, #jsonToObject(ExclusionStrategy?) and #sendRequestAsync(String, Any?, Int, Token?), are marked as inlinable and have reifiable type parameters, there's still something you've missed: T is, at least in your call to #toJsonObject(ExclusionStrategy?), NOT reifiable.

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

One reason for this is your call to #suspendCoroutine(...). To understand why this is a problem, we have to look at its declaration first:

suspend inline fun <T> suspendCoroutine(
    crossinline block: (Continuation<T>) -> Unit
): T

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

The crossinline-keyword is problematic as it stops the compiler from inlining the code that is declared inside block. The lambda you pass to #suspendCoroutine will therefore be transfered into an anonymous inner class. Technically, this happens under the hood at runtime.

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

The generic type information is therefore NOT available anymore, at least not at runtime. At the point where you invoke #jsonToObject(...), the type variable T is erased to Object. The TypeToken Gson generates therefore looks like this:

TypeToken<HttpResponse<Object>>

更新:根据我的进一步研究发现,这是不正确的. crossinline 不会阻止编译器内联lambda,而只是禁止它们影响函数的控制流.我可能将其与关键字 noinline 混合使用,顾名思义,该关键字实际上禁止内联.

Update: This is, as I've found after some further research, NOT true. crossinline doesn't stop the compiler from inlining lambdas, it just forbids them to influence the function's control flow. I probably mixed it up with the keyword noinline, which, as the name implies, actually forbids inlining.

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

However, I'm pretty sure about the following part. However, I still have to find out why Gson is unable to determine and/or to deserialize the type correctly. I'll update this post as soon as I know more.

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

That brings us to the final part which tries to explain the weird exception you received. For that, we have to take a look at Gsons' internals.

内部,Gson具有负责反射序列化和反序列化的两种主要类型: TypeAdapterFactory TypeAdapter< T>.

Internally, Gson has two main types that are responsible for reflective serialization and deserialization: TypeAdapterFactory and TypeAdapter<T>.

TypeAdapter< T> 仅适应一种特定类型(为该类型提供(反序列化)逻辑).这意味着 Integer Double List< String> List< Float> 均由不同的 TypeAdapter< T> s.

A TypeAdapter<T> only adapts (= provides the (de-)serialization logic for) one specific type. This means that Integer, Double, List<String> and List<Float> are all handled by different TypeAdapter<T>s.

TypeAdapterFactory 负责提供匹配的 TypeAdapter< T> . TypeAdapter< T> s和 TypeAdapterFactory s之间的区别非常有用,因为一个工厂可能会创建所有适配器,例如像 List 这样的集合类型,因为它们的工作方式都相似.

TypeAdapterFactorys are responsible for, as their names already imply, providing matching TypeAdapter<T>s. The differentiation between TypeAdapter<T>s and TypeAdapterFactorys is extremely useful as one factory might create all adapters for e.g. a collection type like List as they all work in a similar way.

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

In order to determine what kind of adapter you need, Gson expects you to pass a TypeToken<T> when calling a (de-)serialization function which should process a generic type. TypeToken<T> uses a "trick" to access the type information passed to its type parameter.

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

As soon as you call Gson#fromJson(this, object: TypeToken<T>() {}.type), Gson iterates through all available TypeAdapterFactorys until it finds one that can provide an appropriate TypeAdapter<T>. Gson comes with a variety of TypeAdapterFactorys, including factories for primitive data types, wrapper types, basic collection types, date and many more. Besides that, Gson provides two special factories:

  • 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;   // <-----
    ...
  }

这就是为什么您使用 com.google.gson.internal.LinkedTreeMap 来获得 ClassCastException 的原因.

That's why you get a ClassCastException with a com.google.gson.internal.LinkedTreeMap.

这篇关于协程内部的通用泛型参数不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆