Kotlin泛型与DSL重点记录

作者: susion哒哒 | 来源:发表于2018-09-28 20:31 被阅读10次

泛型

kotlin的泛型语法与java类似,比如声明一个泛型类:

interface List<T>{
    operator fun get(index:Int):T
}

类型参数约束

类似于java中的entends关键字,在kotlin中可以把冒号放在类型参数名称之后,作为类型形参上界的类型紧随其后:

fun <T : Number> List<T>.sum():T

在kotlin中还可以为一个类型参数指定多个约束,比如:

fun <T> ensureTraillingPeriod(seq:T) where T:CharSequence, T:Appendable{
    if(!seq.startWith('.')){
        seq.append('.')
    }
}

由于kotlin中 AnyAny?是不同的类型,因此TT?定义类型参数时意义也是不一样的。并且在kotlin中,如果你没有指定类型形参的上界,默认使用Any?作为上界。

` <T : Any> ` 这个就确保了T永远是非空类型。

泛型擦除

java中的泛型是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的。kotlin的泛型在运行时也被擦除了。即下面这个代码是编译不过的:

if(value is List<String>)  //编译错误,这是因为在运行时,是没有String这个类型实参的。 

泛型擦除的好处是可以降低应用程序使用的内存,因为保存在内存中的类型信息更少。

看下面这个函数,来理解一下类型擦除:

//这个函数式可以正常通过编译的
fun printSum(c:Collection<*>){ // * 表示不确定是哪种类型
    val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
    println(intList.sum())
}

printSum(setOf(1,2,3))  //IllegalArgumentException -> Set不是List
printSum(listOf("a", "b", "c")) //ClassCastException -> String转Int失败

对于第二个字符串列表的调用,这是因为没有办法判断实参是不是一个List<Int>(运行时被擦除了),因此类型转换会成功,接下来调用inList.sum()时,由于sum函数会把list中的元素转换为Int类型,因此就会报
ClassCastException.

下面这个函数就是ok的,这是因为编译器是很智能的,在编译阶段就能杜绝类型转换失败的问题:

fun printSum(c:Collection<Int>){
    if(c is List<Int>) print(c.sum())
}

声明带实化类型参数的函数

由于泛型擦除,在调用泛型函数的时候,在函数体中你不能决定调用它用的类型实参,比如:

fun <T> isA(value:Any) = value is T  //编译失败

但是内联函数+reified可以解决这个问题,由于编译器在编译内联函数时,会把每一次函数调用都转换为函数的实际代码实现,所有编译器知道每次调用中用作类型实参的确切类型,因此,编译器可以生成引用作为类型实参的具体类的字节码。
把上面代码这样一改就可以通过编译:

inline fun <reified T> isA(value:Any) = value is T
-> print(isA<String>("q"))  true
-> print(isA<Int>("a"))     false

但需要注意,这种函数不能在java中调用。

一个简单的简化startActivity的例子:

inline fun <reified T : Activity> Context.startActivity(){
    startActivity(Intent(this, T::class.java))
}

-> startActivity<DetailActivity>()

kotlin推荐我们在下列场景时使用实化类型参数:

  1. 用在类型检查和类型转换中(is/!is/as/as?)
  2. 使用kotlin反射API (::class)
  3. 获取相应的java.lang.Class (::class.java)
  4. 作为调用其他函数的类型实参

协变(out)与逆变(in)

在kotlin的泛型的概念里,如果A是B的子类型,那么List<A>就是List<B>的子类型,这样的类或者接口就是协变的。

假如有下面这个函数:

open class Animal

class Cat : Animal()

class Herd<T : Animal>

fun flap(animals: Herd<Animal>) {
    //...
}

flap(animals)
flap(cats) //编译报错, 这是因为 Herd<Cat> 并不是 Herd<Animal>的子类型。

我们可以在声明泛型时使用out关键字来解决上面这个问题, class Herd<out T : Animal>, 这样就说明Herd这个类是协变的。

out关键字要求所有使用T的方法只能把T放在out位置(返回值)而不能放在in位置(参数)上。如果对一个类使用的out,说明这个类只能生产类型T(返回)而不能消费他们(作为参数)。即类型T上的out有下面两层含义:

  1. 子类型化会被保留
  2. T只能用在out位置(返回值)

什么是out位置,什么是in位置
比如一个类,它声明了一个类型参数T并包含了一个使用T的函数,如果函数时把T当成返回类型,我们说它在out位置,这种情况下,该函数生产类型为T的值,如果T用在函数参数的类型,它就在in位置。这样的函数消费类型为T的值。

为什么协变的泛型只能放在out位置呢?
假如可以放在in位置, 还是上面Herd的例子,如果animals 为Herd<Cat>, 假如在flap函数中,把Herd<Cat>变为了Herd<Dog>并返回,那么使用者接下来就可能报错,因此,out约束保证了子类型关系的安全性。

构造方法的参数既不在in位置,也不再out位置

