Kotlin中带参数的单例模式

108

我正在尝试将一个Android应用从Java转换为Kotlin。该应用程序中有一些单例。对于没有构造函数参数的单例,我使用了伴生对象。还有一个需要构造函数参数的单例。

Java代码:

public class TasksLocalDataSource implements TasksDataSource {

    private static TasksLocalDataSource INSTANCE;

    private TasksDbHelper mDbHelper;

    // Prevent direct instantiation.
    private TasksLocalDataSource(@NonNull Context context) {
        checkNotNull(context);
        mDbHelper = new TasksDbHelper(context);
    }

    public static TasksLocalDataSource getInstance(@NonNull Context context) {
        if (INSTANCE == null) {
            INSTANCE = new TasksLocalDataSource(context);
        }
        return INSTANCE;
    }
}

我的 Kotlin 解决方案:

class TasksLocalDataSource private constructor(context: Context) : TasksDataSource {

    private val mDbHelper: TasksDbHelper

    init {
        checkNotNull(context)
        mDbHelper = TasksDbHelper(context)
    }

    companion object {
        lateinit var INSTANCE: TasksLocalDataSource
        private val initialized = AtomicBoolean()

        fun getInstance(context: Context) : TasksLocalDataSource {
            if(initialized.getAndSet(true)) {
                INSTANCE = TasksLocalDataSource(context)
            }
            return INSTANCE
        }
    }
}

我有什么遗漏吗?线程安全?懒加载?

有几个类似的问题,但我不喜欢答案 :)


INSTANCE 属性公开了公共的 setter,这有点尴尬。 - miensol
@miensol,有没有其他方法可以将参数(Context)传递给伴生对象? - LordRaydenMK
1
将Context实例存储在全局单例对象中(无论是Java还是Kotlin)会创建内存泄漏:https://dev59.com/GWct5IYBdhLWcg3wqvTL#11908685 - yole
6
@yole,如果应用程序上下文是单例模式,则不算内存泄漏。 - LordRaydenMK
1
@LordRaydenMK,如果你在安卓上使用 Kotlin 的话,我认为你的代码可以有4个改进。我已经创建了一个Gist并附上了一些解释,请查看:https://gist.github.com/gaplo917/f186d5c541fbc0d6f77f9b720ec4694c - Gary LO
显示剩余2条评论
14个回答

158

这里有一个来自Google架构组件的简洁替代方案示例代码,它使用also函数:

class UsersDatabase : RoomDatabase() {

    companion object {

        @Volatile private var INSTANCE: UsersDatabase? = null

        fun getInstance(context: Context): UsersDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
            }

        private fun buildDatabase(context: Context) =
            Room.databaseBuilder(context.applicationContext,
                    UsersDatabase::class.java, "Sample.db")
                    .build()
    }
}

3
我不确定为什么在同步块内部有INSTANCE ?:?因为该块仅在INSTANCE为null时调用,所以为什么还要再次检查INSTANCE是否为null呢?有人能解释一下吗? - Sandip Fichadiya
10
@SandipSoni,阅读有关双重检查锁定的内容:https://dev59.com/xmMl5IYBdhLWcg3w1py6 - kuhnroyal
4
我看到这段代码有两个问题。
  1. 您总是需要传递上下文(或任何其他已定义的变量),但可能在您想要检索单例的时间不可用。
  2. 假设您在第二次使用时传递了不同的上下文。那意味着什么?您只会获得旧的单例。这对我来说似乎是一种反面模式。
- A1m
3
关于第二点:第二次传递不同的上下文并不重要,因为应用程序上下文被用来构建数据库。你传递的 Context 对象仅用于检索应用程序上下文。 - mfb
2
@MathiasBrandt 在这种情况下说“Context”本身应该是单例可能是正确的,但总体上问题是关于创建这个单例的模式。我觉得这种行为是未定义和误导的。 - A1m
显示剩余2条评论

56

线程安全的解决方案 # 仅需编写一次,多次使用;

创建一个类来实现单例逻辑并持有单例实例是一个不错的解决方案,示例如下。

该类使用synchronized块和双重检查锁定的方式在多线程环境中消除了竞态条件,并使用它来实例化单例对象。

SingletonHolder.kt

open class SingletonHolder<out T, in A>(private val constructor: (A) -> T) {

    @Volatile
    private var instance: T? = null

    fun getInstance(arg: A): T =
        instance ?: synchronized(this) {
            instance ?: constructor(arg).also { instance = it }
        }
}

使用方法

