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。然后:伴生对象:SingletonHolderCompilationException: 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 的方式来做。谢谢。 请注意,此解决方案不是线程安全的。如果多个线程尝试同时访问它,则可能存在两个单例实例并被初始化。 @musooffTasksLocalDataSource(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 中带参数的单例的主要内容,如果未能解决你的问题,请参考以下文章