Kotlin泛型上界与扩展函数

泛型场景

先由我们熟悉的Java说起,有时在使用泛型的时候,会有这样的场景。
比如我们封装一个参数类,里面提供一个放入参数的方法,并且方法返回类型为它自身,以便调用时进行链式调用。

泛型递归和链式调用

为了让它的子类在调用时也返回它自身,我们定义一个泛型继承于它,并且返回值为这个泛型。如下:

public class Params<T extends Params> extends HashMap<String, Object> {
    protected final T putParams(String key, Object value) {
        put(key, value);
        return (T) this;
    }
}

然后我们的子类,如下声明参数:

public class OrderParams extends Params<OrderParams> {
   public OrderParams page(int page) {
        return putParams("page", page);
    }

    public OrderParams pageSize(int size) {
        return putParams("page_items", size);
    }
    // ...
}

调用的时候就可以实现链式调用了:

final OrderParams params = new OrderParams()
        .page(page)
        .pageSize(Constants.PAGE_SIZE)
        //...

这是第一种场景。

子类的泛型递归

在上面的前提下,比如我们发现有许多参数类都要声明一个共同的参数,于是我们继承自Params再封装一个类,并且还是使用递归泛型,使得子类调用这个类声明的方法的时候能够返回子类的类型,以便一路链式调用。如下:

public class ParkParams<T extends ParkParams> extends Params<T> {
    public final T park(String parkCode) {
        return putParams("park_code", parkCode);
    }
}

这是第二种场景。

直接使用父类

在项目里,还有另一种场景:某个接口声明里,我们只需要Params类,不需要添加其他参数,所以不需要再去声明一个它的子类。
比如我们使用Retrofit时,声明如下的接口:

    @POST("/iop/auth/logout")
    @NeedToken
    Observable<DataResponse> logout(@Body Params params);

这是一个注销退出的接口,需要传token,但是我们已经在实现了Converter<T, RequestBody>的类里实现了token的添加,这里我们只需要一个Params参数。
这是第三种场景。

这几种场景,使用Java如此实现是没有问题的,但是转成Kotlin代码问题却出来了。

Kotlin的问题与方式

使用时必须指定类型的泛型的问题

我们使用Idea的转成Kotlin代码的功能,将Params类转成Kotlin代码,如下:

open class Params<T : Params<*>> : HashMap<String, Any?>() {
    protected fun putParams(key: String, value: Any?): T {
        put(key, value)
        return this as T
    }
}

这是编译不过去的,因为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代码就好了:

open class ParkParams<T : ParkParams<T>> : Params<T>() {
    fun park(parkCode: String): T {
        return putParams("park_code", parkCode)
    }
}

第二种场景也没问题。

现在第三种场景的问题来了。
我们调用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的方式

我们回顾一下,其实只是想要三个诉求:

  1. Params类封装一个putParams(key, value)的方法,方法能够返回它的子类。
  2. Params的子类能够封装一些方法,方法能返回它的子类的子类。
  3. Params能够被直接调用及创建它的实例。

Kotlin做不到吗?不是的。如果是递归泛型是Java相对于Kotlin的特性,那Kotlin也有相对于Java的语言特性可以实现以上的需求,那就是扩展函数。

现在我们使用扩展函数来实现前面的功能。
首先,我们的Params也不需要声明泛型了,它定义如下:

open class Params : HashMap<String, Any?>() {
    fun <T : Params> T.putParams(key: String, value: Any?): T {
        put(key, value)
        return this
    }
}

由于Params没有泛型参数,所以第三个种场景也就解决了。然后我们在Params类里声明一个扩展函数,在这里我们使用了泛型。我们让这个泛型有Params上界,返回值类型是这个泛型。这样第一种场景也解决了。

对于第二种场景,也是使用扩展函数来实现,但是这里的扩展函数需要声明在类的外面,否则无法调用到这个函数。

open class ParkParams : Params()

fun <T : ParkParams> T.park(parkCode: String) = putParams("park_code", parkCode)

这样问题就解决了。
对比Java,我们也可以看到,Kotlin的实现看起来更简洁清晰。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值