现在,在您想要成为单例的每个类中,编写一个扩展上述类的companion objectSingletonHolder是一个通用类,它接受目标类和其所需参数的类型作为泛型参数。它还需要目标类的构造函数的引用,该构造函数用于实例化一个实例:

class MyManager private constructor(context: Context) {

    fun doSomething() {
        ...
    }

    companion object : SingletonHolder<MyManager, Context>(::MyManager)
}

最后:

MyManager.getInstance(context).doSomething()

7
假设MyManager需要多个构造函数参数,如何使用SingletonHolder处理它。 - Abhi Muktheeswarar
@AbhiMuktheeswarar:由于SingletonHolder是一个通用类,不幸的是它的通用类型不能在运行时动态定义,可能对于每个输入参数的计数,我们应该定义一个像这样的类:https://hastebin.com/avunuwiviv.m - aminography
2
@AbhiMuktheeswarar 使用 Pair 来表示 A。然后:伴生对象:SingletonHolder<MyManager,Pair<A,B>> { MyManager(it.first,it.second) } - Dan Brough
@aminography 我尝试使用SingletonHolder来创建我的房间数据库的单例,但它无法编译。可能是因为Room在底层使用了kotlinx.serialization。是的,Android API 30。我注意到在github上有一个关于这个问题的开放问题。 - ThanosFisherman
1
我真的很喜欢这个解决方案,但是:(1)我对扩展伴生对象感到紧张(如果其他东西也想要扩展它怎么办?),(2)我对所有的 !! 感到紧张,(3)我认为应该在你可能创建单例时更明确地表达出来,而不是只是想访问它。因此,我制作了自己的版本,受到这个答案的启发:https://gist.github.com/chriscoomber/80a9e66dd4fba92e7480d29c925440e9 - Chrispher
显示剩余4条评论

23

我不完全确定你为什么需要这样的代码,但这是我最好的尝试:

class TasksLocalDataSource private constructor(context: Context) : TasksDataSource {
    private val mDbHelper = TasksDbHelper(context)

    companion object {
        private var instance : TasksLocalDataSource? = null

        fun  getInstance(context: Context): TasksLocalDataSource {
            if (instance == null)  // NOT thread safe!
                instance = TasksLocalDataSource(context)

            return instance!!
        }
    }
}

这与您所写的类似,并且具有相同的API。

几点说明:

  • 不要在此处使用lateinit。它有不同的用途,在此处使用可空变量更为理想。

  • checkNotNull(context)是什么意思?在 Kotlin 中,context 永远不会为空,编译器已经实现了所有的检查和断言。

更新:

如果您只需要一个懒加载初始化的 TasksLocalDataSource 实例,则可以使用一堆懒加载属性(在对象或包级别内):

val context = ....

val dataSource by lazy {
    TasksLocalDataSource(context)
}

