Kotlin 中带参数的单例

Posted

技术标签:

【中文标题】Kotlin 中带参数的单例【英文标题】:Singleton with parameter in Kotlin 【发布时间】:2017-03-16 20:02:55 【问题描述】:

我正在尝试将 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 属性暴露了 public setter 有点尴尬 @miensol 是否有任何其他选项可以将参数(上下文)传递给伴随对象? 将 Context 实例存储在全局单例对象中(无论是 Java 还是 Kotlin)都会造成内存泄漏:***.com/a/11908685/147024 @yole 如果它是单例的应用程序上下文,则它不是内存泄漏。 @LordRaydenMK 如果您在 Android 中使用 Kotlin,我认为您的代码有 4 项改进。我已经做了一些解释的要点。看看:gist.github.com/gaplo917/f186d5c541fbc0d6f77f9b720ec4694c 【参考方案1】:

这是 Google 架构组件 sample code 的一个简洁替代方案,它使用 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()
    

【讨论】:

我不确定为什么有 INSTANCE ?: 在同步块内?因为只有当 INSTANCE 为空时才会调用该块,所以为什么要再次检查 INSTANCE 是否为空呢?谁能解释一下? @SandipSoni 了解双重检查锁定:***.com/questions/18093735/… 我发现这段代码有两个问题。 1.您总是必须传递在您想要检索单例时可能不可用的上下文(或定义的任何其他变量) 2.假设您在第二次使用时传递不同的上下文。那是什么意思?你只会得到旧的单身人士。对我来说似乎是一种反模式。 @A1m 关于 2):第二次传递不同的上下文无关紧要,因为应用程序上下文用于构建数据库。您传递的Context 对象仅用于检索应用程序上下文。 @MathiasBrandt 在这种情况下可能是真的,因为 Context 本身应该是一个单例。但一般的问题是关于创建这个单例的模式。我觉得这种行为是未定义且具有误导性的。【参考方案2】:

Thread-Safe Solution # Write Once; Use Many;

创建一个实现单例逻辑的类是一个很好的解决方案,该类还包含单例实例,如下所示。

它在同步块中使用Double-Check Locking实例化实例,以消除多线程环境中出现竞争条件的可能性。

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 
        


Usage

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

class MyManager private constructor(context: Context) 

    fun doSomething() 
        ...
    

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

最后:

MyManager.getInstance(context).doSomething()

【讨论】:

假设MyManger需要多个构造函数参数,SingletonHolder如何处理 @AbhiMuktheeswarar:SingletonHolder 是一个泛型类,不幸的是它的泛型类型不能在运行时动态定义,可能对于每个输入参数计数,我们应该定义一个这样的类:@987654322 @ 如何为这些类创建测试用例? @AbhiMuktheeswarar 为 A 使用 Pair。然后:伴生对象:SingletonHolder> MyManager(it.first,it.second) 这很简洁,但遗憾的是它使我的编译器崩溃了CompilationException: Back-end (JVM) Internal error: Failed to generate expression: KtCallExpression【参考方案3】:

我不完全确定您为什么需要这样的代码,但这是我最好的选择:

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) 是做什么的? context 在这里永远不会为空,这是由 Kotlin 保证的。所有检查和断言都已由编译器实现。

更新:

如果您只需要一个延迟初始化的类 TasksLocalDataSource 实例,那么只需使用一堆延迟属性(在对象内部或包级别):

val context = ....

val dataSource by lazy 
    TasksLocalDataSource(context)

【讨论】:

