Skip to content

Latest commit

 

History

History
1047 lines (786 loc) · 42.6 KB

5.Kotlin_内部类&密封类&枚举&委托.md

File metadata and controls

1047 lines (786 loc) · 42.6 KB

5.Kotlin_内部类&密封类&枚举&委托

泛型

class Data<T>(var t : T)
interface Data<T>
fun <T> logic(t : T){}

定义:

class TypedClass<T>(parameter: T) {
    val value: T = parameter
}

这个类现在可以使用任何的类型初始化,并且参数也会使用定义的类型,我们可以这么做:

val t1 = TypedClass<String>("Hello World!")
val t2 = TypedClass<Int>(25)

但是Kotlin很简单并且缩减了模版代码,所以如果编译器能够推断参数的类型,我们甚至也就不需要去指定它:

val t1 = TypedClass("Hello World!")
val t2 = TypedClass(25)
val t3 = TypedClass<String?>(null)

泛型实化

泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、T::class.java这样的语法成为了可能。而灵活运用这一特性将可以实现一些不可思议的语法结构,下面我们赶快来看一下吧。到目前为止,我们已经将Android的四大组件全部学完了,除了ContentProvider之外,你会发现其余的3个组件有一个共同的特点,它们都是要结合Intent一起使用的。比如说启动一个Activity就可以这么写:

val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)

有没有觉得TestActivity::class.java这样的语法很难受呢?当然,如果在没有更好选择的情况下,这种写法也是可以忍受的,但是Kotlin的泛型实化功能使得我们拥有了更好的选择。新建一个reified.kt文件,然后在里面编写如下代码:

inline fun <reified T> startActivity(context: Context) {
    val intent = Intent(context, T::class.java)
    context.startActivity(intent)
}

这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用inline和reified关键字让泛型T成为了一个被实化的泛型。接下来就是神奇的地方了,Intent接收的第二个参数本来应该是一个具体Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。现在,如果我们想要启动TestActivity,只需要这样写就可以了:[插图]Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。

startActivity<TestActivity>(context)

Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。 不过,现在的startActivity()函数其实还是有问题的,因为通常在启用Activity的时候还可能会使用Intent附带一些参数,比如下面的写法:

val intent = Intent(context, TestActivity::class.java)
intent.putExtra("param1", "data")
intent.putExtra("param2", 123)
context.startActivity(intent)

而经过刚才的封装之后,我们就无法进行传参了。这个问题也不难解决,只需要借助之前在第6章学习的高阶函数就可以轻松搞定。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示:

inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
    val intent = Intent(context, T::class.java)
    intent.block()
    context.startActivity(intent)
}

可以看到,这次的startActivity()函数中增加了一个函数类型参数,并且它的函数类型是定义在Intent类当中的。在创建完Intent的实例之后,随即调用该函数类型参数,并把Intent的实例传入,这样调用startActivity()函数的时候就可以在Lambda表达式中为Intent传递参数了,如下所示:

startActivity<TestActivity>(context) {
    putExtra("param1", "data")
    putExtra("param2", 123)
}

不得不说,这种启动Activity的代码写起来实在是太舒服了,泛型实化和高阶函数使这种语法结构成为了可能.

类型擦除
class Data<T>{}

Log.d("test", Data<Int>().javaClass.name)
Log.d("test", Data<String>().javaClass.name)

// 输出
com.study.jcking.weatherkotlin.exec.Data
com.study.jcking.weatherkotlin.exec.Data

声明了一个泛型类Data<T>,并实现了两种不同类型的实例。但是在获取类名是,却发现得到了同样的结果 com.study.jcking.weatherkotlin.exec.Data,这其实是在编译期擦除了泛型类型声明。

嵌套类

嵌套类顾名思义,就是嵌套在其他类中的类。而嵌套类外部的类一般被称为包装类或者外部类。

class Outter{
    class Nested{
        fun execute(){
            Log.d("test", "Nested -> execute")
        }
    }
}

// 调用
Outter.Nested().execute()

//输出
Nested -> execute

嵌套类可以直接创建实例,方式是包装类.嵌套类 val nested : Outter.Nested()

内部类

内部类和嵌套类有些类似,不同点是内部类用关键字inner修饰。

