5.7 Kotlin 中的泛型

作者: 常思行 | 来源:发表于2018-06-01 14:55 被阅读106次

    一、泛型基础

    泛型编程包括,在不指定代码中使用到的确切类型的情况下来编写算法。用这种方式,我们可以创建函数或者类型,唯一的区别只是它们使用的类型不同,提高代码的可重用性。这种代码单元就是我们所知道的泛型,它们存在于很多的语言之中,包括Java和Kotlin。

    在Kotlin中,泛型甚至更加重要,因为经常使用扩展函数将会成倍增加我们泛型使用频率。尽管我们已经在本书中盲目地使用了泛型,但是泛型在任何语言中通常都是比较困难的一部分,所以我尝试使用尽可能简单的方式来讲解它,这样主要的思想也会足够地清晰。

    举个例子,我们可以创建一个指定泛型类:

    class TypedClass<T>(parameter: T) {
        val value: T = parameter
    }
    

    这个类现在可以使用任何的类型初始化,并且参数也会使用定义的类型,我们可以这么做:

    val t1 = TypedClass<String>("Hello World!")
    val t2 = TypedClass<Int>(25)
    

    但是Kotlin很简单并且缩减了模版代码,所以如果编译器能够推断参数的类型,我们甚至也就不需要去指定它:

    val t1 = TypedClass("Hello World!")
    val t2 = TypedClass(25)
    val t3 = TypedClass<String?>(null)
    

    如第三个对象接收一个null引用,那仍然还是需要指定它的类型,因为它不能去推断出来。

    我们可以像Java中那样在定义中指定的方式来增加类型限制。比如,如果我们想限制上一个类中为非null类型,我们只需要这么做:

    class TypedClass<T : Any>(parameter: T) { 
        val value: T = parameter
    }
    

    如果你再去编译前面的代码,你将看到t3现在会抛出一个错误。可null类型不再被允许了。但是限制明显可以更加严厉。如果我们只希望Context的子类该怎么做?很简单:

    class TypedClass<T : Context>(parameter: T) { 
        val value: T = parameter
    }
    

    现在所有继承Context的类都可以在我们这个类中使用。其它的类型是不被允许的。

    当然,可以使用函数中。我们可以相当简单地构建泛型函数:

    fun <T> typedFunction(item: T): List<T> {
        ...
    }
    

    二、泛型变体

    这是真的是最难理解的部分之一。在Java中,当我们使用泛型的时候会出现问题。逻辑告诉我们List<String>应该可以转型为List<Object>,因为它有更弱的限制。但是我们来看下这个例子:

    List<String> strList = new ArrayList<>();
    List<Object> objList = strList;
    objList.add(5);
    String str = objList.get(0);
    

    如果Java编译器允许我们这么做,我们可以增加一个Integer到Object List,但是它明显会在某一时刻奔溃。这就是为什么语言中增加了通配符。通配符可以在限制这个问题中可以增加灵活性。

    如果我们增加了? extends Object,我们使用了协变(covariance),它表示我们可以处理任何使用了类型,比Object更严格的对象,但是我们只有使用get操作时是安全的。如果我们想去拷贝一个Strings集合到Objects集合中,我们应该是允许的,对吧?然后,如果我们这样:

    List<String> strList = ...;
    List<Object> objList = ...;
    objList.addAll(strList);
    

    这样是可以的,因为定义在Collection接口中的addAll()是这样的:

    List<String>
    interface Collection<E> ... {
        void addAll(Collection<? extends E> items);
    }
    

    否则,没有通配符,我们不会允许在这个方法中使用String List。相反地,当然会失败。我们不能使用addAll()来增加一个Objects List到Strings List中。因为我们只是用那个方法从collection中获取元素,这是一个完美的协变(covariance)的例子。

    另一方面,我们可以在对立面上发现逆变(contravariance)。按照集合的例子,如果我们想把传过来的参数增加到集合中去,我们可以增加更加限制的类型到泛型集合中。比如,我们可以增加Strings到ObjectList:

    void copyStrings(Collection<? super String> to, Collection<String> from) {
        to.addAll(from);
    }
    

    增加Strings到另一个集合中唯一的限制就是那个集合接收Strings或者父类。

    但是通配符都有它的限制。通配符定义了使用场景变体(use-site variance),这意味着当我们使用它的时候需要声明它。这表示每次我们声明一个泛型变量时都会增加模版代码。

    让我们看一个例子,使用我们之前相似的类:

    class TypedClass<T> {
        public T doSomething(){
            ...
        }
    }
    

    这些代码不会被编译:

    TypedClass<String> t1 = new TypedClass<>();
    TypedClass<Object> t2 = t1;
    

    尽管它的确没有意义,因为我们仍然保持了类中的所有的方法并且没有任何损坏。我们需要指定的类型可以有一个更加灵活的定义。

    TypedClass<String> t1 = new TypedClass<>();
    TypedClass<? extends String> t2 = t1;
    

    这会让代码更加难以理解,而且增加了一些额外的模版代码。

    另一方面,Kotlin通过内部声明变体(declaration-site variance)可以使用更加容易的方式来处理。这表示当我们定义一个类或者接口的时候我们可以处理弱限制的场景,我们可以在其它地方直接使用它。

    所以让我们看看它在Kotlin中是怎么工作的。相比冗长的通配符,Kotlin仅仅使用out来针对协变(covariance)和使用in来针对逆变(contravariance)。在这个例子中,当我们类产生的对象可以被保存到弱限制的变量中,我们使用协变。我们可以直接在类中定义声明:

    class TypedClass<out T>() {
        fun doSomething(): T {
            ...
        }
    }
    

    这就是所有我们需要的。现在,在Java中不能编译的代码在Kotlin中可以完美运行:

    val t1 = TypedClass<String>()
    val t2: TypedClass<Any> = t1
    

    如果你已经使用了这些概念,我确信你可以很简单地在Kotlin使用in和out。否则,你也只是需要一些联系和概念上的理解。

    三、泛型例子

    理论之后,我们转移到一些实际功能上面,这会让我们更加简单地掌握它。为了不重复发明轮子,我使用三个Kotlin标准库中的三个函数。这些函数让我们仅使用泛型的实现就可以做一些很棒的事情。它可以鼓舞你创建自己的函数。

    1、let

    let实在是一个简单的函数,它可以被任何对象调用。它接收一个函数(接收一个对象,返回函数结果)作为参数,作为参数的函数返回的结果作为整个函数的返回值。它在处理可null对象的时候是非常有用的,下面是它的定义:

    inline fun <T, R> T.let(f: (T) -> R): R = f(this)
    

    它使用了两个泛型类型:T 和 R。第一个是被调用者定义的,它的类型被函数接收到。第二个是函数的返回值类型。

    我们怎么去使用它呢?你可能还记得当我们从数据源中获取数据时,结果可能是null。如果不是null,则把结果映射到domain model并返回结果,否则直接返回null:

    if (forecast != null) dataMapper.convertDayToDomain(forecast) else null
    

    这代码是非常丑陋的,我们不需要使用这种方式去处理可null对象。实际上如果我们使用let,都不需要if:

    forecast?.let { dataMapper.convertDayToDomain(it) }
    

    对亏?.操作符,let函数只会在forecast不是null的时候才会执行。否则它会返回null。也就是我们想达到的效果。

    2、with

    本书中我们大量讲了这个函数。with接收一个对象和一个函数,这个函数会作为这个对象的扩展函数执行。这表示我们根据推断可以在函数内使用this。

    inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()
    

    泛型在这里也是以相同的方式运行:T代表接收类型,R代表结果。如你所见,函数通过f: T.() -> R声明被定义成了扩展函数。这就是为什么我们可以调用receiver.f()。

    通过这个app,我们有几个例子:

    fun convertFromDomain(forecast: ForecastList) = with(forecast) {
        val daily = dailyForecast map { convertDayFromDomain(id, it) }
        CityForecast(id, city, country, daily)
    }
    

    3、apply

    它看起来于with很相似,但是是有点不同之处。apply可以避免创建builder的方式来使用,因为对象调用的函数可以根据自己的需要来初始化自己,然后apply函数会返回它同一个对象:

    inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }
    

    这里我们只需要一个泛型类型,因为调用这个函数的对象也就是这个函数返回的对象。一个不错的例子:

    val textView = TextView(context).apply {
        text = "Hello"
        hint = "Hint"
        textColor = android.R.color.white
    }
    

    它创建了一个TextView,修改了一些属性,然后赋值给一个变量。一切都很简单,具有可读性和坚固的语法。让我们用在当前的代码中。在ToolbarManager中,我们使用这种方式来创建导航drawable:

    private fun createUpDrawable() = with(DrawerArrowDrawable(toolbar.ctx)) {
        progress = 1f
        this
    }
    

    使用with和返回this是非常清晰的,但是使用apply可以更加简单:

    private fun createUpDrawable() = DrawerArrowDrawable(toolbar.ctx).apply {
        progress = 1f
    }
    

    你可以在Kotlin for Android Developer代码库中查看这些小的优化。

    点此进入:GitHub开源项目“爱阅”

    感谢优秀的你跋山涉水看到了这里,欢迎关注下让我们永远在一起!

    相关文章

      网友评论

      本文标题:5.7 Kotlin 中的泛型

      本文链接:https://www.haomeiwen.com/subject/iecksftx.html