Sealed Class 密封类
如果想对能够创建出的子类做限制,可以使用密封类。
下面一个例子是没有使用密封类的:
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(expr: Expr): Int {
return when(expr){
is Sum -> eval(expr.left) + eval(expr.right)
is Num -> expr.value
else -> throw IllegalArgumentException("Unknown Expression")
}
}
这类似于只支持加法的抽象语法树,Expr代表一个表达式,也就是语法树里的一个节点,同时Num代表数字节点,它只可能是叶子,Sum代表加法节点,不可能是叶子。
现在如果我们要实现eval函数来计算抽象语法树的最终结果,我们发现,始终需要一个else来收尾,因为Expr可能还有其他实现类,可能既不是Sum又不是Num,尽管代码里根本没有其他实现类。
密封类能解决这个问题。
sealed class Expr {
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(expr: Expr): Int {
return when(expr){
is Expr.Sum -> eval(expr.left) + eval(expr.right)
is Expr.Num -> expr.value
}
}
密封类表明了该类不可能有除了Num和Sum之外的其他子类,所以编译器可以发现我们when中的代码是无懈可击的,自然不用一个额外的else。
类委托
Java中有一套设计模式就是委托模式,就是指编写一个类,但它不提供实现,所有的功能都会委托给另一个类实现,在必要的时候对类进行增强。Java后面的代理、动态代理技术全部都是基于委托实现的,可以说它是Java世界的一个支柱。
Kotlin默认支持委托,不像Java,要么用IDE生成一大堆代码,要么在编译期使用其他动态代理工具生成,Kotlin默认提供了by关键字。
下面的类继承自MutableCollection,但它完全不存储数据,而是通过by委托给innerSet。
class CountingSet<T> (
val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet{
var objectsAdded = 0
override fun add(element: T): Boolean {
objectsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
objectsAdded += elements.size
return innerSet.addAll(elements)
}
}
fun main(){
val set = CountingSet<Int>()
set.add(10)
set.addAll(listOf(1,2,4,15,5,3))
set.remove(10)
println(set.objectsAdded)
}
比较常用的就是委托类作为构造器参数传入,Java中比较常见的就是基于委托的IO流,我们经常这样写:
new BufferedInputStream(new FileInputStream(...));
这里BufferedInputStream并不会自己实现InputStream的读取功能,而是委托给FileInputStream并对它的功能进行增强(通过建立缓冲区)。
我们上面编写的类也是,你可以调用CountingSet传入不同的Collection实现,不同的是我们提供了一个默认值。
除了使用构造器参数,还可以直接新建一个类委托,因为有时候我们就想让它委托同一个类,不想让用户自己抉择。
class MySet<T> () :
MutableCollection<T> by HashSet<T>(){
}
属性委托
Jetpack Compose中有一个记录状态并自动更新UI的东西,就是var value by remember,这种监测数据更新并自动刷新UI的东西在如今数据驱动的框架中并不少见。Jetpack Compose就是通过属性委托来实现的数据监测。
class Remember{
lateinit var name: String
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
// return name
return name
}
operator fun setValue(thisRef: Any?, property: KProperty<*>,value: String) {
// name = value
println("${property.name} is changed!!!")
name = value
}
}
fun main(){
var name: String by Remember()
name = "ASDFASDF"
println(name)
}
/*
name is changed!!!
ASDFASDF
*/
被委托的类应该实现一个getValue和setValue方法,委托方的变量不再存储值,而是由被委托的类提供存储功能。
我们接下来编写一个懒加载的属性委托,就是第一次访问属性时才为属性赋值
class LazyDelegate<T>(private val compute: ()->T){
var t: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (t == null) t = compute()
return t!!
}
operator fun setValue(thisRef: Any?, property: KProperty<*>,value: T) {
t = value
}
}
fun <T> lazy(compute: ()->T) = LazyDelegate<T>(compute)
fun main(){
var name: String by lazy {
"HelloWorld"
}
println(name)
}
这一次我们提供了一个lazy方法,Lazy方法会返回我们的委托人LazyDelegate,因为Kotlin官方就为一些自带的委托封装了方法,可能是Kotlin社区惯用的编码规范,确实,这样好看一些,而且Jetpack Compose中的remember实际上也是这样写的。
然后,我们还运用了泛型和lambda表达式,lambda用于返回一个值,一般使用懒加载的时候,这个lambda表达式都会是一个很复杂并且可能并不常用的运算,所以这样如果这个值如果没被需要,懒加载就不会执行。泛型用于支持全部类型的值。
伴生对象
Java中经常会使用静态工厂方法来构造对象,这是因为静态工厂方法比构造器更加适用于处理那些很多属性可以不在构造时提供的类。静态工厂方法更加具有可读性。Kotlin根本没有静态这一说,Kotlin代替静态的办法一个是object,一个是顶层函数。但这俩都不适用于静态工厂,因为静态工厂经常要访问类中的私有成员。
伴生对象是用来干这些的。
class Person private constructor(
name: String?,
age: Int?,
address: String?
){
companion object {
fun fromNameAndAge(name: String, age: Int): Person = Person(name,age,null)
fun fromName(name: String): Person = Person(name,null,null)
fun fromNameAndAddress(name: String, address: String): Person = Person("Lucy",null,address)
fun newPerson(name: String, age: Int, address: String): Person = Person(name,age,address)
}
}
fun main(){
Person.fromNameAndAge("Lucy",12)
}
这对于Builder模式同样适用,对于绝大多数需要和类中私有成员进行交互的地方,都适用。
但是,别忘了Kotlin中的命名参数,上面的例子本可以用命名参数更加方便的解决。
class Person constructor(
name: String,
age: Int? = null,
address: String? = null
)
fun main(){
Person(name = "Lucy", age = 12)
}
当然伴生对象可以命名
class Person constructor(
name: String,
age: Int? = null,
address: String? = null
){
companion object Loader{
fun fromJson(json: String): Person{ ... }
}
}
fun main(){
Person.Loader.fromJson()
}
伴生对象也可以有扩展函数,这是因为像上面的Loader这种伴生对象和类中的逻辑关系不大,分离到外部可以实现关注点分离。
class Person constructor(
name: String,
age: Int? = null,
address: String? = null
){
companion object Loader{}
}
fun Person.Loader.fromJson(json: String): Person {
...
}
fun main(){
Person.Loader.fromJson()
}
如果是没有名字的伴生对象,也可以
fun Person.Companion.fromJson(json: String): Person{
...
}