class Outter{
    val testVal = "test"
    inner class Inner{
        fun execute(){
            Log.d("test", "Inner -> execute : can read testVal=$testVal")
        }
    }
}

// 调用
val outter = Outter()
outter.Inner().execute()

var v = this@Outer.testVal // 引用外部类的成员

// 输出
Inner -> execute : can read testVal=test

内部类不能直接创建实例,需要通过外部类调用

val outter = Outter()
outter.Inner().execute()

引用外部类中对象的方式为: var 变量名 = this@外部类名

在类成员中,this指代该类的当前对象。而在扩展函数或者带接受者的函数中,this表示在点左侧传递的接收者参数。

内部类vs嵌套类

在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类, 必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。

内部类和嵌套类有明显的差别,具体体现在:

  • 嵌套类相当于Java中的静态内部类,不能使用外部类的this。
  • 内部类必须以inner class定义,相当于Java中的私有非静态内部类,可以访问外部类的this。
  • 内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。

匿名内部类

没有名字的内部类被称为匿名内部类,使用匿名内部类必须继承一个父类或实现一个接口,匿名内部类可以简化代码编写。

// 通过对象表达式来创建匿名内部类的对象,可以避免重写抽象类的子类和接口的实现类,这和Java中匿名内部类的是接口和抽象类的延伸一致。
text.setOnClickListener(object : View.OnClickListener{
    override fun onClick(p0: View?) {
        Log.d("test", p0.string())
    }
})

或
mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
    override fun onPageScrollStateChanged(state: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun onPageSelected(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

})

如果对象实例是一个函数接口,还可以使用Lambda表达式来实现。

val listener = ActionListener {
    println("clicked")
}

枚举

与Java中的enum语法大体类似,无非多了一个class关键字,表示它是一个枚举类。

enum class Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY
}

不过Kotlin中的枚举类当然没有那么简单,由于它是一种类,我们可以猜测它自然应该可以拥有构造函数,以及定义额外的属性和方法。

enum class DayOfWeek(val day: Int) {
    MON(1),
    TUE(2),
    WEN(3),
    THU(4),
    FRI(5),
    SAT(6),
    SUN(7)
    ; // 需要注意的是,当在枚举类中存在额外的方法或或属性定义,则必须强制加上分号,虽然你可能不会喜欢这个语法
    fun getDayNumber(): Int {
        return day
    }
}

枚举可以通过String匹配名字来获取,我们也可以获取包含所有枚举的Array,所以我们可以遍历它。

val search: DayOfWeek = DayOfWeek.valueOf("MON")
val iconList: Array<DayOfWeek> = DayOfWeek.values()

而且每一个枚举都有一些函数来获取它的名字、声明的位置:

val searchName: String = Icon.SEARCH.name()
val searchPosition: Int = Icon.SEARCH.ordinal()

枚举可以让你创建受限制的一组值,但在某些情况下,你需要更多的灵活性。假设你希望能在应用程序中使用两种不同的消息类型:一种用于”成功“,另一种用于”失败“,并且你希望能够将邮件限制为这两种类型。

如果你用枚举类对此进行建模,代码会如下:

enum class MessageType(var msg: String) {
    SUCCESS("Yay!"),
    FAILURE("Boo!")
}

但是使用这种方法存在两个问题:

  • 每个值都是一个常量,并且仅作为单个实例存在。

    例如,你无法只在特定情况下修改SUCCESS的msg属性--因为一旦更改,代码中其他SUCCESS出现的地方也会被相应更改。

  • 每个值必须有相同的属性和函数。

    向FAILURE值中添加Exception属性十分有用,它可以帮助你检查哪里出错了。但是枚举类不允许你这么做。

那么有其他的方法吗? 密封类可以拯救你。

密封类

Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承。密封类就像枚举类的加强版本。它允许你将类层次结构限制为一组特定的子类型,每个子类型都可以定义自己的属性和函数。与枚举类不同,你可以创建每种类型的多个实例。 你可以通过使用sealed前缀类名来创建密封类。例如,以下代码创建一个名为MessageType的密封类,其中包含名为MessageSuccess和MessageFailure的子类型。每个子类型都有一个名为msg的String属性,并且MessageFailure子类型中有一个额外的名为e的Exception属性:

sealed class MessageType
// MessageSuccess和MessageFailure从MessageType继承而来,并且它们自己的类型是在自己的构造函数中定义的
class MessageSuccess(var msg: String) : MessageType()
class MessageFailure(var msg: String, var e: Exception) : MessageType()

由于MessageType是一个有限子类的密封类。你可以使用when来检查每个子类型,这样可以避免使用额外的else子句,如以下代码所示:

fun main(args: Array<String>) {
    val messageSuccess = MessageSuccess("Yay!")
    val messageSuccess2 = MessageSuccess("It worked!")
    val messageFailure = MessageFailure("Boo!", Exception("Gone wrong."))
    
    var myMessageType: MessageType = messageFailure
    val myMessage = when(myMessageType) {
        is MessageSuccess -> myMessageType.msg
        is MessageFailure -> myMessageType.msg + myMessageType.e.message
    }
    println(myMessage)
}

Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的。

sealed class Bird {
    open fun fly() = "I can fly"
    class Eagle : Bird()
}

这一点可以从它转换后的Java代码看出:

public abstract class Bird {
    @NotNull
    public String fly() {
        return "I can fly"
    }
    private Bird() {
        
    }
    // $FF: synthetic method
    public Bird(DefaultConstructorMarker $constructor_maker) {
        this();
    }
    
    public static final class Eagle extends Bird {
        public Eagle() {
            super((DefaultConstructorMarker) null);
        }
    }
}

密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。(在Kotlin 1.1之前,该规则更加严格:子类必须嵌套在密封类声明的内部)。

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

使用密封类的关键好处在于使用when表达式的时候,如果能够 验证语句覆盖了所有情况,就不需要为该语句再添加一个else语句了。

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // 不再需要 `else` 子句,因为我们已经覆盖了所有的情况
}

异常

Kotlin中,所有的Exception都是实现了Throwable,含有一个message且未经检查。这表示我们不会强迫我们在任何地方使用try/catch。 这与Java中不太一样,比如在抛出IOException的方法,我们需要使用try-catch包围代码块。但是通过检查exception来处理显示并不是一个 好的方法。

抛出异常的方式与Java很类似:

throw MyException("Exception message")

try表达式也是相同的:

try{
    // 一些代码
}
catch (e: SomeException) {
    // 处理
}
finally {
    // 可选的finally块
}

Kotlin中,throwtry都是表达式,这意味着它们可以被赋值给一个变量。这个在处理一些边界问题的时候确实非常有用:

val s = when(x){
    is Int -> "Int instance"
    is String -> "String instance"
    else -> throw UnsupportedOperationException("Not valid type")
}

或者

val s = try { x as String } catch(e: ClassCastException) { null }

对象(Object)

在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的, 但在定义时却与普通的变量和方法混杂在一起,显得格格不入。

在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。 当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。

声明对象就如同声明一个类,你只需要用保留字object替代class,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。

object Resource {
    val name = "Name"
}

对象表达式

对象也能用于创建匿名类实现。

recycler.adapter = object : RecyclerView.Adapter() {
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
    }
 
    override fun getItemCount(): Int {
    }
 
    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
    }
}

例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。

object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。

伴生对象

先看一个Java例子:

public class Prize {
    private String name;
    private int count;
    private int type;
    
    public Prize(String name, int count, int type) {
        this.name = name;
        this.count = count;
        this.type = type;
    }
    
    static int TYPE_REDPACK = 0;
    static int TYPE_COUPON = 1;
    
    static boolean isRedpack(Prize prize) {
        return prize.type == TYPE_REDPACK;
    }
    
    public static void main(String[] args) {
        Prize prize = new Prize("hongbao", 10, Prize.TYPE_REDPACK);
        System.out.println(Prize.isRedpack(prize));
    }
}

上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法, 也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。 虽然有static作为区分,然而在代码结构上职能并不是区分的很清晰。

那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说, 这是一种利用companion object两个关键字创造的语法。

伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样, 全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。

准确的说是Kotlin使用伴生对象来实现Java静态的效果,但不能说伴生就是静态。

现在将上面的例子改写成伴生对象的版本:

class Prize(val name: String, val count: Int, val type: Int) {
    companion object {
        val TYPE_REDPACK = 0
        val TYPE_COUPON = 1
        
        fun isRedpack(prize: Prize): Boolean {
            return prize.type == TYPE_REDPACK
        }
    }
    
