[译]Object的局限性——Kotlin中的带参单例模式

栏目: 后端 · 发布时间: 5年前

内容简介:在与类不同,这样

Kotlin 中,单例模式被用于替换该编程语言中不存在的 static 成员和字段。 你通过简单地声明 object 以创建一个单例:

object SomeSingleton
复制代码

与类不同, object 不允许有任何构造函数,如果有需要,可以通过使用 init 代码块进行初始化的行为:

object SomeSingleton {
    init {
        println("init complete")
    }
}
复制代码

这样 object 将被实例化,并且在初次执行时,其 init 代码块将以线程安全的方式懒惰地执行。 为了这样的效果, Kotlin 对象实际上依赖于 Java静态代码块 。上述Kotlin的 object 将被编译为以下等效的 Java 代码:

public final class SomeSingleton {
   public static final SomeSingleton INSTANCE;

   private SomeSingleton() {
      INSTANCE = (SomeSingleton)this;
      System.out.println("init complete");
   }

   static {
      new SomeSingleton();
   }
}
复制代码

这是在JVM上实现单例的首选方式,因为它可以在线程安全的情况下懒惰地进行初始化,同时不必依赖复杂的双重检查加锁 (double-checked locking) 等加锁算法。 通过在 Kotlin中 简单地使用 object 进行声明,您可以获得安全有效的单例实现。

[译]Object的局限性——Kotlin中的带参单例模式

图:无尽的孤独——单例(译者:作者的描述让我想起了一个悲情的角色,Maiev Shadowsong)

传递一个参数

但是,如果初始化的代码需要一些额外的参数呢?你不能将任何参数传递给它,因为 Kotlinobject 关键字不允许存在任何构造函数。

有些情况下,将参数传递给单例初始化代码块是被推荐的方式。 替代方法要求单例类需要知道某些能够获取该参数的外部组件 (component) ,但违反了关注点分离的原则并且使得代码不可被复用。

为了缓解这个问题,该外部组件可以是 依赖注入系统 。这的确是一个具有可行性的解决方案,但您并不总是希望使用这种类型的库——并且,在某些情况下您也无法使用它,就像在接下来的Android示例中我将会所提到的。

在Kotlin中,您必须通过不同的方式去管理单例的另一种情况是,单例的具体实现是由外部 工具 或库(比如 RetrofitRoom 等等)生成的,它们的实例是通过使用 Builder 模式或 Factory 模式来获取的——在这种情况下,您通常将单例通过 interfaceabstract class 进行声明,而不是 object

一个Android示例

Android 平台上,您经常需要将 Context 实例作为参数传递给单例组件的初始化代码块中,以便它们可以获取 文件路径读取系统设置开启Service 等等,但您还希望避免对其进行静态引用(即使是 Application 的静态引用在技术上是安全的)。 有两种方法可以实现这一目标:

  • 提前初始化 :在运行任何(几乎)其他代码之前,通过在 Application.onCreate() 中调用初始化所有组件,此时 Application 是可用的——这个简单的解决方案的主要缺点是它是通过阻塞主线程的方式来减慢应用程序启动,并初始化了所有组件, 甚至包括那些不会立即使用的组件 。另一个鲜为人知的问题是,在调用此方法之前, Content Provider 也许已经被实例化了(正如文档中所提到的),因此,若 Content Provider 使用全局的相关组件,则您必须保证能够在 Application.onCreate() 之前初始化该组件,否则您的申请依然可能会导致应用崩溃。
  • 延迟初始化 :这是推荐的方法。组件是单例,返回其实例的函数持有 Context 参数。该单例将在第一次调用该函数时使用此参数进行创建和初始化操作。这需要一些同步机制才能保证线程的安全。使用此模式的标准 Android 组件的示例是 LocalBroadcastManager
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
复制代码

可复用的Kotlin实现方式

我们可以通过封装逻辑来懒惰地在 SingletonHolder 类中创建和初始化带有参数的单例。

为了使该逻辑的线程安全,我们需要实现一个同步算法,它是最有效的算法,同时也是最难做到的——它就是 双重检查锁定算法 (double-checked locking algorithm)

