kotlin延迟初始化和密封类详细讲解

目录
  • 对变量延迟初始化
  • 使用密封类优化代码

对变量延迟初始化

Kotlin语言有许多特性,包括变量不可变,变量不可为空,等等。这些特性都是为了尽可能地保证程序安全而设计的,但是有些时候这些特性也会在编码时给我们带来不少麻烦。

比如,你的类中存在许多全局变量实例,为了保证他们能够满足kotlin的空指针检查语法标准,你不得不做出许多的非空判断保护才行,即使你非常确定它们不会为空。

通过一个例子:

class MainActivity:AppCompatActivity(),View.OnClickListener{
  private var adapter:MsgAdapter?=null
  override fun onCreate(savedInstanceState:Bundle?){
  ...
  adapter=MsgAdapter(msgList)
  ...
}
  override fun onClick(v:View?){
  ...
  adapter?.notifyItemInserted(msgList.size-1)
  ...
}
}

这里我们将adapter设置了全局变量,但是它的初始化工作是在onCreate()方法中进行的,因此不得不先将adapter赋值为null,同时把它的类型声明成MsgAdapter?。

虽然我们会在onCreate()方法中对adapter进行初始化,同时能确保onClick()方法必然在onCreate()方法之后才会调用,但是我们在onClick()方法中调用adapter的任何方法时仍然要进行判空处理才行,否则编译肯定无法通过。

而当你的代码中有了越来越多的全局变量实例时,这个问题就会变得越来越明显,到时候你可能必须编写大量额外的判空处理代码,只是为了满足kotlin编译器的要求。

这个问题其实是可以解决的,而且非常简单,那就是对全局变量进行延迟初始化。

延迟初始化使用的是lateinit关键字,它可以告诉kotlin编译器,我会在晚些时候对这个变量进行初始化,这样我就不用在一开始的时候就将它赋值为null了。

接下来我就就使用延迟初始化的方式对上述代码进行优化,如下所示:

class MainActivity:AppCompatActivity(),View.OnClickListener{
  private lateinit var adapter:MsgAdapter
  override fun onCreate(savedInstanceState:Bundle?){
  ...
  adapter=MsgAdapter(msgList)
  ...
}
  override fun onClick(v:View?){
  ...
  adapter.notifyItemInserted(msgList.size-1)
  ...
}
}

可以看到,我们在adapter变量的前面加上lateinit关键字,这样就不用在一开始的时候将它赋值为null,同时类型声明也就可以改成MsgAdapter了,由于MsgAdapter是不可为空的类型,所以我们在onClick()方法中也就不再需要进行判空处理,直接调用adaper的任何方法就可以了。

当然,使用lateinit关键字也不是没有任何风险,如果在adapter变量还没有初始化的情况下就直接使用它,那么程序就一定会奔溃,并且抛出一个UninitializedPropertyAccessException异常。

所以,当你对一个全局变量使用了lateinit关键字时,请一定要确保它在被任何地方调用之前已经完成了初始化工作,否则Kotlin将无法保证程序的安全性。

另外,我们还可以通过代码来判断一个全局变量是否已经完成了初始化,这些某些时候能够有效地避免对某一个变量进行初始化操作,示例代码如下:

class MainActivity:AppCompatActivity(),View.onClickListener{
private lateinit var adapter:MsgAdapter
override fun onCreate(savedInstanceState:Bundle?){
...
if(!::adapter.isInitialized){
adapter=MsgAdapter(msgList)
}
...
}
}

::adapter.isInitialized可用于判断adapter变量是否已经初始化。然后我们再对结果进行取反,如果还没有初始化,那就立即对adapter变量进行初始化,否则什么都不做。

使用密封类优化代码

密封类通常可以结合RecyclerView适配器中的ViewHolder一起使用,它可以在很多时候帮助你写出更加规范和安全的代码。

通过一个例子:

新建一个Kotlin文件

interface Result{
}
class  Success(val msg:String):Result
class Failure(val error:Exception):Result

这里定义一个Result接口,用于表示某个操作的执行结果,接口中不用编写任何内容,然后定义了两个类去实现Result接口:一个Success类用于表示成功时的结果,一个Failure类用于表示失败时的结果。

接下来定义一个getResultMsg()方法,用于获取最终执行结果的信息,代码如下:

fun getResultMsg(result: Result)=when(result){
    is Success-> result.msg
    is Failure-> result.error
    else -> throw  IllegalArgumentException()
}

getResultMsg()方法中接收一个Result参数,我们通过when语句来判断:如果Result属于Success,那么就返回成功的消息;如果Result属于Failure,那么就返回错误信息。接下来我们不得不再编写一个else条件,否则Kotlin编译器会认为这里缺少条件分支,代码将无法编译通过。但实际上Result的执行结果只可能是Success或Failure,这个else条件永远走不到,所以我们在这里直接抛出异常,只是为了满足kotlin编译器的语法检查而已。

但是else还有一个潜在风险,如果我们现在新增一个Unkown类并实现Result接口,用于表示未知的执行结果,但是如果没有在getResultMsg()方法中添加相应的条件分支,编译器这种情况下不会提醒我们而是直接运行进入else条件里面。

这个时候密封类可以解决这个问题,密封类的关键字是sealed class,将Result接口改造成密封类的写法:

sealed class Result{
}
class  Success(val msg:String): Result()
class Failure(val error:Exception):Result()

这个时候会发现getResultMsg()方法中的else条件已经不需要了,如下所示

fun getResultMsg(result: Result)=when(result){
    is Success-> result.msg
    is Failure-> result.error
}

这是因为当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你每一个子类所对应的条件全部处理。这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。而如果我们新增一个Unknown类,并也让它继承自Result,此时getResultMsg()方法就一定会报错,必须新增一个Unknown的条件分支才能让代码编译通过。

密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层的实现机制所限制的。

接下来看一下它是如何结合MsgAdapter中的ViewHolder一起使用,并优化一下MsgAdapter中的代码。

比如在MsgAdapter中的onBindViewHolder()方法中存在一个没有实际作用的else条件,只是抛出一个异常而已。对于这部分的代码,我们就可以借助密封类的特性来进行优化。新建一个MsgViewHolder.kt文件,其中加入如下代码:

sealed class MsgViewHolder(view:View):RecyclerView.ViewHolder(view){
}
class  LeftViewHolder(view: View):MsgViewHolder(view){
    val leftMsg:TextView=view.findViewById(R.id.leftMsg)
}
class RightViewHolder(view: View):MsgViewHolder(view){
    val rightMsg:TextView=view.findViewById(R.id.rightMsg)
}

这里我们定义了一个密封类MsgViewHolder,并让他继承自RecyclerView.ViewHolder,然后让leftViewHolder和RightViewHolder继承自MsgViewHolder。这样就相当于密封类MsgViewHolder只有两个已知子类,因此在when语句中只要处理这两种情况的条件分支即可。

修改MsgAdapter代码,如下所示:

class MsgAdapter(val msgList:List<msg>):RecyclerView.Adapter<MsgViewHolder>(){
...
override fun onBindViewHolder(holder:MsgViewHolder,position:Int){
val msg=msgList[position]
when(holder){
is LeftViewHolder -> holder.leftMsg.text=msg.content
is RightViewHolder -> holder.rightMsg.text=msg.content
}
}
...
}

这里我们将RecycleView.Adapter的泛型指定成刚刚定义的密封类MsgViewHolder,这样onBindViewHolder()方法传入的参数就变成了MsgViewHolder。然后我们只要在when语句当中处理LeftViewHolder和RightViewHolder这两种情况就可以了,else也不需要了。这种RecyclerView适配器的写法更加规范也更加推荐。