    fun main(args: Array<String>) {
        val prize = Prize("hongbao", 10, Prize.TYPE_REDPACK)
        print(Prize.isRedpack(prize))
    }
}

可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。

伴生对象的另一个作用是可以实现工厂方法模式。当然也可以从构造方法实现工厂方法模式,然而这种方法存在以下缺点:

  • 利用多个构造方法语义不够明确,只能靠参数区分
  • 每次获取对象时都需要重新创建对象

你会发现,伴生对象也是实现工厂方法模式的另一种思路,可以改进以上的两个问题。

class Prize private constructor(val name: String, val count: Int, val type: Int) {
    companion object {
        val TYPE_COMMON = 1
        val TYPE_REDPACK = 2
        val TYPE_COUPON = 3
        
        val defaultCommonPrize = Prize("common", 10, Prize.TYPE_COMMON) 
        
        fun newRedpackPrize(name: String, count: int) = Prize(name, count, Prize.TYPE_REDPACK)
        fun newCouponPrize(name: String, count: Int) = Prize(name, count, Prize.TYPE_COUPON)
        fun defaultCommonPrize() = defaultCommonPrize // 无须构造新对象
    }
    
    fun main(args: Array<String>) {
        val redpackPrize = Prize.newRedpackPrize("hongbao", 10)
        val couponPrize = Prize.newCouponPrize("shiyuan", 10)
        vval commonPrize = Prize.defaultCommonPrize()
    }
}

总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。 然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。

每个类都可以实现一个伴生对象,它是该类的所有实例共有的对象。它将类似于Java中的静态字段。

class App : Application() {
    companion object {
         private lateinit var instance: App
     }
 
     override fun onCreate() {
         super.onCreate()
         instance = this
    }
}

在这例子中,创建一个继承Application的类,并且在companion object中存储它的唯一实例。
lateinit表示这个属性开始是没有值的,但是,在使用前将被赋值(否则,就会抛出异常)。

不过,上面companion object中的isRedpack()方法其实也并不是静态方法,companionobject这个关键字实际上会在类的内部创建一个伴生类,而isRedpack方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证类始终只会存在一个伴生类对象,因此调用Prize.isRedpack()方法实际上就是调用了Prize类中伴生对象的isRedpack()方法。由此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。

先来看注解,前面使用的单例类和companion object都只是在语法的形式上模仿了静态方法的调用方式,实际上它们都不是真正的静态方法。因此如果你在Java代码中以静态方法的形式去调用的话,你会发现这些方法并不存在。而如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法

注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。那么现在不管是在Kotlin中还是在Java中,都可以使用Prize.isRedpack()的写法来调用了。

再来看顶层方法,顶层方法指的是那些没有定义在任何类中的方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一个顶层方法,那么它就一定是静态方法。 如果我们新建一个Helper.kt的空文件(注意不是类,是空文件),在文件中定义一个方法:

fun doSomething() {
    println("do something")
}

刚才已经讲过了,Kotlin编译器会将所有的顶层方法全部编译成静态方法,那么这里又没有类我们要怎么调用这个doSomething()方法呢?如果是在Kotlin代码中调用的话,那就很简单了,所有的顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例,直接键入doSomething()即可。

但如果是在Java代码中调用,你会发现是找不到doSomething()这个方法的,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。那么这个doSomething()方法被藏在了哪里呢?我们刚才创建的Kotlin文件名叫作Helper.kt,于是Kotlin编译器会自动创建一个叫作HelperKt的Java类,doSomething()方法就是以静态方法的形式定义在HelperKt类里面的,因此在Java中使用HelperKt.doSomething()的写法来调用就可以了。

单例

因为一个类的伴生对象跟一个静态类一样,全局只能有一个。这让我们联想到了什么? 没错,就是单例对象。

object Resource {
    val name = "Name"
}

因为对象就是具有单一实现的数据类型,所以在kotlin中对象就是单例。
单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。

看一下自动生成的java代码:

public final class SingleTon {
   @NotNull
   private static String name;
   @NotNull
   public static final SingleTon INSTANCE;

   @NotNull
   public final String getName() {
      return name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      name = var1;
   }

