原文:Kotlin singletons with argument ——object has its limits
作者:Christophe Beyls
译者:却把清梅嗅

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进行声明,您可以获得安全有效的单例实现。

图:无尽的孤独——单例(译者:作者的描述让我想起了一个悲情的角色,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? = nullfun getInstance(arg: A): T {val i = instanceif (i != null) {return i}return synchronized(this) {val i2 = instanceif (i2 != null) {i2} else {val created = creator!!(arg)instance = createdcreator = nullcreated}}}
}
复制代码

请注意,为了使算法正常工作,这里需要将@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(): UserDaocompanion 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,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的博客或者Github。

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

  • 我的Android学习体系
  • 关于文章纠错
  • 关于知识付费

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

  1. C语言中的带参宏和带参函数的区别

    C语言中的带参宏和带参函数的区别 (1) 带参函数中的形参是变量,因此有类型检查.而带参宏只是简单的字符串替换. (2) 从程序执行的过程来看,带参宏是在预处理阶段被预处理器处理的.而带参函数是在程序 ...

  2. 关于自定义异常中为什么带参构造器需要显示调用父类异常的带参构造器

    在听课的时候听到自定义异常时,视频上讲的定义异常的时候如果是带参构造器需要显示调用父类异常的带参构造器,原因是什么呢? 首先我们需要看一下Exception和ERROR的父类Throwable的源码: ...

  3. Java中的带参方法

    1.有返回值的带参方法 看下面代码: 1)字符串型: public String zhaZhi( String shuiGuo){ return shuiGuo+"汁"; 代码括号 ...

  4. 【译】如何在 Android 中使用 Retrofit, Moshi, Coroutines Recycler View

    翻译说明: 原标题: How-To: Retrofit, Moshi, Coroutines & Recycler View for REST Web Service Operations w ...

  5. oracle 存储过程带入参,oracle中带参存储过程的使用

    Oracle中存储过程带参分为:输入参数(in)和输出参数(out) 例如: 1 create or replace procedure out_test(v_user in emp.user_nam ...

  6. 构造方法--带参构造方法

    根据前面章节定义的Cat类,现在Cat类中添加带参的Cat方法,注意参数名不能和要赋值的变量名相同: public Cat(String newName, int newMonth, double n ...

  7. [译]带你揭开Kotlin中属性代理和懒加载语法糖衣

    翻译说明: 原标题: How Kotlin's delegated properties and lazy-initialization work 原文地址: https://medium.com/t ...

  8. [译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?

    翻译说明: 原标题: Sequences - a Pragmatic Approach 原文地址: https://proandroiddev.com/sequences-a-pragmatic-ap ...

  9. [kotlin]kotlin中的伴生对象(companion object)到底是个什么东西?

    文章目录 写在前面 第一步,写出kotlin代码 第二步,转成Java代码 第三步,查看java代码 第四步.得出结论 1.`companion object`的作用 2. 写在`companion ...

最新文章

  1. 【转】Odoo装饰器: one装饰
  2. lvdt 运放全波整流接线方式_20种电工最常见照明灯接线电路图另附开关、插座安装技术交底...
  3. 进阶Frida--Android逆向之动态加载dex Hook(三)
  4. 20位数字转化成6位不重复码_人力资源管理浅析身份证数字号码编排常识甄别年龄、性别、籍贯…...
  5. 关于 SAP Spartacus SSR 服务器返回的响应是否应该被缓存的问题
  6. Jenkins+Ant自动布署war
  7. sshpass简介及安装
  8. presto求时间差
  9. C++/CLI学习入门
  10. android9.0 framewrok.jar push到system/framework不起作用,解决方式
  11. 对策《四川省本科毕业论文(设计)抽检实施细则》(二)专业能力
  12. 对事件流的小故事理解
  13. waves系统服务器,waves服务器:插件挂多了电脑宕机了?Impact Server服务器拯救你的电脑CPU!...
  14. TortoiseSVN 打Tags
  15. 大掌门2显示服务器繁忙,大掌门2新手攻略_游戏功能详细攻略(新手攻略二)_软吧...
  16. MFC的导航窗格浮窗设计
  17. Windows 下使用 grub2 制作美观的维护U盘
  18. android 支付宝sdk接入详解
  19. 【Math ML】Lagrange Multipliers 拉格朗日乘数
  20. 包括遗传算法在内的现代优化算法简介

热门文章

  1. java null转integer_java – 从null到int可以转换?
  2. 【Linux 4,2021最新Java笔试题及答案
  3. 第 1 章 Readme
  4. 第 0 章 阳哥MySQL高级
  5. c语言中声明外部函数需要添加的关键字,C语言中声明和定义的区别——分析extern关键词。...
  6. mysql新增字段会锁表_MySQL锁(二)表锁:为什么给小表加字段会导致整个库挂掉?...
  7. windows python安装包_Python-3.9安装包(windows版)
  8. 科大讯飞语音识别芯片_科大讯飞造家电专用语音芯片 市场机会在哪里?
  9. TS高级类型内置工具类型
  10. eclipse git拉取失败_Git(四):分支