Kotlin空安全最佳实践

Kotlin 空安全最佳实践

前言

Kotlin 语言在设计时,重点关注了Java 的空指针异常(NullPointerException,简称NPE),然而在使用时,由于NPE出现的原因及其多样化,对应的处理方式也有很多种,很多时候单靠一个variable != null不能优雅地解决所有问题,在不同场合,我们可以通过不同的方式,来简化书写,明确语义。

Kotlin 产生 NPE 的原因

官网 文档中,列出了以下几种NPE产生原因:

  • 显式调用 throw NullPointerException()

  • 使用了下文描述的 !! 操作符;

    !!,非空断言运算符,作用是将任何值转换为非空类型,若该值为空,则会抛出NPE,例如:

    val b:String? = null
    val c = b!!.length
    println(c)
    /**
    运行报错,输出:
    Exception in thread "main" kotlin.KotlinNullPointerException
    at preview.ChainCallKt.main(ChainCall.kt:25)
    **/
  • 有些数据在初始化时不一致,例如当:

    • 传递一个在构造函数中出现的未初始化的 this 并用于其他地方(“泄漏 this”);
    class StudentUtils {
    
        ......
    
        constructor() {
            add(this)
        }
    
        fun add(studentUtils: StudentUtils) {
            //do something
        }
    }

    在构造方法中传入了 this ,但是 this 可能没有初始化完全(部分成员未初始化),可能造成 NPE

    • 超类的构造函数调用一个开放成员,该成员在派生中类的实现使用了未初始化的状态;
    class MyProgressBar(context: Context) : ProgressBar(context) {
        val bounds = Rect()
        override fun setProgress(progress: Int) {
            super.setProgress(progress)
            with(bounds) {
                val right = left + ((progress.toFloat() / max) * width)
                set(left, top, right.toInt(), bottom)
            }
        }
    }
    //在别处调用
    val progress = MyProgressBar(this)
    // java.lang.NullPointerException: Attempt to read from field 'int android.graphics.Rect.left' on a null object reference

    此处 bounds 的属性 left引用,回抛出 NPE,原因在于 ProgressBar的构造方法中调用了子类的setProgress方法,而此时子类中 bounds 尚未初始化

  • Java 互操作:

    • 企图访问平台类型的 null 引用的成员;

    Java 中所有的类型都是可空的,在 Kotlin 中,Java 声明的类型会在编译期间映射成相应的 Kotlin 类型,比如java.lang.String 对应 kotlin 中的 kotlin.String! ,这时,可能由于在 Kotlin 中引用了可能为空的 java

    String 类型,导致 NPE

    val list = ArrayList() // non-null (constructor result)
    list.add("Item")
    val size = list.size // non-null (primitive int)
    val item = list[0] // platform type inferred (ordinary Java object)

    item 可能为空,因为 list[0]获取的是一个java对象,是可空的。故而在使用诸如item.subString(1)时,可能会抛出 NPE

    • 用于具有错误可空性的 Java 互操作的泛型类型,例如一段 Java 代码可能会向 Kotlin 的 MutableList 中加入 null,这意味着应该使用 MutableList 来处理它;
    //StudentUtils.kt
    class StudentUtils {
    
        val mStudents:MutableList =
                mutableListOf()
    
    
        fun getStudentList():MutableList {
            return mStudents
        }
    
        fun add() {
            //编译报错!!! 因为列表是 MutableList,不允许添加 null 元素
            mStudents.add(null)
        }
    }
    //SecondActivity.java

    上面 StudentUtils类里面定义了一个 MutableList 的 mStudent ,所以在add方法企图添加 null 到集合时,编译直接报错,但是在Java中却可以:

    public class SecondActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            StudentUtils utils = new StudentUtils();
            //编译通过
            utils.getStudentList().add(null);
    
        }
    }
    • 由外部 Java 代码引发的其他问题。
    public class SecondActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }
    
        public static MainActivity.Student getStudent() {
            return null;
        }
    }

    SecondActivity 中的getStudent 方法返回了 null ,在 kotlin 类 MainActivity中引用,编译通过,但是回抛出异常:

    StudentUtils().mStudents.add(SecondActivity.getStudent())
    //Caused by: java.lang.IllegalStateException: SecondActivity.getStudent() must not be null