   private SingleTon() {
   }

   static {
      SingleTon var0 = new SingleTon();
      INSTANCE = var0;
      name = "";
   }
}

这里官网中对object的介绍是: object declarations are initialized lazily, when accessed for the first time

为什么上面说它是饿汉式? 这个从上面反编译的代码可以看到确实有一个static的静态代码块,静态代码块具有懒惰加载特性,但它这个是加载特性,并不是我们说的:

public static Instance getInstance() {
    if (instance == null) {
        instance = new Instance();
    }
}

具体可以看: Kotlin Object Declarations Are Initialized in Static Block

委托(代理)

委托,也就是委托模式,它是23中经典设计模式中的一种,又名代理模式,在委托模式中,有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。

委托模式中,有三个角色:

  • 约束

    约束是接口或抽象类,它定义了通用的业务类型,也就是需要被代理的业务

  • 委托对象

    将约束定义的业务委托给别人

  • 被委托对象

    具体的业务逻辑执行者

类委托

在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。 kotlin中的委托可以算是对委托模式的官方支持。
Kotlin直接支持委托模式,更加优雅,简洁。Kotlin通过关键字by实现委托。

委托模式已被证明是类继承模式的一种很好的替代方案。

我们经常会打游戏进行升级,但是有时候因为我们的水平不行,没法升到很高的级别,这就出现了很多的游戏代练,他们帮我们代打,这就是一个委托模式:

  1. 定义约束类,定义具体需要委托的业务,这里的业务:

    interface IGamePlayer {
        fun upgrade()
    }
  2. 定义被委托对象,也就是游戏代练:

    class RealGamePlayer(private val name: String): IGamePlayer {
        override fun upgrade() {
            println("$name up up up")
        }
    }
  3. 定义委托对象

    // 注意这里不用再去实现IGamePlayer接口的upgrade方法,而是通过关键字by将实现委托给了player
    class DelegateGamePlayer(private val player: IGamePlayer): IGamePlayer by player

    定义了一个委托类DelegateGamePlayer, 现在游戏代练有很多,水平有高有低,如果发现水平不行,我们可以随时换,因此,我们把被委托对象作为委托对象的属性,通过构造方法传进去。

    fun main() {
        val realGamePlayer = RealGamePlayer("real 1")
        val delegateGamePlayer = DelegateGamePlayer(realGamePlayer)
        delegateGamePlayer.upgrade()
    }
    // real 1 up up up
属性委托

在软件的开发中,属性的getter和setter方法的代码有很多相同或相似的地方,虽然为每个类编写相同的代码是可行的,但这样会造成大量的代码冗余,而委托属性正是为解决这一问题而提出的。

属性委托是指一个类的某个属性值不是在类中直接定义,而是将其委托给一个代理类,从而实现对该类属性的统一管理。

在Kotlin中,有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现他们,但是很麻烦,为了解决这些问题,Kotlin标准提供了委托属性。

语法是val/var <属性名>: <类型> by <表达式>。在by后面的表达式是该委托,因为属性对应的get()set()会被委托给它的getValue()setValue()方法。 属性的委托不必实现任何的接口,但是需要提供一个getValue()函数(和setValue()——对于var属性)。

class Example {
    // 语法中,关键词by之后的表达式就是委托的类,而且属性的get()以及set()方法将被委托给这个对象的getValue()和setValue()方法。
    // 属性委托不必实现相关接口,但必须提供getValue()方法,如果是var类型的属性,还需要提供setValue()方法。  
    var property : String by DelegateProperty()
}

class DelegateProperty {
    var temp = "old"
    // getValue和setValue方法的参数是固定的
    // getValue和setValue方法必须用关键字operator标记 
    operator fun getValue(ref: Any?, p: KProperty<*>): String {
        return "DelegateProperty --> ${p.name} --> $temp"
    }

    operator fun setValue(ref: Any?, p: KProperty<*>, value: String) {
        temp = value
    }
}

fun test(){
    val e = Example()
    Log.d(JTAG, e.property)
    e.property = "new"
    Log.d(JTAG, e.property)
}

// 输出
DelegateProperty --> property --> old
DelegateProperty --> property --> new

