泛型场景
先由我们熟悉的Java说起,有时在使用泛型的时候,会有这样的场景。
比如我们封装一个参数类,里面提供一个放入参数的方法,并且方法返回类型为它自身,以便调用时进行链式调用。
泛型递归和链式调用
为了让它的子类在调用时也返回它自身,我们定义一个泛型继承于它,并且返回值为这个泛型。如下:
1 | public class Params<T extends Params> extends HashMap<String, Object> { |
然后我们的子类,如下声明参数:
1 | public class OrderParams extends Params<OrderParams> { |
调用的时候就可以实现链式调用了:
1 | final OrderParams params = new OrderParams() |
这是第一种场景。
子类的泛型递归
在上面的前提下,比如我们发现有许多参数类都要声明一个共同的参数,于是我们继承自Params
再封装一个类,并且还是使用递归泛型,使得子类调用这个类声明的方法的时候能够返回子类的类型,以便一路链式调用。如下:
1 | public class ParkParams<T extends ParkParams> extends Params<T> { |
这是第二种场景。
直接使用父类
在项目里,还有另一种场景:某个接口声明里,我们只需要Params
类,不需要添加其他参数,所以不需要再去声明一个它的子类。
比如我们使用Retrofit时,声明如下的接口:
1 |
|
这是一个注销退出的接口,需要传token,但是我们已经在实现了Converter<T, RequestBody>
的类里实现了token的添加,这里我们只需要一个Params
参数。
这是第三种场景。
这几种场景,使用Java如此实现是没有问题的,但是转成Kotlin代码问题却出来了。
Kotlin的问题与方式
使用时必须指定类型的泛型的问题
我们使用Idea的转成Kotlin代码的功能,将Params
类转成Kotlin代码,如下:
1 | open class Params<T : Params<*>> : HashMap<String, Any?>() { |
这是编译不过去的,因为Kotlin的泛型不能省略,所以原来Java代码里泛型里面的<T extends Params>
就被转成了<T : Params<*>>
,但Params
已经约束了是有上界的,所以不能使用*
,我们改为class Params<T : Params<T>>
。这样看起来第一种场景是通过了。
但是我们刚才的ParkParams
报错了,因为我们声明的是public class ParkParams<T extends ParkParams> extends Params<T>
,但它需要T
继承自Params
。没关系,如上转为Kotlin代码就好了:
1 | open class ParkParams<T : ParkParams<T>> : Params<T>() { |
第二种场景也没问题。
现在第三种场景的问题来了。
我们调用logout(params)
需要传一个Params
参数,把调用代码转为Kotlin之后报错了,因为Params没有指定泛型,推断出来的是Params<*>
,它想要的是Params<Params<*>>
。但是我们把它改为Params<Params<*>>
,里面的<params<*>>
又报了这一个问题。好吧,这对Kotlin而言,是个无穷无尽的泛型递归问题。
这里的问题就在于,logout(params)
接口是用Java声明的,没有指定泛型参数的类型,所以Kotlin这边推断出来的是Params<*>
,而由于Params
的泛型有其上界,所以Kotlin创建不了这个类型的实例。
我们把logout(params)
接口的代码也转成kotlin,会发现接口的参数类型只能声明为Params<*>
才能编译通过。但是会有两个问题:
一是使用的时候,上面的问题还是没有解决。所以只能传Params
的其他子类,比如传个前面定义的OrderParams
。暂时忽略这个不可理喻的调用,至少编译通过了。
二是运行的时候Retrofit报错了,因为它不允许参数的泛型类型为通配符。<*>
就相当于Java中的<?>
。
所以Kotlin使用泛型来解决这些问题是走不通的。
Kotlin的方式
我们回顾一下,其实只是想要三个诉求:
Params
类封装一个putParams(key, value)
的方法,方法能够返回它的子类。Params
的子类能够封装一些方法,方法能返回它的子类的子类。Params
能够被直接调用及创建它的实例。
Kotlin做不到吗?不是的。如果是递归泛型是Java相对于Kotlin的特性,那Kotlin也有相对于Java的语言特性可以实现以上的需求,那就是扩展函数。
现在我们使用扩展函数来实现前面的功能。
首先,我们的Params也不需要声明泛型了,它定义如下:
1 | open class Params : HashMap<String, Any?>() { |
由于Params
没有泛型参数,所以第三个种场景也就解决了。然后我们在Params
类里声明一个扩展函数,在这里我们使用了泛型。我们让这个泛型有Params
上界,返回值类型是这个泛型。这样第一种场景也解决了。
对于第二种场景,也是使用扩展函数来实现,但是这里的扩展函数需要声明在类的外面,否则无法调用到这个函数。
1 | open class ParkParams : Params() |
这样问题就解决了。
对比Java,我们也可以看到,Kotlin的实现看起来更简洁清晰。