1
@LordRaydenMK 我的看法是,不要通过Java转换开始学习Kotlin。先阅读官方文档,尝试从头开始编写完全等效的Kotlin实现,而不是进行Java转换。"不要用Java方式编写Kotlin"的概念非常重要,因为大多数时候你不需要Java(臭名昭著的)编码模式(因为它是为Java设计的)。 - Gary LO
1
@GaryLO 我在try.kotlinglang.org上做了koans。就像Hadi Hariri(https://twitter.com/hhariri)在一次会议上说的那样(现在找不到链接了)……一些Kotlin比没有Kotlin更好作为起点。我写在这里的原因是想以Kotlin的方式尝试并实践。谢谢。 - LordRaydenMK
2
请注意,此解决方案不是线程安全的。如果多个线程同时尝试访问它,则可能存在两个单例实例并进行初始化。 - BladeCoder
第一个代码片段显然不是线程安全的,而第二个是线程安全的。 - voddan
1
@musooff TasksLocalDataSource(context) 是一个构造函数,它要么返回一个对象,要么抛出一个异常。 - voddan
显示剩余5条评论

9
你可以声明一个 Kotlin 对象,重载 "invoke" 运算符
object TasksLocalDataSource: TasksDataSource {
    private lateinit var mDbHelper: TasksDbHelper

    operator fun invoke(context: Context): TasksLocalDataSource {
        this.mDbHelper = TasksDbHelper(context)
        return this
    }
}

无论如何,我认为您应该将TasksDbHelper注入到TasksLocalDataSource中,而不是注入Context。

这种方法是最直接的,也是将 Class 转换为函数的 invoke 函数的一个很好的用例。我在我的 Coinverse 应用程序中使用了这个模式来创建我的 _Repository_。 - AdamHurwitz
3
但它是线程安全的吗?我看到很多人赞扬这个,实际上是从这篇两年前的热门Medium文章中借鉴来的,而这篇文章又是从Kotlin“by lazy”源代码本身借鉴而来,正如作者所述。这个答案看起来更干净(我讨厌添加样板文件,更不用说类了),但我对在多线程场景中使用这个单例深感忧虑。 - leRobot

5
如果您想以更简单的方式向单例传递参数,我认为这种方法更好、更短。
object SingletonConfig {

private var retrofit: Retrofit? = null
private const val URL_BASE = "https://jsonplaceholder.typicode.com/"

fun Service(context: Context): Retrofit? {
    if (retrofit == null) {
        retrofit = Retrofit.Builder().baseUrl(URL_BASE)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }
    return retrofit
}

你可以用这种简单的方式调用它

val api = SingletonConfig.Service(this)?.create(Api::class.java)

你的方法正在填充Retrofit对象,但它返回了可空值。这很令人困惑,也是错误的。 - Saman Sattari

5

synchronized()这个方法在通用标准库中已被弃用,可以选择以下替代方法:

class MySingleton private constructor(private val param: String) {

    companion object {
        @Volatile
        private var INSTANCE: MySingleton? = null

        @Synchronized
        fun getInstance(param: String): MySingleton = INSTANCE ?: MySingleton(param).also { INSTANCE = it }
    }
}

1
导入此内容的是kotlin.jvm.Synchronized。所以我猜它不能与多平台一起使用? - tonisives

2

2
如果你正在寻找一个具有多个参数的基本SingletonHolder类,那么我已经创建了SingletonHolder通用类,支持使用一个参数、两个参数和三个参数创建单例类的唯一实例。 这里是基本类的Github链接 无参数(Kotlin的默认设置):
object AppRepository 

一个参数(来自上面链接中的示例代码):

class AppRepository private constructor(private val db: Database) {
    companion object : SingleArgSingletonHolder<AppRepository, Database>(::AppRepository)
}
// Use
val appRepository =  AppRepository.getInstance(db)

两个参数:

class AppRepository private constructor(private val db: Database, private val apiService: ApiService) {
    companion object : PairArgsSingletonHolder<AppRepository, Database, ApiService>(::AppRepository)
}
// Use
val appRepository =  AppRepository.getInstance(db, apiService)

三个参数:

class AppRepository private constructor(
   private val db: Database,
   private val apiService: ApiService,
   private val storage : Storage
) {
   companion object : TripleArgsSingletonHolder<AppRepository, Database, ApiService, Storage>(::AppRepository)
}
// Use
val appRepository =  AppRepository.getInstance(db, apiService, storage)

超过3个参数:

为了实现这种情况,我建议创建一个配置对象来传递给单例构造函数。


1

使用lazy的解决方案

class LateInitLazy<T>(private var initializer: (() -> T)? = null) {

    val lazy = lazy { checkNotNull(initializer) { "lazy not initialized" }() }

    fun initOnce(factory: () -> T) {
        initializer = factory
        lazy.value
        initializer = null
    }
}

val myProxy = LateInitLazy<String>()
val myValue by myProxy.lazy

println(myValue) // error: java.lang.IllegalStateException: lazy not initialized

myProxy.initOnce { "Hello World" }
println(myValue) // OK: output Hello World

myProxy.initOnce { "Never changed" } // no effect
println(myValue) // OK: output Hello World

1

我刚开始学习Kotlin开发,所以我想要一个最简单的解决方案,但也要尽可能地类似于Java Singleton。通过双重检查确保线程安全,私有构造函数,使用volatile关键字引用。下面的代码对我来说是最好的。在这里分享给其他需要的人。

class InstrumentationManager private constructor(prodToken: String, intToken: String) {
companion object {
    @Volatile
    private var INSTANCE: InstrumentationManager? = null
    fun getInstance(prodToken: String, intToken: String): InstrumentationManager =
        INSTANCE ?: synchronized(this) {
            INSTANCE ?: InstrumentationManager(prodToken, intToken).also { INSTANCE = it }
    }
}

}

描述

  • 私有构造函数 --> 私有的InstrumentationManager()
  • InstrumentationManager? --> @Nullable
  • INSTANCE ?: --> 如果(instance == null) { }
  • InstrumentationManager(prodToken, intToken).also --> InstrumentationManager对象创建后进行额外处理。

当我将这段代码放入Android Studio时,它会提示构造函数中的参数未被使用。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接