像上面的DelegateProperty这样,被一个属性委托的类,我叫它被委托类,委托它的属性叫委托属性。其中:

  • 如果委托属性是只读属性即val,则被委托类需要实现getValue方法
  • 如果委托属性是可变属性即var,则被委托类需要实现getValue方法和setValue方法
  • getValue方法的返回类型必须是与委托属性相同或是其子类
  • getValue方法和setValue方法必须要用关键字operator标记

在上面类委托的示例中,有一个约束,里面定义了代理的业务逻辑。而委托属性呢? 其实就是上面的简化,被代理的逻辑就是这个属性的get/set方法。get/set会委托给被委托对象的getValue/setValue方法,因此被委托类需要提供getValue/setValue这两个方法。如果是val属性,只需提供getValue。如果是var属性,则getValue/setValue都需要提供。

属性委托,必须要提供getValue/setValue方法,但是这里有一个问题,就是这么复杂的参数,还要每次手写,太麻烦了。为了解决这个问题,Kotlin标准库中声明了2个包含所需operator方法的ReadOnlyProperty/ReadWriteProperty接口。

/**
 * Base interface that can be used for implementing property delegates of read-only properties.
 *
 * This is provided only for convenience; you don't have to extend this interface
 * as long as your property delegate has methods with the same signatures.
 *
 * @param T the type of object which owns the delegated property.
 * @param V the type of the property value.
 */
public fun interface ReadOnlyProperty<in T, out V> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

/**
 * Base interface that can be used for implementing property delegates of read-write properties.
 *
 * This is provided only for convenience; you don't have to extend this interface
 * as long as your property delegate has methods with the same signatures.
 *
 * @param T the type of object which owns the delegated property.
 * @param V the type of the property value.
 */
public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> {
    /**
     * Returns the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @return the property value.
     */
    public override operator fun getValue(thisRef: T, property: KProperty<*>): V

    /**
     * Sets the value of the property for the given object.
     * @param thisRef the object for which the value is requested.
     * @param property the metadata for the property.
     * @param value the value to set.
     */
    public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)
}

被委托类实现这两个接口其中之一就可以了,val属性实现ReadOnlyProperty,var属性实现ReadOnlyProperty。

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.properties.ReadWriteProperty

// val 属性委托实现
class Delegate1: ReadOnlyProperty<Any,String>{
    public override operator fun getValue(thisRef: Any, property: KProperty<*>): String {
        return "通过实现ReadOnlyProperty实现,name:${property.name}"
    }
}
// var 属性委托实现
class Delegate2: ReadWriteProperty<Any,Int>{
    override operator fun getValue(thisRef: Any, property: KProperty<*>): Int {
        return  20
    }

    override operator fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
       println("委托属性为: ${property.name} 委托值为: $value")
    }

}

class Test {
    val d1: String by Delegate1()
    var d2: Int by Delegate2()
}

fun main() {
   val test = Test()
    println(test.d1)
    println(test.d2)
    test.d2 = 100
    
    // 通过实现ReadOnlyProperty实现,name:d1
    // 20
    // 委托属性为: d2 委托值为: 100
}

标准委托

Kotlin通过属性委托的方式在Kotlin标准库中内置了很多工厂方法来实现属性委托,包括:

  • 延迟属性lazy properties
  • 可观察属性observable properties
  • map映射
延迟属性

延迟属性我们应该不陌生,也就是通常说的懒汉,在定义的时候不进行初始化,把初始化的工作延迟到第一次调用的时候。kotlin中实现延迟属性很简单, 来看一下。

lazy()是一个接收Lambda表达式作为参数并返回一个Lazy实例的函数,返回的实例可以作为实现延迟属性的委托。
也就是说,延迟属性第一次调用get()时,将会执行传递给lazy()函数的Lambda表达式并记录执行结果,后续所有对get()的调用只会返回记录的结果。

val lazyValue: String by lazy {
    Log.d(JTAG, "Just run when first being used")
    "value"
}

fun test(){
    Log.d(JTAG, lazyValue)
    Log.d(JTAG, lazyValue)
}

// 输出
Just run when first being used
value
value

可以看到,只有第一次调用了lazy里的日志输出,说明lazy方法块只有第一次执行了。按照个人理解,上面的lazy模块可以这么翻译:

