Kotlin新手入坑:委托和泛型.._...ST

抓住今天,尽可能少的信赖明天。 喝汤能补 (* ^ ▽ ^ *)

前言

  该文章作为学习交流,如有错误欢迎各位大佬指正 (* ^ ▽ ^ *)

  • 自身技能
    (1)已具备计算机的基本知识
  • 本文简介
    主要讲解:kotlin中委托设计模式;泛型的基本使用,协变,逆变。

委托

  委托是种设计模式,基本理念:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。

  • 委托分为:类委托和委托属性。
  • 类委托,将一个类的具体实现委托给另一个类去完成。
  • 类委托方式,在待实现的方法比较多时,每个都要写一遍就麻烦了。Kotlin中,通过类委托的功能进行解决。关键字是by。写法:在接口声明后面使用by关键字,加上受委托的辅助对象,就可以不用写大量的实现方法了。
  • 委托属性,是将一个属性(字段)的具体实现委托给另一个类去完成。其定义的方式有一套标准的模板,使用operator关键字。
  • 属性委托的模板代码:operator fun getValue(info: Info, property: KProperty <*>): Any? 。第一个参数指定委托类Message的委托功能在什么类(Info)使用;第二参数KProperty<*>是Kotlin中属性操作类,用于获取各种属性相关的值;返回值可以是任何类型,可自行定义。operator fun setValue(info: Info, property: KProperty<*>, any: Any?) 参数与getValue含义类似,只是三个参数的类型与getValue的返回类型要一致。当委托属性的类型是val类型时,因不可变,就无需setValue方法。

  以系统提供的Set类为例子,存储的特点是无序,且不能存储重复的数据结构。Set只是一个接口,一般使用的是HashSet实现类。
  下面代码中,Set接口中的方法,没有进行实现,而是调用了辅助对象中的相应方法,这就是一种委托模式。这种委托方式好处是:如果将大部分方法实现调用辅助对象中的方法,少部分自己重写,或加入独有的方法,则MySet就是一个新的数据结构类。

class MySet<T>(val helperSet: HashSet<T>) : Set<T>{
    override val size: Int
        get() = helperSet.size

    override fun contains(element: T): Boolean = helperSet.contains(element)

    override fun containsAll(elements: Collection<T>): Boolean = helperSet.containsAll(elements)

    override fun isEmpty(): Boolean = helperSet.isEmpty()

    override fun iterator(): Iterator<T> = helperSet.iterator()

}

使用by关键字进行简化,这样我们只需编写需要重写的方法即可,其他的方法Kotlin编译器完成了处理。

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{

}

属性委托示例
这里将message的属性具体实现委托给Message类去完成。调用message时,就自动调用Message类的getValue方法,赋值则自动调用setValue。

class Info {
    var message by Message()
}

class Message {
    
    var message: Any? = null
    
    operator fun getValue(info: Info, property: KProperty<*>): Any? {
        return message
    }

    operator fun setValue(info: Info, property: KProperty<*>, any: Any?) {
        message = any
    }
}

泛型

基本使用

  泛型,允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。如:List可以存放数据,但是没有限制只能存放整型,字符串数据。通过泛型来实现存放具体的数据类型,也就是类似List、List的语法。

  • 定义:一种是定义泛型类,另一种是定义泛型方法。
  • 语法结构是 <T> 。T并不是固定的,可以使用其他任何字母或者单词,T只是约定俗成的写法。类:在类名后面加 <T>   方法:在方法名前加 <T>
  • 通过上界的方式对泛型的类型进行限制,实现方式:<T : 类名>
  • 所有的泛型都是可以指定成可空类型的。因为在不手动指定上界的时候,默认的的上界是Any? 。想泛型类型不可为空,将泛型的上界手动指定成Any就行。

这里Generic就是泛型类。

fun main() {
    val gen = Generic<String>()
    val ret = gen.method("sss")
}

class Generic<T> {
    fun method(param: T):T {
        return param
    }
}

只定义泛型方法


fun main() {
    val gen = Generic()
    val ret0 = gen.method<String>("sss")
    //kotlin 类型推导机制,简化
    val ret1 = gen.method("sss")
}