正是我正在寻找的...关于此代码的用法...我正在尝试将github.com/googlesamples/android-architecture 转换为 Kotlin 以使用 Kotlin 并比较代码。这就是为什么我不想包含 Dagger 或 github.com/SalomonBrys/Kodein @LordRaydenMK 恕我直言,不要通过进行 Java 转换来开始学习 Kotlin。阅读官方文档一次,尝试从头开始做一个完整的等效 Kotlin 实现,但不做 Java 转换。 “不要以 Java 方式编写 Kotlin 代码”的概念非常重要,因为大多数时候你不需要 Java(in)著名的编码模式(因为它是为 Java 设计的)。 @GaryLO 我在 try.kotlinglang.org 上做了 koans。正如 Hadi Hariri (twitter.com/hhariri) 在某个会议上的一次演讲中所说的那样(现在找不到链接)......一些 Kotlin 总比没有 Kotlin 好。我写在这里的原因是尝试用 Kotlin 的方式来做。谢谢。 请注意,此解决方案不是线程安全的。如果多个线程尝试同时访问它,则可能存在两个单例实例并被初始化。 @musooff TasksLocalDataSource(context) 是一个构造函数,所以它会返回一个对象或抛出一个异常。【参考方案4】:

您可以声明一个 Kotlin 对象 overloading "invoke" operator。

object TasksLocalDataSource: TasksDataSource 
    private lateinit var mDbHelper: TasksDbHelper

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

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

【讨论】:

这个方法是最直接的,是invoke函数的一个很好的用例,它可以将一个Class变成一个函数。我在Coinverse 应用程序中将这种模式用于我的Repository 但是它是线程安全的吗?我看到很多人称赞this,实际上是从this 2yo popular medium post 借来的,正如作者所说,this 2yo popular medium post 又是从 Kotlin 的“懒惰”来源借来的。这个答案看起来更干净(我讨厌添加样板 util 文件,更不用说类了),但我离题了在多线程场景中使用这个单例。【参考方案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)

【讨论】:

您的方法正在填充改造对象,但它返回的可为空值。这是令人困惑和错误的【参考方案6】:

方法 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 
    

【讨论】:

对此的导入是 kotlin.jvm.Synchronized。所以我猜它不适用于多平台?【参考方案7】:

如果您需要的唯一参数是应用程序Context,那么您可以在ContentProvider 的早期将其初始化为***val,就像Firebase SDK 所做的那样。

由于声明 ContentProvider 有点麻烦,I made a library that provides a top level property named appCtx 适用于所有不需要 Activity 或其他特殊生命周期绑定上下文的地方。

【讨论】:

【参考方案8】:
class CarsRepository(private val iDummyCarsDataSource: IDummyCarsDataSource) 

    companion object 
        private var INSTANCE: CarsRepository? = null
        fun getInstance(iDummyCarsDataSource: IDummyCarsDataSource): CarsRepository 
            if (INSTANCE == null) 
                INSTANCE = CarsRepository(
                    iDummyCarsDataSource = iDummyCarsDataSource)
            
            return INSTANCE as CarsRepository
        
    


【讨论】:

【参考方案9】:

如果您正在寻找具有多个参数的基本 SingletonHolder 类。我创建了 SingletonHolder 泛型类,它支持只创建一个具有一个参数、两个参数和三个参数的单例类实例。

link Github of the base class here