String lazyValue;
String getLazyValue(){
    if(lazyValue == null){
        Log.d(JTAG, "Just run when first being used");
        lazyValue = "value";
    }
    return lazyValue;
}

void test(){
    Log.d(JTAG, getLazyValue());
    Log.d(JTAG, getLazyValue());
}

lazy延迟初始化是可以接受参数的,提供了如下三个参数:

  • LazyThreadSafetyMode.SYNCHRONIZED: 添加同步锁,使lazy延迟初始化线程安全
  • LazyThreadSafetyMode.PUBLICATION: 初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。
  • LazyThreadSafetyMode.NONE:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程

如果你指定的参数为LazyThreadSafetyMode.SYNCHRONIZED,则可以省略,因为lazy默认就是使用的LazyThreadSafetyMode.SYNCHRONIZED

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

val p by lazy { ... }

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

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懒加载函数就已经完成了,你可以直接使用它来替代之前的lazy函数

可观察属性

所谓可观察属性(observable),就是当属性发生变化时能自动拦截其变化,实现观察属性值变化的委托函数是Delegates.observable()。

该函数接受两个参数:

  • 初始值
  • 属性值变化时间的响应器(handler)

每当给属性值赋值时都会调用该响应器,该响应器主要有3个参数:

  • 被赋值的属性
  • 赋值前的旧属性值
  • 赋值后的新属性值

如果你要观察一个属性的变化过程,那么可以将属性委托给Delegates.observable

observable函数原型如下:

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
        ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
        override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
    }

接受2个参数:

  • initialValue:初始值
  • onChange:属性值被修改时的回调处理器,回调有三个参数property,oldValue,newValue,分别为:被赋值的属性、旧值与新值。

可观察属性对应的是我们常用的观察者模式,机制类似于我们给View增加Listener。同样的kotlin给了我们很方便的实现:

class User {
    // 默认值为0
    var name: Int by Delegates.observable(0) {
        prop, old, new -> Log.d(JTAG, "$old -> $new")
    }

    var gender: Int by Delegates.vetoable(0) {
        prop, old, new -> (old < new)
    }
}

fun test(){
    val user = User()
    user.name = 2    // 输出 0 -> 2        
    user.name = 1   // 输出 2 -> 1    

    user.gender = 2
    Log.d(JTAG, user.gender.string())   // 输出 2
    user.gender = 1
    Log.d(JTAG, user.gender.string())   // 输出 2
}

Delegates.observable()接受两个参数:初始值和修改时处理程序handler。 每当我们给属性赋值时会调用该处理程序(在赋值后执行)。 它有三个参数:被赋值的属性、旧值和新值。在上面的例子中,我们对user.name赋值,set变化触发了观察者,执行了Log.d代码段。

除了Delegates.observable()之外,我们还把gender委托给了Delegates.vetoable(),和observable不同的是,observable是执行了 set变化之后,才触发observable,而vetoable则是在set执行之前被触发,它返回一个Boolean,如果为true才会继续执行set。 在上面的例子中,我们看到在第一次赋值user.gender = 2时,由于2>0,所以old<new判断成立,所以执行了set方法,gender为2, 第二次赋值user.gender = 1则没有通过判断,所以gender依然为2。

vetoableobservable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效

如声明一个Int类型的属性vetoableProp,如果新的值比旧值大,则生效,否则不生效。

map委托

Map委托常常用来将Map中的key-value映射到对象的属性中,这经常出现在解析JSON或者其他动态事情的应用中。

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}
// 在这个例子中,构造函数接受一个映射参数
val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))
委托属性会从这个映射中取值(通过字符串键——属性的名称)
println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25
这也适用于var属性,如果把只读的Map换成MutableMap的话
class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

委托在kotlin中占有举足轻重的地位,特别是属性委托,lazy延迟初始化使用非常多,还有其他一些场景,比如在我们安卓开发中,使用属性委托来封装SharePreference,大大简化了SharePreference的存储和访问。在我们软件开发中,始终提倡的是高内聚,低耦合。而委托,就是内聚,可以降低耦合。另一方面,委托的使用,也能减少很多重复的样板代码。

委托模式是一种被广泛使用的设计模式,Kotlin的委托主要用来实现代码的重用,Kotlin默认支持委托模式,且不需任何模版方法。