【问题标题】:Singleton with parameter in KotlinKotlin 中带参数的单例
【发布时间】: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)都会造成内存泄漏:stackoverflow.com/a/11908685/147024
  • @yole 如果它是单例的应用程序上下文,则它不是内存泄漏。
  • @LordRaydenMK 如果您在 Android 中使用 Kotlin,我认为您的代码有 4 项改进。我已经做了一些解释的要点。看看:gist.github.com/gaplo917/f186d5c541fbc0d6f77f9b720ec4694c

标签: android kotlin


【解决方案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 了解双重检查锁定:stackoverflow.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!!
                   }
                }
                

                【讨论】:

                • 不回答问题。问题是当你必须在构造函数中传递一个参数时如何创建一个单例。
                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-05-11
                • 2010-11-06
                • 1970-01-01
                • 1970-01-01
                • 2020-09-20
                相关资源
                最近更新 更多