kotlin值得系列5、关于kotlin泛型,也许值得看的文章

345 阅读6分钟

  • 泛型的基本概念
  • 泛型约束
  • 泛型的型变
  • UnsafeVariance
  • 星投影Start Projection
  • 泛型的实现原理与内联特化

一、泛型的基本概念

泛型,即 "参数化类型",将类型参数化,可以用在类,接口,方法上。

与 Java 一样,Kotlin 也提供泛型,为类型安全提供保证,消除类型强转的烦恼。

  • 创建类的实例时我们需要指定类型参数
  • kt泛型可以自动推导类型参数,省略类型参数

原理:类型擦除

  • 伪泛型:编译时类型擦除,运行时无实际类型生成。比如Java,Kotlin
  • 真泛型:编译时生成真实类型运行时也存在该类,比如C#,C++

创建类的实例时我们需要指定类型参数

class Box<T>(t : T) {
    var value = t
}

fun main(args: Array<String>) {
    var boxInt = Box<Int>(10)
    var boxString = Box<String>("Hello")

    println(boxInt.value)
    println(boxString.value)
}

输出:
10
Hello

kt泛型可以自动推导类型参数,省略类型参数

fun <T> boxIn(value: T) = Box(value)

// 以下都是合法语句
val box4 = boxIn<Int>(1)
val box5 = boxIn(1)     // 编译器会进行类型推断

再来一个

fun main(args: Array<String>) {
    val age = 23
    val name = "runoob"
    val bool = true

    doPrintln(age)    // 整型
    doPrintln(name)   // 字符串
    doPrintln(bool)   // 布尔型
}

fun <T> doPrintln(content: T) {

    when (content) {
        is Int -> println("整型数字为 $content")
        is String -> println("字符串转换为大写:${content.toUpperCase()}")
        else -> println("T 不是整型,也不是字符串")
    }
}

输出:
整型数字为 23
字符串转换为大写:RUNOOB
T 不是整型,也不是字符串

二、泛型约束

  • 我们可以使用泛型约束来设定一个给定参数允许使用的类型。

  • Kotlin 中使用 : 对泛型的类型上限进行约束。最常见的约束是上界(upper bound)。

  • Kotlin 泛型更加简洁安全,但是和 Java 一样都是有类型擦除的,都属于编译时泛型

Java 使用 extends 关键字指明上界。
在 Kotlin 中使用:代替 extends 对泛型的的类型上界进行约束。

来个泛型约束的简单例子

fun <T : Number> sum(vararg param: T) = param.sumByDouble { it.toDouble() }

fun main() {

    val result1 = sum(1,10,0.6)
    val result2 = sum(1,10,0.6,"kotlin") // compile error
}

Kotlin 默认的上界是Any?。Any 类似于 Java 中的 Object,它是所有非空类型的超类型。但是 Any 不能保存 null 值,如果需要 null 作为变量的一部分,则需要使用Any?Any?是 Any 的超类型,所以 Kotlin 默认的上界是Any?

对于多个上界约束条件,可以用 where 子句:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

三、泛型的型变

type variance(型变)

Kotlin 中没有通配符类型,它有两个其他的东西:

  • 声明处型变(declaration-site variance)
  • 类型投影(type projections)。

型变是指我们是否允许对参数类型进行子类型转换。

举个例子你就明白了,假设Orange类是Fruit类的子类,Crate 是一个泛型类,那么,Crate<Orange>Crate<Fruit> 的子类型吗? 直觉可能告诉你,Yes。但是,答案是No。

对于Java而言,两者没有关系。
对于Kotlin而言,Crate<Orange>可能是Crate<Fruit>的子类型,或者其超类型,或者两者没有关系。
这取决于Crate<T>中的 T 在类Crate中是如何使用的。简单来说,型变就是指 Crate<Orange>Crate<Fruit> 是什么关系这个问题,对于不同的答案,有如下几个术语。

  • invariance(不型变) :也就是说,Crate<Orange>Crate<Fruit> 之间没有关系。

  • covariance(协变) :也就是说,Crate<Orange>Crate<Fruit> 的子类型。

  • contravariance(逆变) :也就是说,Crate<Fruit>Crate<Orange> 的子类型。

相当于说,invariance就是父与子没关系,covariance就是正常的父子,contravariance说父子关系反过来。

image.png

  • Kotlin中的泛型类在定义时即可标明型变类型(协变或逆变,当然也可以不标明,那就是不型变的),在使用处可以直接型变(称为声明处型变)。
  • 如果想要Kotlin的型变和Java兼容,那么就需要用到 类型投影(type projections) 的方式来处理型变。