空检查应用场景

  1. 只关注非空场景,不需要处理为空的case

    1. 使用 ? ,声明可空类型,避免抛出NPE

      如果变量可能为空,则在定义时一定要使用 ?标示,当为空时,程序不会往下执行,也不会抛出NPE

      // 对于一些可能返回为空的方法,在赋值时一定要注意
      fun getSudent(name: String?): Student? {
            if (!name.isNullOrEmpty())
                return Student(name!!, 24)
            else
                return null
        }
      //如果不加 ? ,就会抛出NPE
      val student = getSudent("")
      print(student?.name)
      // 使用安全的类型转换 as? ,如果转换不成功则返回 null ,而不是抛出 ClassCastException
      val aInt: Int? = a as? Int //不会抛出异常,失败则返回 null
      val aInt: Int? = a as Int //可能抛出 ClassCastException
      
      // 使用可空的集合类型
      val nullableList: List = listOf(1, 2, null, 4)
      val intList: List = nullableList.filterNotNull()
    2. 使用 !! 声明非空类型,为空则抛出NPE

      print(student!!.name)

      上述 1 中代码里,将 student 从 ? 改为 !!修饰,则 student 为空时回抛出 NPE

    3. 使用 let 标注代码块

      //student使用 ? 标注,则let里面一定不为空,为空则let内部所有代码都不会执行,它是空安全的
      student?.let {
        print(it.name)
        print(it.age)
      }
      //不实用 ? 标注student,则 let 里面每次使用 student(it),都需要判断是否为空
      student.let {
        print(it?.name)
        print(it?.age)
      }
  2. 非空和不非空,都需要处理

    1. 使用三元操作符 ?:

      val name:String = student?.name ?: "defaultName"

      当为空时,输出的是 ?:后面的默认字符串

      fun foo(node: Node): String? {
        val parent = node.getParent() ?: return null
        val name = node.getName() ?: throw IllegalArgumentException("name expected")
        // ……
      }
    2. 使用if else

      Kotlin 中,if else 是表达式,是有返回值的,所以不仅能做空判断,还能作为返回值

      //1. if else 作为表达式,返回String
      val name:String = if (student != null) {
              //student.name可能为空,所以用 !! 转成非空字符串
               student.name!!
           } else {
               "defaultName"
           }
      //2. if else 作为条件判断
      if (student != null) {
                print(student.name)
            } else{
                print("other action")
            }
    3. 使用类型判断 is 来判断是否为空,如果符合 is 条件,则 student 不为空

      val student = getSudent("")
      
      if (student is Student) {
        student.name
      }
    4. 也可以通过 when 来判断是否为空

      when (student) {
        null -> {
            print("null")
        }
        else -> print(student.name)//else 之后,student一定不为空,所以不需要用?修饰
      }
      //对于类型不确定的,比如Any类型( 类似于Java object 类型),还可以结合whenis
      val student:Any = getSudent("") as Any
      
      when (student) {
        is Student -> { print(student.name) }
        is Teacher -> { print("is teacher") }
      }

引用资料

  1. 官网: https://kotlinlang.org/docs/reference/null-safety.html
  2. 空指针异常: https://medium.com/keepsafe-engineering/an-in-depth-look-at-kotlins-initializers-a0420fcbf546
  3. kotlin 空安全: https://stackoverflow.com/questions/34498562/in-kotlin-what-is-the-idiomatic-way-to-deal-with-nullable-values-referencing-o

你可能感兴趣的:(Android个人学习笔记,Kotlin)