[Kotlin]Kotlin学习笔记(三):Null检查机制及lateinit与by lazy(委托机制)的应用

>Null检查机制

    Kotlin中,使用 ? !!这两个安全检测操作符来进行安全值检测。其目的是为了消除Java中常见的NPE异常(NullPointerException)。要想正确理解Kotlin中这两个安全检测符,就要知道Java中触发NPE的原因主要有哪些:

 

  • 显示调用抛出NullPointerException的方法(较易被肉眼发现)
  • 对象忘记赋值、多线程运行造成的空指针、时序造成的空指针等初始化赋值原因导致的空指针

    在Kotlin中,对每个var和val都要求强制赋初值,否则在使用该值及其本身属性与方法时均为编译报错,这就使得上述两个原因中的第二个得到了很大程度的根除。首先要认识这两个符号的具体用法:

    看上图,我在main()函数中定义了一个字符串(String)变量t,并未附初值,由于String不是Nullable的数据类,所以此时IDE报错。由于t不可为null,因此此时对t=null,IDE也是会报错的。

    那么如果我想让 t=null 通过编译,我需要用利用 ? 符来标识这个类型:var t:String? ,告诉编译器,我定义的这个类型可为null,也就是nullable;

    此时 t=null 是可以通过IDE编译的,但能否调用它的属性或方法呢?如图:

    依旧是不行的,首先,它会告诉你这个值未被初始化,让后告诉你,只有 ? 或 !! 类型的调用才能对一个被表示为nullable的变量进行操作:

    有人可能会有疑问,既然此时t可为空了,并利用 ?.是可以使用其属性的。那对象本身为空,这样不就抛出NPE了吗?

    其实不然,?. 操作符被叫做安全调用符,如果程序运行到这里发现对象本身为空,那么就不执行任何指令,相当于:

 

if(obj==null);
else obj.fun();

    那么,现在再来回顾一下 ? 操作符的使用方法:

 

  • 定义一个var或者val时,加在类型后,表示这个值可为空(可以被赋值null)
  • 调用一个对象的方法时,在对象名后面加上,表示任意情况下允许使用这个对象(哪怕为空),但对象为空时,该语句不执行;换句话说,就是告诉编译器,我知道这个对象可能为空,所以你别BB了,赶紧给我编译通过。

    现在依旧有个问题:这不就是把对空值的try-catch检查放到每一个使用对象的时刻吗

  • 对啊,就是这样,利用IDE编码报错的机制,告诉你这个地方可能为空,你如果很自信,觉得绝对不会空,当然可以用安全调用符去调用语句(空掉的话这个语句不执行,很可能导致整个后续逻辑崩溃);本质上其实还是NPE,只不过把类似“免检”写成了"必检";

    那如果此时对这个对象是否为空并不那么自信怎么办?(虽然是使用了?已经在99%的程度上杜绝了NPE的问题)Kotlin为习惯了try-catch的JAVA程序员准备了另外一个符号—— !!

 

    这个符号被称作警告调用符,其意义是:如果这对象为空,直接终止程序,抛出NPE异常。例如,针对上面的对象t:

 

t!!.length

    程序运行到这,IDE并不会编译报错,但是在程序运行之后,会直接抛出NPE,然后终止程序。怎么办呢?老办法呗,try-catch:

    同样的,对一个返回值可为null的函数,需要这么定义:

    可以看到,这时候这个函数可以正常返回一个Int值或是一个null。

 

  • 总结一下:Kotlin的Null安全检查机制,就是利用IDE,强制在程序执行前,让编程者知道自己的某个语句会不会为空,并提前手动操作,杜绝所有可能造成空NPE的情形。

 

 

 

>lateinit 与 by lazy (委托机制)的应用与区别

    观察下面这个例子:

 

class DemoClass{
    var sex:String//未赋初值,IDE报错:必须初始化或为抽象属性
}
  • 在Kotlin中,一个类的成员属性必须被初始化(不为空),或者写为obj:class?=null

    这就解决了一个问题:在JAVA或C++等面对对象编程的语言中,程序员可能会依赖于默认赋值,而实际上这个默认赋值很可能为null,进而埋下了NPE隐患。

 

    可强制赋初值或是指明该值可空(利用 ),有时候又不是那么方便,比如我要为Person类的成员属性name赋值,一个人一定是有名字的,即不可为null,但我在定义这个类时又不可能为其赋值,只有在实例化这个类的对象时,才有可能通过构造器为其赋值。在更复杂一点的情况中,我可能还要基于某个人的身份证位数去自动计算这个人的性别,这个显然是需要在构造函数/初始化函数中进行计算的,没法在定义类时就进行初始化,怎么解决这个问题呢?这里,我们需要利用到lateinit

 

class Person(personName:String){
    var name=personName // 主构造器直接赋值
    lateinit var id:String // 延迟初始化
    lateinit var sex:String // 延迟初始化
    constructor(personName: String,idCode:String):this(personName){
        id=idCode // 次构造器中赋值
        //idCode->sex , 为性别赋值 , 详细代码略
    }
}

    注意到,lateinit标示着某个变量将会在稍后的操作中进行初始化,是用来对不可空的var变量(可读可写)进行操作的,对val变量(只读变量)无法进行操作。那么,要解决val变量的延迟初始化问题,要怎么处理呢?答案是只能通过主构造器进行赋值处理
     那么,by lazy又是用来做什么的呢?其实这是Kotlin中的委托机制(属性委托):

 

 