class Generic {
    fun <T> method(param: T): T{
        return param
    }
}

对泛型类进行限制,此时该方法只能传入数字类型,如:Int,Float等等

class Generic {
    fun <T: Number> methodNum(param: T): T{
        return param
    }
}

泛型实化

  java中没有泛型实化概念。在JDK1.5之前,Java是没有泛型功能的,List的数据结构可以存储任意类型的数据,取出数据的时候也需要手动转化类型才行,这样就比较麻烦和危险。如果同时存储整型和字符串类型,那么取数据时会因无法区分类型,导致抛出类型转换异常的错误。
  在jdk1.5中,Java引入了泛型功能,这样使List之类的数据结构变得既简单好用,又安全。
  JDK1.5以后,Java的泛型功能是通过类型擦除机制实现的。即,泛型对于类型的约束只在编译时期存在,在运行的时候仍然会按照JDK1.5之前的机制来运行,JVM是识别不出来我们在代码中指定的泛型类型的。
  例子,List集合,虽然在编译时期只能向集合中添加字符串类型元素,但是运行时期JVM并不能知道它原来包含那种类型的元素,只能识别出来它是个List。

  基于JVM的语言,其泛型功能都是通过类型擦除机制实现的。这种机制不能使用a is T或者T::class.java语法,因为T在实际运行的时候是没有类型的。Kotlin中泛型也是类型擦除机制实现的,但是它支持内联函数(编译时进行代码替换的特性),所以可以解决泛型的类型擦除问题。即,Kotlin中是可以将内联函数中的泛型进行实化的。

  • 泛型实化,函数必须是内联函数(inline关键字),在声明泛型的前面必须加上reified关键字来表示该泛型要进行实化。
  • 泛型实化后,可以使用a is T,T::class.java 的语法。
fun foo(){
    bar<String>()
}

inline fun <T> bar(){
    // 处理逻辑
}

// 内联函数在编译的时候,直接将处理的逻辑放到了使用的地方,这样类型就已经确定了。
fun foo(){
    // 处理逻辑
}

泛型实化的示例,可以得到泛型实际的类型,以及使用T::class的方式。

fun main() {
    val ret1 = getGenericType<String>()
    val ret2 = getGenericType<Int>()
    println("ret1 is $ret1")
    println("ret2 is $ret2")
}

inline fun <reified T> getGenericType() = T::class.java

泛型的协变

  先了解一个约定,一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称为in 位置,而返回值是输出数据地方,因此称为out位置。

  • 定义,一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么就可以称MyClass在T这个泛型上是协变的。
  • MyClass<A>又是MyClass<B>的子类型实现:对MyClass<T>类中所有方法都不能接受T类型的参数。即,T只能出现在out位置上不能出现在in位置上; 也就是说泛型类在其泛型类型的数据上是只读的。语法:在泛型T的声明前面加上out关键字,这就意味着只能出现在out位置,不能在in位置。
  • 这种协变的类,通过构造函数的方式进行赋值,因为使用了val关键字,所以构造函数中的泛型T仍然是只读的。或者也能使用var关键字,但要加上private修饰符,保证这个泛型T对于外部不可修改,这样的写法也是合法的。

  Kotlin内置的API中,使用了许多协变的特性。比如集合的类和接口,因为在声明list时,一般都是只读的,只有使用MutableList才能声明可变的List 。在List 的源码中,就会使用out关键字进行协变;其中contains()方法能接受泛型E,这是因为该方法的功能并不涉及修改,因此也是安全的,但为了让编译器能编译过,就需要添加一个@UnsafeVariance注解,这样就能让泛型E出现在in的位置上。

  下面代码中,某个方法接收一个List<Animal>类型的参数,传入一个List<Dog>的实例是合法的;但在java中是不合法的,会存在类型转换的安全隐患的。

open class Animal(val name: String,val age: Int)
class Bird(name: String,age: Int):Animal(name,age)
class Dog(name: String,age: Int):Animal(name,age)

