Kotlin泛型和委托

1、泛型

1.1 基本语法

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是。当然括号内的T并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常情况下,T是一种约定俗成的泛型写法。

如果我们要定义一个泛型类,就可以这么写:

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

此时的MyClass就是一个泛型类,MyClass中的方法允许使用T类型的参数和返回值。

我们在调用MyClass类和method()方法的时候,就可以将泛型指定成具体的类型,如下所示:

val myClass = MyClass<Int>()
val result = myClass.method(123)

这里我们将MyClass类的泛型指定成Int类型,于是method()方法就可以接收一个Int类型的参数,并且它的返回值也变成了Int类型。

定义一个泛型方法:将定义泛型的语法结构写在方法上面即可,如下所示

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

此时调用方式也需要进行调整:

val myClass = MyClass()
val result = myClass.method<Int>(123)

可以看到,现在是在调用method()方法的时候指定泛型类型了。另外,Kotlin还拥有非常出色的类型推导机制,例如我们传入了一个Int类型的参数,它能够自动推导出泛型的类型就是Int型,因此这里也可以直接省略泛型的指定:

val myClass = MyClass()
val result = myClass.method(123)

Kotlin还允许我们对泛型的类型进行限制。目前你可以将method()方法的泛型指定成任意类型,但是如果这并不是你想要的话,还可以通过指定上界的方式来对泛型的类型进行约束,比如这里将method()方法的泛型上界设置为Number类型,如下所示:

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

这种写法就表明,我们只能将method()方法的泛型指定成数字类型,比如Int、Float、Double等。但是如果你指定成字符串类型,就肯定会报错,因为它不是一个数字。

另外,在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是Any?。而如果想要让泛型的类型不可为空,只需要将泛型的上界手动指定成Any就可以了。

2、类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。这个概念对于Java程序员来讲可能相对比较陌生,因为Java对于委托并没有语言层级的实现,而像C#等语言就对委托进行了原生的支持。

Kotlin中也是支持委托功能的,并且将委托功能分为了两种:类委托和委托属性。下面我们逐个进行学习。

首先来看类委托,它的核心思想在于将一个类的具体实现委托给另一个类去完成。在前面的章节中,我们曾经使用过Set这种数据结构,它和List有点类似,只是它所存储的数据是无序的,并且不能存储重复的数据。Set是一个接口,如果要使用它的话,需要使用它具体的实现类,比如HashSet。而借助于委托模式,我们可以轻松实现一个自己的实现类。比如这里定义一个MySet,并让它实现Set接口,代码如下所示:

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

    override fun contains(element: T): Boolean {
    
    
        return helperSet.contains(element)
    }

    override fun containsAll(elements: Collection<T>): Boolean {
    
    
        return helperSet.containsAll(elements)
    }

    override fun isEmpty(): Boolean {
    
    
        return helperSet.isEmpty()
    }

    override fun iterator(): Iterator<T> {
    
    
        return helperSet.iterator()
    }

}

可以看到,MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后在Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中相应的方法实现,这其实就是一种委托模式。

那么,这种写法的好处是什么呢?既然都是调用辅助对象的方法实现,那还不如直接使用辅助对象得了。这么说确实没错,但如果我们只是让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就会成为一个全新的数据结构类,这就是委托模式的意义所在。

但是这种写法也有一定的弊端,如果接口中的待实现方法比较少还好,要是有几十甚至上百个方法的话,每个都去这样调用辅助对象中的相应方法实现,那可真是要写哭了。那么这个问题有没有什么解决方案呢?在Java中确实没有,但是在Kotlin中可以通过类委托的功能来解决。

Kotlin中委托使用的关键字是by,我们只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式的代码了,如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{
    
    	//helperSet就是传进来的HashSet

}

这两段代码实现的效果是一模一样的,但是借助了类委托的功能之后,代码明显简化了太多。另外,如果我们要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利,如下所示:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{
    
    
    fun helloWorld() = println("Hello World")

    override fun isEmpty(): Boolean {
    
    
        return false;
    }
}

这里我们新增了一个helloWorld()方法,并且重写了isEmpty()方法,让它永远返回false。这当然是一种错误的做法,这里仅仅是为了演示一下而已。现在我们的MySet就成为了一个全新的数据结构类,它不仅永远不会为空,而且还能打印helloWorld(),至于其他Set接口中的功能,则和HashSet保持一致。这就是Kotlin的类委托所能实现的功能。

掌握了类委托之后,接下来我们开始学习委托属性。它的基本理念也非常容易理解,真正的难点在于如何灵活地进行应用。

类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。

我们看一下委托属性的语法结构,如下所示:

class MyClass {
    
    
    var p by Delegate()
}

可以看到,这里使用by关键字连接了左边的p属性和右边的Delegate实例,这是什么意思呢?这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。

因此,我们还得对Delegate类进行具体的实现才行,代码如下所示:

class Delegate {
    
    
    var propValue: Any? = null

    operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any?{
    
    
        return propValue
    }

    operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?){
    
    
        propValue = value
    }
}

这是一种标准的代码实现模板,在Delegate类中我们必须实现getValue()和setValue()这两个方法,并且都要使用operator关键字进行声明。

getValue()方法要接收两个参数:第一个参数用于声明该Delegate类的委托功能可以在什么类中使用,这里写成MyClass表示仅可在MyClass类中使用;第二个参数KProperty<*>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不着,但是必须在方法参数上进行声明。另外,<*>这种泛型的写法表示你不知道或者不关心泛型的具体类型只是为了通过语法编译而已,有点类似于Java中<?>的写法。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了,上述代码只是一种示例写法。

setValue()方法也是相似的,只不过它要接收3个参数。前两个参数和getValue()方法是相同的,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法返回值的类型保持一致。

整个委托属性的工作流程就是这样实现的,现在当我们给MyClass的p属性赋值时,就会调用Delegate类的setValue()方法,当获取MyClass中p属性的值时,就会调用Delegate类的getValue()方法。是不是很好理解?

不过,其实还存在一种情况可以不用在Delegate类中实现setValue()方法,那就是MyClass中的p属性是使用val关键字声明的。这一点也很好理解,如果p属性是使用val关键字声明的,那么就意味着p属性是无法在初始化之后被重新赋值的,因此也就没有必要实现setValue()方法,只需要实现getValue()方法就可以了。

实现一个自己的lazy函数

在8.4.2小节初始化uriMatcher变量的时候,我们使用了一种懒加载技术。把想要延迟执行的代码放到by lazy代码块中,这样代码块中的代码在一开始的时候就不会执行,只有当uriMatcher变量首次被调用的时候,代码块中的代码才会执行。

那么学习了Kotlin的委托功能之后,我们就可以对by lazy的工作原理进行解密了,它的基本语法结构如下:

val p by lazy {
    
    ...}

现在再来看这段代码,是不是觉得更有头绪了呢?实际上,by lazy并不是连在一起的关键字,只有by才是Kotlin中的关键字,lazy在这里只是一个高阶函数而已。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。

新建一个Later.kt文件,并编写如下代码:

class Later<T> (val block: () -> T){
    
    
    
}

这里我们首先定义了一个Later类,并将它指定成泛型类。Later的构造函数中接收一个函数类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是Later类指定的泛型。

接着我们在Later类中实现getValue()方法,代码如下所示:

class Later<T> (val block: () -> T){
    
    
    var value: Any? = null

    operator fun getValue(any: Any?, prop: KProperty<*>): T {
    
    
        if (value == null){
    
    
            value = block()
        }
        return value as T
    }
}

这里将getValue()方法的第一个参数指定成了Any?类型,表示我们希望Later的委托功能在所有类中都可以使用。然后使用了一个value变量对值进行缓存,如果value为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。

由于懒加载技术是不会对属性进行赋值的,因此这里我们就不用实现setValue()方法了。

代码写到这里,委托属性的功能就已经完成了。虽然我们可以立刻使用它,不过为了让它的用法更加类似于lazy函数,最好再定义一个顶层函数。这个函数直接写在Later.kt文件中就可以了,但是要定义在Later类的外面,因为只有不定义在任何类当中的函数才是顶层函数。代码如下所示:

fun <T> later(block: () -> T) = Later(block)

我们将这个顶层函数也定义成了泛型函数,并且它也接收一个函数类型参数。这个顶层函数的作用很简单:创建Later类的实例,并将接收的函数类型参数传给Later类的构造函数。

现在,我们自己编写的later懒加载函数就已经完成了

但是如何才能验证later函数的懒加载功能有没有生效呢?这里我有一个非常简单方便的验证方法,写法如下:

        val p by later {
    
    
            Log.d("TAG", "run codes inside later block")
            "text later"
        }
class MainActivity : AppCompatActivity() {
    
    

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        val p by later {
    
    
            Log.d("TAG", "run codes inside later block")
            "text later"
        }

        binding.button1.setOnClickListener{
    
    
            Log.d("TAG", p)
            Toast.makeText(this, "点击了按钮", Toast.LENGTH_SHORT).show()
        }
    }
}

image-20210915163528581

你会发现,当Activity启动的时候,later函数中的那行日志是不会打印的。只有当你首次点击按钮的时候,日志才会打印出来,说明代码块中的代码成功执行了。而当你再次点击按钮的时候,日志也不会再打印出来,因为代码块中的代码只会执行一次。

另外,必须说明的是,虽然我们编写了一个自己的懒加载函数,但由于简单起见,这里只是大致还原了lazy函数的基本实现原理,在一些诸如同步、空值处理等方面并没有实现得很严谨。因此,在正式的项目中,使用Kotlin内置的lazy函数才是最佳的选择。

猜你喜欢

转载自blog.csdn.net/qq_41811862/article/details/120808866