一周入门Kotlin(三)
2017-05-28 11:04
387 查看
本章有2点主要内容,一个是关于属性的委托,二是关于对象接口和委托的问题探讨。
于是,我在Activity中的onCreate()初始化了Student对象,并给他赋值了,代码如下:
为了使用mStudent的属性/方法,以后我每调用该对象,就应该多写一个问号,因为他有可能为null,这里仅仅是有可能。但作为程序员,我认为我已经初始化了,无需多此一举,那么问题就来了,能不能去掉这玩意(?)
例子2: 之前我做过电商的项目中有很多类里面出现了价格的问题,我们都知道价格为了方便,应该都是double类型,但是有的时候因为计算的问题,很容易造成某个对象的的价格为负数。为此,我希望每次为某个类重新设值的时候都不能为负数。
有的同学就说了,之前不是讲解了全局变量默认会自动生成setter/getter方法吗?我们可以重写setter/getter来达到我们的目的。
那么问题来了,如果有100个类里面都有一至两个价格的属性,我们是否要重写n次呢,是否有更加统一的做法来管理属性的内容?答案是有的
2.而上面第二个问题无非就是怕设置某个值到某个属性(间接调用setter方法)里面出现为负数的错误操作。
3.我们确实可以重写setter/getter,但是亲爱的乡亲们啊,一个项目的规模如此之大,是皇军也不会傻到一个个去重(抢)写(粮)吧(食),于是…
委托属性就这么产生了。大概的思想是这样,我们创建一个统一管理的类来处理我们要实现的业务逻辑,而在需要管理的属性上做个简单的标记,就能达到我们的想要的效果了。
这里提到三个问题:
1. 首先作为统一管理的类有什么特征?该类必须是ReadWriteProperty的子类。
2. 我们要管理什么东西呢?上面说到了,当然是属性的存(set)取(get )问题了
3. 如何将该类标记到某个属性上?在属性声明后面添加 by XXX()
接下来创建一个统一管理类,代码如下:
接下来,我创建了一个类,并将管理类设置给某个属性,从这里可以看出 该对象不用做任何初始化:
在MainActivity中,我创建了该对象,并给p赋值后重新读取。当然肯定会调用p对象内部的setter()&getter()方法。除此之外,还会调用管理类的内部的setValue()&getValue()方法。
这两方法可以监听哪个对象哪个属性传入什么值,取出什么值。在内部传值的时候我将数据缓存起来,等到取值的时候我再将数据放回去。如果我们有什么业务,都可以在这两方法中完成。完成后,哪些属性需要实现该规则的,都在后面添加 by Delegate()即可。
通过委托属性,问题1只需要在getValue()方法中实现如果当时属性值为null,就抛出空指针异常即可;而问题2只需要在setValue()方法中判断是否小于0即可,如果小于0则等于0
在Application中,如果我想在onCreate()的时候才创建该dataBase对象,那么在声明dataBase对象的时候采用lazy属性委托,可以无需先创建该对象或者先将该对象置为null。
这段代码告诉系统,一开始dataBase是不会创建的,直到了onCreate()方法被调用,发现此刻需要dataBase这个对象,才调用了MySqliteOpenHelper(ctx:Context)单参构造器创建该对象。
1.首先这段代码会创建lazy对象。查看源码发现,lazy本身是一个接口,并在内部提供了value值(这里指代dataBase的属性值 而T代表MySqliteOpenHelper的类型)。
当获取某个被lazy装备的属性时,就会调用可扩展的getValue方法,并返回接口的value值。
2.如下图展示了他的子类情况:
SynchronizedLazyImpl:默认创建的对象 保证只有在某一线程中才能创建该对象
UnsafeLazyImpl:一个线程不安全的对象 多条线程调用可能会有问题
SafePublicationLazyImpl:线程安全对象,你可以创建在不同线程中多次创建 但是他只会返回第一次创建的实例
3.回头发现我们刚刚调用的是lazy的单参构造器,内部创建了一个lazy的子类,默认就是线程安全的,并重写了value属性的get()方法,源码如下:
如果你觉得下面的代码每次都要复制粘贴,可以将其封装到一个新类里:
该构造器内部的是怎么写的呢?
为了让Product里面的name能够一开始就定义,而在需要的时候才重新赋予新的值,我们可以定义自己的委托,只需修改系统noNull的setValue().
接下来我们让学生类和教师类实现该接口,并分别多实现了学习和教书的方法。当然,每个学生和老师也应该有自己的姓名,注意继承的代码中Person后面已经无需在写主构造器,也就是()了,因为他是接口不是类,代码如下:
换做是java类,我们发现相同的属性肯定是无法抽取的到接口中的,只能定义一个抽象的父类才能这么做。但是kotlin的接口是”无状态”,所以可以将属性的声明写到父类上,但却无法赋值。同时,接口还可以实现内部的函数,这点也是比较新颖的做法,Person接口的代码如下:
作为子类,主需要实现自己的业务,有必要的情况下重写即可,接口的name已经声明了但是没赋值 所以这里必须重新定义,我们让构造器来声明该变量:
类的委托主要是用来声明一个类大部分主要功能.这里涉及到接口的2种用法:
* 如果想让自己的类本身实现某个组件的大部分功能,只需要实现接口即可(如上面的学生实现了Person接口,并默认实现了吃和玩的功能)
* 如果想让其他对象来来实现某个组件的大部分功能,就需要实现类的委托的(下面我们根据学生的例子来实现这个例子)。
上面我们重新规划了学生类,希望在其他类中(如MainActivity)重新定义学生类的吃饭行为。那么在其他类中如何操作该实例呢?
这里有必要告诉大家object这个关键字到底是什么意思,一般他代表的就是声明一个实例,对!你没看错。连对象我们都可以直接定义。而且我们的静态变量也使用了object关键字。如下我创建了Student对象,可以直接调用构造器,也可以这样:
如果想让大部分功能自己实现,直接实现接口即可;如果想他类实现,用类委托即可。
举个例子:我们很多时候在同一APP中定义了不同样式但功能统一的ToolBar.这时,我们可以将ToolBar委托给他的使用者,让不同的页面对ToolBar做不同的处理。
再举个例子:如果一个Fragment在一个APP中频繁使用,你也可以抽取统一的功能。利用类的委托,在不同的Activity中控制该Fragment的行为。
属性委托
1.现有代码的问题
例子1:下面的代码中,我创建了一个学生对象,并且希望不要一开始就为学生的姓名年龄和身高赋值,为了达到目的,我选择了将所有的属性置为null:class Student { var name: String? = null var age: Int? = null var height: Double? = null fun test() { Log.i("IT520", "SIMPLE TEST METHOD !") } }
于是,我在Activity中的onCreate()初始化了Student对象,并给他赋值了,代码如下:
class MainActivity : AppCompatActivity() { var mStudent: Student?= null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mStudent=Student() mStudent?.name="zhangsan" mStudent?.age=18 mStudent?.height=1.70 } }
为了使用mStudent的属性/方法,以后我每调用该对象,就应该多写一个问号,因为他有可能为null,这里仅仅是有可能。但作为程序员,我认为我已经初始化了,无需多此一举,那么问题就来了,能不能去掉这玩意(?)
例子2: 之前我做过电商的项目中有很多类里面出现了价格的问题,我们都知道价格为了方便,应该都是double类型,但是有的时候因为计算的问题,很容易造成某个对象的的价格为负数。为此,我希望每次为某个类重新设值的时候都不能为负数。
有的同学就说了,之前不是讲解了全局变量默认会自动生成setter/getter方法吗?我们可以重写setter/getter来达到我们的目的。
那么问题来了,如果有100个类里面都有一至两个价格的属性,我们是否要重写n次呢,是否有更加统一的做法来管理属性的内容?答案是有的
2.委托属性
1.上面第一个问题无非就是怕获取某个属性(间接调用getter方法)报空指针而已。2.而上面第二个问题无非就是怕设置某个值到某个属性(间接调用setter方法)里面出现为负数的错误操作。
3.我们确实可以重写setter/getter,但是亲爱的乡亲们啊,一个项目的规模如此之大,是皇军也不会傻到一个个去重(抢)写(粮)吧(食),于是…
委托属性就这么产生了。大概的思想是这样,我们创建一个统一管理的类来处理我们要实现的业务逻辑,而在需要管理的属性上做个简单的标记,就能达到我们的想要的效果了。
这里提到三个问题:
1. 首先作为统一管理的类有什么特征?该类必须是ReadWriteProperty的子类。
2. 我们要管理什么东西呢?上面说到了,当然是属性的存(set)取(get )问题了
3. 如何将该类标记到某个属性上?在属性声明后面添加 by XXX()
接下来创建一个统一管理类,代码如下:
/** * Created by lean on 2017/5/27. * T 委托属性的类型 */ class Delegate<T> : ReadWriteProperty<Any?, T> { var mValue: T? = null /** * @param thisRef 类的引用 * @param property 属性元数据 * */ override fun getValue(thisRef: Any?, property: KProperty<*>): T { Log.i("IT520", "getValue $thisRef -- ${property.name}") return mValue!! } /** * @param thisRef 类的引用 * @param property 被设置值的属性 .name获取属性名 * @param value 被设置的新值 * */ override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { Log.i("IT520", "setValue $thisRef -- ${property.name} -- $value ") mValue=value } }
接下来,我创建了一个类,并将管理类设置给某个属性,从这里可以看出 该对象不用做任何初始化:
class Example{ var p: String by Delegate() }
在MainActivity中,我创建了该对象,并给p赋值后重新读取。当然肯定会调用p对象内部的setter()&getter()方法。除此之外,还会调用管理类的内部的setValue()&getValue()方法。
var example = Example() example.p="Hello" Log.i("IT520","log ${example.p} ")
这两方法可以监听哪个对象哪个属性传入什么值,取出什么值。在内部传值的时候我将数据缓存起来,等到取值的时候我再将数据放回去。如果我们有什么业务,都可以在这两方法中完成。完成后,哪些属性需要实现该规则的,都在后面添加 by Delegate()即可。
通过委托属性,问题1只需要在getValue()方法中实现如果当时属性值为null,就抛出空指针异常即可;而问题2只需要在setValue()方法中判断是否小于0即可,如果小于0则等于0
3.Lazy代理的使用
除了我们自己定义的委托属性,系统还开放了一些极为好用的委托属性,Lazy就是系统提供的一个单纯的get委托属性控制接口,比如下面的代码,我首先创建了一个MySqliteOpenHelper类用来实现数据库的创建,在类的构造器中,需要传入ctx上下文参数:class MySqliteOpenHelper(val context: Context) : SQLiteOpenHelper(context, DB_NEME, null, DB_VERSION) { companion object{ public var DB_NEME ="xxx.db" public var DB_VERSION =1 } override fun onCreate(db: SQLiteDatabase?) { } override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { } }
在Application中,如果我想在onCreate()的时候才创建该dataBase对象,那么在声明dataBase对象的时候采用lazy属性委托,可以无需先创建该对象或者先将该对象置为null。
这段代码告诉系统,一开始dataBase是不会创建的,直到了onCreate()方法被调用,发现此刻需要dataBase这个对象,才调用了MySqliteOpenHelper(ctx:Context)单参构造器创建该对象。
class App: Application(){ //这里使用了lazy的单参构造器 将一个需要被延迟创建的对象的构造器作为参数传进来。 val dataBase :SQLiteOpenHelper by lazy { MySqliteOpenHelper(applicationContext) } override fun onCreate() { super.onCreate() val db=dataBase.writableDatabase } }
4.Lazy代理源码剖析
val dataBase :SQLiteOpenHelper by lazy { MySqliteOpenHelper(applicationContext) }
1.首先这段代码会创建lazy对象。查看源码发现,lazy本身是一个接口,并在内部提供了value值(这里指代dataBase的属性值 而T代表MySqliteOpenHelper的类型)。
当获取某个被lazy装备的属性时,就会调用可扩展的getValue方法,并返回接口的value值。
public interface Lazy<out T> { public val value: T public fun isInitialized(): Boolean } @kotlin.internal.InlineOnly public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
2.如下图展示了他的子类情况:
SynchronizedLazyImpl:默认创建的对象 保证只有在某一线程中才能创建该对象
UnsafeLazyImpl:一个线程不安全的对象 多条线程调用可能会有问题
SafePublicationLazyImpl:线程安全对象,你可以创建在不同线程中多次创建 但是他只会返回第一次创建的实例
3.回头发现我们刚刚调用的是lazy的单参构造器,内部创建了一个lazy的子类,默认就是线程安全的,并重写了value属性的get()方法,源码如下:
@kotlin.jvm.JvmVersion public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer) private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer @Volatile private var _value: Any? = UNINITIALIZED_VALUE // final field is required to enable safe publication of constructed instance private val lock = lock ?: this override val value: T //当获取该对象的时候,就会调用get() get() { val _v1 = _value if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } return synchronized(lock) { val _v2 = _value if (_v2 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") (_v2 as T) } else { //这里拿到开始的构造器 如果构造器有问题则报异常 val typedValue = initializer!!() _value = typedValue initializer = null //默认返回值在最后一行 隐藏了return typedValue } } } override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }
5.系统提供的优秀委托属性
在kotlin的properties包下,有个Delegates类,其提供了几个优秀的 常见的委托属性:public object Delegates { //不允许为null public fun <T: Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar() //检查传入的数据 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) } //决定传进来的数据是否保留 public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) { override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue) } }
Observable检查传入数据
不知道大家是否还记得我们之前谈到的问题2,现在,通过Observable对象,我们可以观察到内部的结果如果你觉得下面的代码每次都要复制粘贴,可以将其封装到一个新类里:
class Product { var id: Long = 0 //0.0 代表的是该属性一开始的默认值 var price: Double by Delegates.observable(0.0) { //这里有3个参数property代表price属性 后2者代表其参数 property, old, newValue -> if (newValue < 0) Log.i("IT520","newValue lt 0") else Log.i("IT520","newValue gt 0") } }
vetoable决定是否保留某个数据
为了根据条件决定是否保留某个值,我们可以使用vetoable对象:class Product { var id: Long = 0 var price: Double by Delegates.vetoable(0.0) { property, old, newValue -> //只有大于等于0才能被保留 newValue >= 0.0 } }
notNull定义某个不为null的属性
在开发的过程中,比如在MainActivity中,我们创建一个全局变量,但是希望他在onCreate()中才初始化,所以一开始就必须置为null,以后用到该属性,后面都必须添加”?”,特别麻烦,现在,我们可以使用notNull委托来解决该问题了。如下给出一个实例:class Product { var id: Long = 0 //这里定义了一个商品名 并告诉系统 该对象不为null var name :String by Delegates.notNull() }
该构造器内部的是怎么写的呢?
public fun <T: Any> notNull(): ReadWriteProperty<Any?, T> = NotNullVar() private class NotNullVar<T: Any>() : ReadWriteProperty<Any?, T> { private var value: T? = null //这里已经说明了 如果你一开始没给该值赋新的数据,就直接调用他的get() 则会抛出一个IllegalStateException public override fun getValue(thisRef: Any?, property: KProperty<*>): T { return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.") } public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { this.value = value } }
为了让Product里面的name能够一开始就定义,而在需要的时候才重新赋予新的值,我们可以定义自己的委托,只需修改系统noNull的setValue().
//定义自己的委托属性 private class SingleNotNullVar<T: Any>() : ReadWriteProperty<Any?, T> { private var value: T? = null //这里已经说明了 如果你一开始没给该值赋新的数据,就直接调用他的get() 则会抛出一个IllegalStateException public override fun getValue(thisRef: Any?, property: KProperty<*>): T { return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.") } public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { when (this.value?) null -> this.value = value else -> Unit } }
类的委托
1.Kotlin中的接口&实现接口的对象
kotlin的接口定义与java的类似,比如你想定义一个人类的共同特征,可以使用Person接口。interface Person { //定义了吃 玩的行为 fun eat() fun play() }
接下来我们让学生类和教师类实现该接口,并分别多实现了学习和教书的方法。当然,每个学生和老师也应该有自己的姓名,注意继承的代码中Person后面已经无需在写主构造器,也就是()了,因为他是接口不是类,代码如下:
class Student : Person { var name: String = "" override fun eat() { Log.i("IT520", "$name 正在吃饭..") } override fun play() { Log.i("IT520", "$name 正在玩..") } //定义学习的方法 fun study() { Log.i("IT520", "$name 正在学习..") } } class Teacher : Person { var name: String = "" override fun eat() { Log.i("IT520", "$name 正在吃饭..") } override fun play() { Log.i("IT520", "$name 正在玩..") } //定义学习的方法 fun study() { Log.i("IT520", "$name 正在教书..") } }
换做是java类,我们发现相同的属性肯定是无法抽取的到接口中的,只能定义一个抽象的父类才能这么做。但是kotlin的接口是”无状态”,所以可以将属性的声明写到父类上,但却无法赋值。同时,接口还可以实现内部的函数,这点也是比较新颖的做法,Person接口的代码如下:
interface Person { var name: String //定义了吃 玩的行为 fun eat(){ Log.i("IT520", "$name 正在吃饭..") } fun play(){ Log.i("IT520", "$name 正在玩..") } }
作为子类,主需要实现自己的业务,有必要的情况下重写即可,接口的name已经声明了但是没赋值 所以这里必须重新定义,我们让构造器来声明该变量:
class Student(override var name: String) : Person { //定义学习的方法 fun study() { Log.i("IT520", "$name 正在学习..") } } class Teacher(override var name: String) : Person { //定义学习的方法 fun study() { Log.i("IT520", "$name 正在教书..") } }
2.类委托的作用
上面我们已经讲解了属性委托的作用:它类似于AOP编程中对属性的传递的控制。类的委托主要是用来声明一个类大部分主要功能.这里涉及到接口的2种用法:
* 如果想让自己的类本身实现某个组件的大部分功能,只需要实现接口即可(如上面的学生实现了Person接口,并默认实现了吃和玩的功能)
* 如果想让其他对象来来实现某个组件的大部分功能,就需要实现类的委托的(下面我们根据学生的例子来实现这个例子)。
//创建了类的代理 代理由外部传进来的参数决定 class Student(var p:Person) : Person by p{ //定义学习的方法 fun study() { Log.i("IT520", "$name 正在学习..") } }
上面我们重新规划了学生类,希望在其他类中(如MainActivity)重新定义学生类的吃饭行为。那么在其他类中如何操作该实例呢?
var student = Student(object : Person { //重新定义人物名称 override var name = "zhangsan" //重写接口吃的行为 override fun eat() { Log.i("IT520","小明 和 $name 一起去吃饭") } }) student.eat()
这里有必要告诉大家object这个关键字到底是什么意思,一般他代表的就是声明一个实例,对!你没看错。连对象我们都可以直接定义。而且我们的静态变量也使用了object关键字。如下我创建了Student对象,可以直接调用构造器,也可以这样:
object Student{ ... }
3.常见类委托都用在哪里
从上面的例子中我们可以看出,接口主要是抽取一个概念的大部分主要功能。如果想让大部分功能自己实现,直接实现接口即可;如果想他类实现,用类委托即可。
举个例子:我们很多时候在同一APP中定义了不同样式但功能统一的ToolBar.这时,我们可以将ToolBar委托给他的使用者,让不同的页面对ToolBar做不同的处理。
再举个例子:如果一个Fragment在一个APP中频繁使用,你也可以抽取统一的功能。利用类的委托,在不同的Activity中控制该Fragment的行为。
相关文章推荐
- 一周入门Kotlin(五)
- 一周入门Kotlin(一)
- 一周入门Kotlin(二)
- 一周入门Kotlin(四)
- 一周入门Kotlin(一)
- Kotlin入门资料搜集
- Kotlin快速入门(3) -- 其他常用新特性
- Kotlin入门学习
- Kotlin入门(2)让App开发变得更容易
- Kotlin入门第一课:从对比Java开始
- Android kotlin入门与基础语法二
- 【Kotlin从入门到深坑】之基础类型
- Kotlin 新手别慌,可先了解这些入门知识
- Kotlin入门(12)类的概貌与构造
- [置顶] Kotlin入门教程——目录索引
- Kotlin 语言高级安卓开发入门
- Kotlin 入门基础语法学习笔记
- Kotlin 语言入门宝典 | Android 开发者 FAQ Vol.5
- kotlin开发Android入门篇七Kotlin与Java相互调用
- Kotlin 入门初体验