看一个带out投影类型参数的数据拷贝函数:

//这里可以给类型的用法加上"out"关键字:没有使用那些T用在"in"位置的方法
fun <T> copyData(source:MutableList<out T>, des:MutableList<T>){
    for(item in source){
        des.add(item)
    }
}

//如果不写out,这里是无法返回成功的
fun getAnimal(type:String):List<out Animal>{
    return List<Cat>()
}
  • 与协变相反的逆变in

逆变的概念可以被看成是协变的镜像,具体是这样定义的:如果B是A的子类型,那么Consumer<A>就是Consumer<B>的子类型,类型A和B交换了位置,所有说子类型化被反转了。

星号投影:使用"*"代替类型参数

一个包含未知类型的元素的列表可以用List<*>表示。

MutableList< * >与MutableList<Any?>是不一样的,MutableList<Any?>包含的是任意类型的元素,而 MutableList< * >包含的是某种特定类型的元素列表,但不知道是哪个类型。

举个例子:

val list1 = listOf('a', 2, "qq")
val unknowTypeList:MutableList<*> = list1

在这个例子中编译器会把 MutableList<*> 当成 out投影类型,即你只能在这个列表上读取元素,而不能写入元素。

星号投影的语法很简洁,但只能用在对泛型类型实参的确切值不感兴趣的地方:只是使用生产值的方法,而不关心那些值的类型

kotlin DSL

这里不去看kotlin中的DSL的具体内容,我们只看一个组成kotlin DSL的非常基本的特性:

DSL中带接收者的lambda

先来看一个普通的例子:

fun buildString(builderAction: (StringBuilder) -> Unit):String{
    val sb = StringBuilder()
    builderAction(sb)
    return sb.toString
}

val s = buildString{
    it.append("a")
    it.append("b")
}

上面是定义了一个接收函数类型的函数builderAction, 上面我们直接传递了一个lambda表达式,并且使用kotlin提供的it来访问唯一的StringBuilder参数。

但是有没有感觉到,每次都使用it很麻烦? 可不可以直接调用append方法,就是作用在StringBuilder参数上?其实这个kotlin是支持的,只需要将lambda转换成带接收者的lambda,我们来改写上面的实现:

fun buildString(builderAction:StringBuilder.()->Unit):String{
    val sb = StringBuilder()
    sb.builderAction()  // -> builderAction(sb)  也可以这样调用
    return sb.toString()
}

val s = builderAction{
    append("a")
    append("b")
}

可以看到上面我们使用: StringBuilder().->Unit 代替了 (StringBuilder) ->Unit,这个特殊的类型就叫做 接受者类型 或 这是一个 扩展函数类型 : 接收者类型是String,没有参数,返回值是Unit。

一个扩展函数类型描述了一个尅被当做扩展函数调用的代码块 (对于扩展函数,我们可以直接在函数中,调用扩展类的属性或方法,这个是我们已知的)。

我们知道apply函数就是带接收者的函数,我们看看这个函数是怎么声明的:

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

inline fun <T, R> with(reveiver:T, block:T.()->R):R = reveiver.block()

相关文章

  • Kotlin泛型与DSL重点记录

    泛型 kotlin的泛型语法与java类似,比如声明一个泛型类: 类型参数约束 类似于java中的entends关...

  • 泛型

    与Java泛型相同,Kotlin同样提供了泛型支持。对于简单的泛型类、泛型函数的定义,Kotlin 与 Java ...

  • 【Android】 Kotlin(七)泛型

    深入理解Kotlin泛型 Kotlin 的泛型与 Java 一样,都是一种语法糖,即只在源代码中有泛型定义,到了c...

  • Kotlin---泛型

    Kotlin不变型泛型 Kotlin的不变型泛型和Java一样,通过声明泛型类型来使用泛型类。而该种泛型声明后,则...

  • 泛型、型变与投影

    简单泛型 kotlin 对于简单泛型的支持与java类似, 可以通过通配符,提升代码的灵活度 限定型泛型 在编码实...

  • Kotlin 泛型 VS Java 泛型

    建议先阅读我的上一篇文章 -- Java 泛型 和 Java 泛型一样,Kotlin 泛型也是 Kotlin 语言...

  • Kotlin 之 DSL 篇一

    Kotlin DSL 什么是DSL Domain Special Language DSL是领域特定语言,与通用语...

  • Kotlin for android学习六:泛型

    前言 kotlin官网和kotlin教程学习教程的笔记。 1. 声明泛型 2. 泛型约束 : 对泛型的类型上限进行...

  • Java泛型与Kotlin泛型

    正文 本文主要列举Java泛型与Kotlin泛型的基本知识,以及两者的区别。 什么泛型 泛型程序设计是程序设计的一...

  • Kotlin之美——DSL篇

    Kotlin 系列:Kotlin之美——高效篇Kotlin之美——DSL篇 Kotlin DSL 把 Kotlin...

网友评论

    本文标题:Kotlin泛型与DSL重点记录

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