open class SingletonHolder<out T, in A>(creator: (A) -> T) {
    private var creator: ((A) -> T)? = creator
    @Volatile private var instance: T? = null

    fun getInstance(arg: A): T {
        val i = instance
        if (i != null) {
            return i
        }

        return synchronized(this) {
            val i2 = instance
            if (i2 != null) {
                i2
            } else {
                val created = creator!!(arg)
                instance = created
                creator = null
                created
            }
        }
    }
}
复制代码

请注意,为了使算法正常工作,这里需要将 @Volatile 注解对 instance 成员进行标记。

这可能不是最紧凑或优雅的 Kotlin 代码,但它是为双重检查锁定算法生成最行之有效的代码。请信任 Kotlin 的作者:实际上,这些代码正是从 Kotlin 标准库中的lazy() 函数的实现中直接借用的,默认情况下它是同步的。它已被修改为允许将参数传递给 creator 函数。

有鉴于其相对的复杂性,它不是您想要多次编写(或者阅读)的那种代码,实际上其目标是,让您每次必须使用参数实现单例时,都能够重用该 SingletonHolder 类进行实现。

声明 getInstance() 函数的逻辑位置在singleton类的伴随对象内部,这允许通过简单地使用单例类名作为限定符来调用它,就好像 Java 中的静态方法一样。 Kotlin 的伴随对象提供的一个强大功能是它也能够像任何其他对象一样从基类继承,从而实现与仅静态继承相当的功能。

在这种情况下,我们希望使用 SingletonHolder 作为单例类的伴随对象的基类,以便在单例类上重用并自动公开其 getInstance() 函数。

对于 SingletonHolder 类构造方法中的 creator 参数,它是一个函数类型,您可以声明为一个内联 (inline) 的lambda,但更常用的情况是 作为一个函数引用的依赖交给构造器 ,最终其代码如下所示:

class Manager private constructor(context: Context) {
    init {
        // Init using context argument
    }

    companion object : SingletonHolder<Manager, Context>(::Manager)
}
复制代码

现在可以使用以下语法调用单例,并且它的初始化将是 lazy 并且线程安全的:

Manager.getInstance(context).doStuff()
复制代码

当三方库生成单例实现并且 Builder 需要参数时,您也可以使用这种方式,以下是使用 Room 数据库的示例:

@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object : SingletonHolder<UsersDatabase, Context>({
        Room.databaseBuilder(it.applicationContext,
                UsersDatabase::class.java, "Sample.db")
                .build()
    })
}
复制代码

注意:当 Builder 不需要参数时,您只需使用 lazy 的属性委托:

interface GitHubService {

    companion object {
        val instance: GitHubService by lazy {
            val retrofit = Retrofit.Builder()
                    .baseUrl("https://api.github.com/")
                    .build()
            retrofit.create(GitHubService::class.java)
        }
    }
}
复制代码

我希望这些代码能够给您带来一些启发。如果您有建议或疑问,请不要犹豫,在评论部分开始讨论,感谢您的阅读!

--------------------------广告分割线------------------------------

关于我

Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 :heart:,也欢迎关注我的博客或者 Github

如果您觉得文章还差了那么点东西,也请通过 关注 督促我写出更好的文章——万一哪天我进步了呢?


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

六度分隔

六度分隔

邓肯·J·瓦茨 / 陈禹 / 中国人民大学出版社 / 2011-3 / 46.00元

正如副标题所表明的,《六度分隔:一个相互连接的时代的科学》的基本内容是介绍一门正在形成中的新科学——关于网络的一般规律的科学。有这样一门科学吗?它的内容和方法是什么?近年来,这门学科有什么实质性的进展吗?在《六度分隔:一个相互连接的时代的科学》中,作者根据自己的亲身经历娓娓道来,用讲故事的方式,对于这些问题给出了令人信服的回答 除了简要的背景和总结以外,《六度分隔:一个相互连接的时代的科学》......一起来看看 《六度分隔》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具