泛型
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中 Any
和Any?
是不同的类型,因此T
和T?
定义类型参数时意义也是不一样的。并且在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推荐我们在下列场景时使用实化类型参数:
- 用在类型检查和类型转换中(is/!is/as/as?)
- 使用kotlin反射API (::class)
- 获取相应的java.lang.Class (::class.java)
- 作为调用其他函数的类型实参
协变(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有下面两层含义:
- 子类型化会被保留
- 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()
网友评论