val/var <属性名>: <类型> by <表达式>

>什么是Kotlin中的委托机制?

    委托机制包含类委托属性委托,通过关键字 by 进行使用。

 

interface MyInterface{
    fun myPrint()
}
//被委托的类
class MyInterfaceImpl(val x:Int):MyInterface{
    override fun myPrint() {
        println(x)
    }
}
//委托类
class DelegateClass(i:MyInterface):MyInterface by i{//委托

}

fun textFun(){
    val d:MyInterfaceImpl = MyInterfaceImpl(10086)
    val s:DelegateClass = DelegateClass(d)
    s.myPrint() // 输出10086
    //或
    DelegateClass(s).myPrint() // 输出10086
}

    这是一个类委托的例子,在委托类DelegateClass中,MyInterfaceImpl是具体主题类MyInterface业务的主要实现者,委托类间接也集成了MyInterface,但是其所有业务由MyInterfaceImpl实现,委托类只负责选择业务的具体执行者,而不关心具体的业务(等于就是把业务交给了MyInterfeceImpl)。

    在样例代码中,委托类DelegateClass中通过xxxx by i,使得内部存储了一个i的实例,并允许调用i:MyInterface接口的所有方法。上面的代码转化成java代码就是:

 

public interface MyInterface{
    public void myPrint();
}
public class MyInterfaceImpl implement MyInterface{
    private int x;
    @override
    public MyInterfaceImpl(int x){
        this.x=x;
    }
    public void myPrint(){
        println(x)
    }
}
public class DelegateClass{
    private MyInterface i;
    public DelegateClass(MyInterface i){
        this.i=i;
    }
    public void myPrint(){
        i.myPrint();
    }
}

    另一种委托就是属性委托了。今天提到的by lazy就是一种属性委托。

    lazy()其实是一个函数(也叫延迟属性委托),其同时接受lambda表达式的形式,下面这个by lazy语句实际上“{}”中间的就是一个lambda表达式:

 

val test:String by lazy {
        print("First")
        "Try"
    }

    println(test) // 输出 First Try
    println(test) // 输出 Try

    对于第一次打印,先执行了print("First")语句,并为test赋值"Try",然后打印出tset;第二次打印时,由于lazy()函数已完成委托它的任务(初始化赋值),因此,不在print("Frist")。

 

 

  •    也就是说,为某个对象进行by lazy操作,只有在该对象第一次被使用时,才进行初始化操作

    既然是表达式可以用 by 来进行委托,有人一定会想到,在kotlin中,if语句也是表达式,能不能进行这样的操作呢:

 

 

val test:String by if(7>5){
        "yes"
    }else{
        "no"
    }

   答案是否定的,实际上,属性委托中的表达式必须重写两个方法:getValue()和setValue(),如我们可以进行下面的操作:

    var test:String by testFun() // 必须为var

    println(test)
    test="10086"
    println(test)//输出内容见下

class testFun(){
    var s:String?=null
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String { // 必须重写,被委托对象被调用时调用该函数
        return "A:${property.name} 被委托!,其值为 $s "
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { // 必须重写,被委托对象被赋值时调用该函数
        println("B:${property.name} 被赋值为 $value")
        s=value
    }
}

    输出为:

 

A:test 被委托!,其值为 null 
B:test 被赋值为 10086
A:test 被委托!,其值为 10086 

  看着似乎和上文的by lazy完全不一样。其实by lazy是一个工厂方法,也被叫做Kotlin中的标准委托,同样属于常用标准委托的还有观察者模式中的观察参数:(顺便一提)

Delegates.observable([初始化String参数]){
    代码块
}

   其具体定义为:

 

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)
    }

 

    举个使用例子:

 

class PC{
    var type:String by Delegates.observable("神船大法好"){
        //接受一个lambda表达式
        a,b,c -> println("$a : $b -> $c")
    }
}
var pc:PC = PC()
pc.type="联想"
pc.type="戴尔"

输出:
var PC.type: kotlin.String : 神船大法好 -> 联想
var PC.type: kotlin.String : 联想 -> 戴尔


>lateinit 与 by lazy的区别

 

 

    其实看完上面的内容,已经可以很方便的认识到lateinit与lazy的区别了,首先从根本上,二者的用法与机制是完全不同的,但在使用时二者往往处于一种并行的状态,因此还是有一些需要进行区别的内容:

 

 

 

  • lateinit针对var;bylazy 针对val(但不是所有by ... 都是针对val)。
  • lateinit仅可用于nullable的类型,如String,如果是Int,是不行的。
  • lateinit标示的值可被多次赋值;而by lazy标示的值仅在第一次被调用时赋值,且有且仅有一次;
  • lateinit仅用于类中的成员属性;by lazy 可用于类中与函数中;
  • lateinit支持Backing Fields机制

    那么,如果想在函数和类中使用类似的lateinit,要怎么做呢?可以使用Delegates.notNull<*>(),如:

 

var fr:String by Delegates.notNull<String>()
val fl:Int by Delegates.notNull<Int>()


 

 

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值