非参数(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 个参数:

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

【讨论】:

【参考方案10】:

懒惰的解决方案

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

【讨论】:

【参考方案11】:

我看到了所有的答案。我知道这是一个重复的答案,但如果我们在方法声明中使用 synchronized 关键字,它会将整个方法同步到对象或类。并且 synchronized 块还没有被弃用。

您可以使用以下实用程序类来获取单例行为。

open class SingletonWithContextCreator<out T : Any>(val creator: (Context) -> T) 
    @Volatile
    private var instance: T? = null

    fun with(context: Context): T = instance ?: synchronized(this) 
        instance ?: creator(context).apply  instance = this 
    

你可以扩展上面提到的任何你想要单例的类。

在您的情况下,以下是使 TasksLocalDataSource 类单例的代码。

companion object : SingletonWithContextCreator<TasksDataSource>(::TasksLocalDataSource)

【讨论】:

【参考方案12】:

这是一个 kotlin 中的单例示例,我用线程对其进行了测试,没有异常

class ShoppingCartClassic private  constructor() 

   private var outfits: ArrayList<Outfit> = ArrayList()
   
   companion object
       @Volatile
       private var instance: ShoppingCartClassic? = null

       fun get(): ShoppingCartClassic 
           synchronized(this) 
//                return instance?: ShoppingCartClassic()  // I commented this because I got lower performance 
               if (instance == null) 
                   instance = ShoppingCartClassic()
               
               return this.instance!!
           
       
   

   fun addOutFit(outfit: Outfit)
       outfits.add(outfit)
   

   fun removeOutFit(outfit: Outfit)
       outfits.remove(outfit)
   

   fun checkout() :List<Outfit>

       return outfits
   

这是测试

companion object 
        @JvmStatic
        fun main(args: Array<String>) 
            val outfit1 = Outfit(
                pants = Pants("short pants1", Color.BLACK),
                shoes = Shoes("cool shoes1", Color.BLACK),
                shirt = Shirt("my shirt1", Color.GREEN)
            )
            val outfit2 = Outfit(
                pants = Pants("short pants2", Color.BLACK),
                shoes = Shoes("cool shoes2", Color.BLACK),
                shirt = Shirt("my shirt2", Color.BLUE)
            )
            val outfit3 = Outfit(
                pants = Pants("short pants3", Color.BLACK),
                shoes = Shoes("cool shoes3", Color.BLACK),
                shirt = Shirt("my shirt3", Color.BLACK)
            )
            val threads: ArrayList<Thread> = arrayListOf()

            for (i in 0..3) 

                val thread = Thread 
                    val instance = ShoppingCartClassic.get()
                    instance.addOutFit(outfit1)
                    instance.addOutFit(outfit2)
                    instance.addOutFit(outfit3)


                    instance.checkout().forEach 
                        println(it.shirt.style)
                    
                
                threads.add(thread)
            
            threads.forEach (Thread::start)
        
    

这是我的结果

my shirt1
my shirt1
my shirt2
my shirt3
my shirt1
my shirt2
my shirt3
my shirt1
my shirt2
my shirt3
my shirt2
my shirt3
....

**我也测试过** 我得到了这个错误

Exception in thread "Thread-1" Exception in thread "Thread-3" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)
    at dp.sigleton.Main$Companion.main$lambda-1(Main.kt:51)
    at java.base/java.lang.Thread.run(Thread.java:844)
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 

        

【讨论】:

【参考方案13】:

我是 Kotlin 开发的新手,所以我想要一个最简单但又尽可能类似于 Java Singleton 的解决方案。双重检查线程安全、私有构造函数、可变引用。下面的代码最适合我。在这里分享,以防其他人需要。

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() 仪表管理器? --> @Nullable INSTANCE ?: --> if(instance == null) InstrumentationManager(prodToken, intToken).also --> InstrumentationManager 对象创建后的额外处理。

【讨论】:

【参考方案14】:
Singletons

Singleton 的使用经常足以让创建它们的方式更简单。 Kotlin 没有使用通常的静态实例、getInstance() 方法和私有构造函数,而是使用对象表示法。 为了保持一致性,对象表示法也用于定义静态方法。

 object CommonApiConfig 
private var commonApiConfig: CommonApiConfig? = null
fun getInstance(): CommonApiConfig 
    if (null == commonApiConfig) 
        commonApiConfig = CommonApiConfig
       
    return CommonApiConfig.commonApiConfig!!
   

【讨论】:

不回答问题。问题是当你必须在构造函数中传递一个参数时如何创建一个单例。

以上是关于Kotlin 中带参数的单例的主要内容,如果未能解决你的问题,请参考以下文章

带参数的单例

Kotlin 中的单例类

CodeWorldkotlin带参数的单例模式封装实现

CodeWorldkotlin带参数的单例模式封装实现

CodeWorldkotlin带参数的单例模式封装实现

只用一行代码的单例模式