声明处型变

  • 声明处的类型变异使用协变注解修饰符:in(逆变)out(协变)消费者 in, 生产者 out

  • 使用 out 使得一个类型参数协变,协变类型参数只能用作输出,可以作为返回值类型但是无法作为入参的类型:

  • in 使得一个类型参数逆变,逆变类型参数只能用作输入,可以作为入参的类型但是无法作为返回值的类型。(协变只能出现在返回值中,逆变只能出现在方法的参数中)

这有篇关于型变的文章说得不错。www.jianshu.com/p/0c2948f7e…

out 协变

还是上例子吧

open class Person(val name: String, val age: Int) {
    open fun toWork() {
        println("我是父类工人:$name,我要好好干活!!!")
    }
}

class Worker1(name: String, age: Int) : Person(name, age) {
    override fun toWork() {
        println("我是电工:$name,接电线!!!")
    }
}

class Worker2(name: String, age: Int) : Person(name, age) {
    override fun toWork() {
        println("我是木工:$name,做衣柜!!!")
    }
}

fun setWork(workerList: List<out Person>) {
    for (o in workerList) {
        o.toWork()
    }
}

fun main(args: Array<String>) {
    val personArrayList: MutableList<Person> = ArrayList()
    // 注意这里添加的 Person
    personArrayList.add(Person("aaa", 11))

    // 以下两个添加的是 Worker1 和 Worker2
    personArrayList.add(Worker1("bbb", 12))
    personArrayList.add(Worker2("ccc", 13))
    setWork(personArrayList)

    val personArrayList1: MutableList<Worker1> = ArrayList()
    personArrayList1.add(Worker1("QQ", 14))
    setWork(personArrayList1)

    val personArrayList2: MutableList<Worker2> = ArrayList()
    personArrayList2.add(Worker2("TT", 15))
    setWork(personArrayList2)
}

输出:
我是父类工人:aaa,我要好好干活!!!
我是电工:bbb,接电线!!!
我是木工:ccc,做衣柜!!!
我是电工:QQ,接电线!!!
我是木工:TT,做衣柜!!!

.
.
再来一个例子

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)

class SimpleData<out T>(val data: T) {

    fun get(): T? {
        return data
    }
}

fun main() {
    val stu = Student("liu",23)
    val data = SimpleData<Student>(stu)
    val studentData = data.get()
    println(studentData!!::class.java)
}

.
.

in 逆变

例子1

class Reverse<in T> {
    fun setValue(value: T) {
        println("value: $value")
    }
}

fun main() {
    var reverse1 = Reverse<Int>()
    val reverse2 = Reverse<Number>()
    reverse2.setValue(3.14f) // value: 3.14

    // 可以将 Reverse<Number> 赋值给 Reverse<Int>
    reverse1 = reverse2
}

.
.
例子2

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)

interface ITransform<in T> {
    fun transform(t : T) : String
}

fun handleMyData(trans: ITransform<Student>) {
    val student = Student("liu",23)
    val result = trans.transform(student)
}

fun main() {
    val trans = object : ITransform<Person> {
        override fun transform(t: Person): String {
            println("姓名: ${t.name}   年龄: ${t.age}")
            return "${t.name} ${t.age}"
        }
    }

    handleMyData(trans)
}

输出:
姓名: liu   年龄: 23

四、UnsafeVariance

@UnsafeVariance:型变点 违例

那是否意味着out关键字修饰的泛型参数是不是不能出现在in位置 ?当然不是,只要函数内部能保证不会对泛型参数存在写操作的行为,可以使用UnSafeVariance注解使编译器停止警告,就可以将其放在in位置。out关键字修饰的泛型参数也是同理。

例如Kotlin的Listcontains函数等,就是应用UnSafeVariance注解使泛型参数存在于in位置,其内部没有写操作。

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

五、星投影Start Projection

星号投影用来表明“不知道关于泛型实参的任何信息”。

类似于 Java 中的无界类型通配符?, Kotlin 使用星号投影*

*代指了所有类型,相当于Any?

例如:MutableList<*> 表示的是 MutableList<out Any?>

fun main() {
    val list1 = mutableListOf<String>()
    list1.add("string1")
    list1.add("string2")
    printList(list1)

    val list2 = mutableListOf<Int>()
    list2.add(123)
    list2.add(456)
    printList(list2)
}

fun printList(list: MutableList<*>) {

    println(list[0])
}
}

正是由于使用 out 修饰以及星号投影的类型不确定性,会导致写入的任何值都有可能跟原有的类型冲突。因此,星号投影不能写入,只能读取。


参考:

www.runoob.com/kotlin/kotl…