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)
而经过刚才的封装之后,我们就无法进行传参了。这个问题也不难解决,只需要借助之前学习的高阶函数就可以轻松搞定。回到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 = [email protected] // 引用外部类的成员
// 输出
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
中,throw
和try
都是表达式,这意味着它们可以被赋值给一个变量。这个在处理一些边界问题的时候确实非常有用:
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)
val 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()方法其实也并不是静态方法,companion object这个关键字实际上会在类的内部创建一个伴生类,而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
实现委托。
委托模式已被证明是类继承模式的一种很好的替代方案。
我们经常会打游戏进行升级,但是有时候因为我们的水平不行,没法升到很高的级别,这就出现了很多的游戏代练,他们帮我们代打,这就是一个委托模式:
定义约束类,定义具体需要委托的业务,这里的业务:
interface IGamePlayer { fun upgrade() }
定义被委托对象,也就是游戏代练:
class RealGamePlayer(private val name: String): IGamePlayer { override fun upgrade() { println("$name up up up") } }
定义委托对象
// 注意这里不用再去实现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 { ... }
现在再来看这段代码,是不是觉得更有头绪了呢?实际上,by lazy并不是连在一起的关键字,只有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。
vetoable
与 observable
一样,可以观察属性值的变化,不同的是,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默认支持委托模式,且不需任何模版方法。
- 邮箱 :[email protected]
- Good Luck!