类型转换的安全隐患
  在下面代码中,将ResultInfo<Dog>的数据传入到函数testResultInfo中,然后使用Bird的实例替换掉ResultInfo<Animal>中的数据,操作上面是合法的。但是,我们在使用ResultInfo<Dog>的get方法获取数据时,内部数据实际是Bird实例,这就出现了类型转换异常。
如果说泛型类在其泛型类型的数据上是只读的(ResultInfo在泛型T上是只读的),无法进行set的修改,就可以解决这种类型转换的问题。

class ResultInfo<T> {
    private var data: T? = null
    fun set(t: T?){
        data = t
    }
    fun get(): T? {
        return data
    }
}

fun main() {
    val data = ResultInfo<Dog>()
    data.set(Dog("30",3))
    testResultInfo(data) //直接提示无法编译
     val dogData = data.get()
}

fun testResultInfo(data: ResultInfo<Animal>) {
    val bird = Bird("G",1)
    data.set(bird)
}

// 正确的写法是,让ResultInfo进行协变
class ResultInfo<out T>(val data: T?) {
    fun get(): T? {
        return data
    }
}

fun main() {
    val dog = Dog("30",3)
    val data = ResultInfo<Dog>(dog)
    testResultInfo(data)
    val dogData = data.get()
}

fun testResultInfo(data: ResultInfo<Animal>) {
    val animal = data.get()
}

泛型的逆变

  • 定义,一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>又是MyClass<A>的子类型,那么就可以称MyClass在T这个泛型上是逆变的。
  • MyClass<B>又是MyClass<A>的子类型实现:对MyClass<T>类中所有方法都不能返回T类型的参数。即,T只能出现在in位置上,不能出现在out位置上; 也就是说泛型类在其泛型类型的数据上是只写的。语法:在泛型T的声明前面加上in关键字,这就意味着只能出现在in位置,不能在out位置。

  Kotlin内置的API中,使用了许多逆变的特性。比如Comparable类就是在T泛型前加了in,进行了逆变,实现比较两个对象大小的逻辑,既能使用父类对象,也能使用子类对象。

下面代码
  main中,通过匿名类Transformer将Animal变为拼接的字符串,testTransformer函数中,将dog对象通过Transformer中的方法变为字符串,这段代码从安全的角度分析没有问题的,但是却提示了错误,因为Transformer<Animal>不是Transformer<Dog>的子类,可以看下面第二份代码的分析例子。

interface Transformer<T> {
    fun transformer(t: T): String
}

fun main() {
    val trans = object : Transformer<Animal>{
        override fun transformer(t: Animal): String {
            return "${t.name} ${t.age}"
        }
    }
    testTransformer(trans) //编译出错
}

fun testTransformer(transformer: Transformer<Dog>) {
    val dog = Dog("30",2)
    val ret = transformer.transformer(dog)
}

//正确的写法,让Transformer进行逆变
interface Transformer<in T> {
    fun transformer(t: T): String
}
fun main() {
    val trans = object : Transformer<Animal>{
        override fun transformer(t: Animal): String {
            return "${t.name} ${t.age}"
        }
    }
    testTransformer(trans) 
}

fun testTransformer(transformer: Transformer<Dog>) {
    val dog = Dog("30",2)
    val ret = transformer.transformer(dog)
}

逆变解决的问题简单列子分析
  下面代码,在testTransformer函数中,希望通过接口方法transformer得到Dog对象,但是实际上,因为Transformer接口在实例化的时候返回的是Bird,所以导致类型转换异常。
  这里因为加入了@UnsafeVariance注解让编译器能编译过,实际运行会报错。

interface Transformer<in T> {
  // 使用@UnsafeVariance注解让编译器能编译过,但是实际运行时,还是会报错
    fun transformer(name: String,age: Int): @UnsafeVariance T
}
fun main() {
    val trans = object : Transformer<Animal>{
        override fun transformer(name: String, age: Int): Animal {
            return Bird(name,age)
        }
    }
    testTransformer(trans)
}

fun testTransformer(transformer: Transformer<Dog>) {
    val ret = transformer.transformer("G",3)
}

(* ^ ▽ ^ *)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值