到此这篇关于kotlin延迟初始化和密封类详细讲解的文章就介绍到这了,更多相关Kotlin延迟初始化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Kotlin浅析延迟初始化与密封类的实现方法

    目录 一.lateinit延迟初始化关键字 二.使用密封类优化代码 一.lateinit延迟初始化关键字 Kotlin中很多语法特性,如变量不可变,变量不可为空,等等 这些特性都是为了尽可能地保证程序安全而设计的,比如你的类中存在很多全局变量实例,为了保证它们的能够满足Kotlin的空指针检查语句标准,你不得不做非空判断保护,即使你非常确定它们不会为空. 下面距离看一下 : class MainActivity : AppCompatActivity() { private var s: Str

  • kotlin延迟初始化和密封类详细讲解

    目录 对变量延迟初始化 使用密封类优化代码 对变量延迟初始化 Kotlin语言有许多特性,包括变量不可变,变量不可为空,等等.这些特性都是为了尽可能地保证程序安全而设计的,但是有些时候这些特性也会在编码时给我们带来不少麻烦. 比如,你的类中存在许多全局变量实例,为了保证他们能够满足kotlin的空指针检查语法标准,你不得不做出许多的非空判断保护才行,即使你非常确定它们不会为空. 通过一个例子: class MainActivity:AppCompatActivity(),View.OnClick

  • Kotlin构造函数与成员变量和init代码块执行顺序详细讲解

    目录 在Kotlin中经常看到主构造函数.成员变量.init代码块(也叫初始化器),它们的执行时机和顺序是什么样的呢?看一下官方的示例: class InitOrderDemo(name: String) { val firstProperty = "First property: $name".also(::println) init { println("First initializer block that prints ${name}") } val se

  • Kotlin修饰符lateinit(延迟初始化)案例详解

    Kotlin定义变量一般有如下写法 lateinit var name: String var age: String? = null 那么用lateinit 修饰和下面那种有什么区别呢,我们来看一下这两行代码反编译成java代码是什么样子的. @NotNull public String name; @Nullable private String age; @NotNull public final String getName() { String var10000 = this.name

  • Java 超详细讲解对象的构造及初始化

    目录 如何初始化对象 构造方法 特性 默认初始化 就地初始化 如何初始化对象 我们知道再Java方法内部定义一个局部变量的时候,必须要初始化,否则就会编译失败 要让这串代码通过编译,很简单,只需要在正式使用a之前,给a设置一个初始值就好那么对于创造好的对象来说,我们也要进行相对应的初始化我们先写一个Mydate的类 public class MyDate { public int year; public int month; public int day; /** * 设置日期: */ pub

  • GoLang RabbitMQ TTL与死信队列以及延迟队列详细讲解

    目录 TTL 死信队列 延迟队列 Go实现延迟队列 TTL TTL 全称 Time To Live(存活时间/过期时间).当消息到达存活时间后,还没有被消费,就会被自动清除.RabbitMQ可以设置两种过期时间: 对消息设置过期时间. 对整个队列(Queue)设置过期时间. 如何设置 设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期. 设置消息过期时间使用参数:expiration,单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这

  • RocketMQ延迟消息超详细讲解

    目录 一.什么是延时消息 二.延时消息等级 三.延时消息使用场景 四.延时消息示例 五.延时消息实现原理 一.什么是延时消息 当消息写入到Broker后,不能立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息,称为延时消息. 二.延时消息等级 RocketMQ延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的.默认支持18个等级的延迟消息,延时等级定义在RocketMQ服务端的MessageStoreConfig类中的如下变量中: // MessageStoreConf

  • java15新功能的详细讲解

    目录 1. JEP 339 爱德华曲线算法(EdDSA) 2. JEP 360:Sealed Classes(密封类)预览 3. JEP 371:Hidden Classes(隐藏类) 4. JEP 372:移除 Nashorn JavaScript 引擎 5. JEP 373:重新实现 DatagramSocket API 6. JEP 374:禁用和废弃偏向锁(Biased Locking) 7. JEP 375:instanceof 类型匹配 (二次预览) 8. JEP 377:ZGC:

  • 详细讲解HDFS的高可用机制

    目录 互斥机制 写流程 读流程 恢复流程 在Hadoop2.X之前,Namenode是HDFS集群中可能发生单点故障的节点,每个HDFS集群只有一个namenode,一旦这个节点不可用,则整个HDFS集群将处于不可用状态. HDFS高可用(HA)方案就是为了解决上述问题而产生的,在HA HDFS集群中会同时运行两个Namenode,一个作为活动的Namenode(Active),一个作为备份的Namenode(Standby).备份的Namenode的命名空间与活动的Namenode是实时同步的

  • Java详细讲解依赖注入的方式

    目录 Spring中的三种依赖注入方式 可能遇到的问题 Spring中的三种依赖注入方式 Field Injection :@Autowired注解的一大使用场景就是Field Injection Constructor Injection :构造器注入,是我们日常最为推荐的一种使用方式Setter Injection: Setter Injection也会用到@Autowired注解,但使用方式与Field Injection有所不同,Field Injection是用在成员变量上,而Sett

随机推荐