Featured image of post 设计模式之单例

设计模式之单例

单例是极其基本且常用的设计模式,本文以 Kotlin 为例,讲解单例的用法。

在知道单例(Singleton)这个名词之前,你就可能就已经创建或使用过它了。甚至常见到 Kotlin 都为其分配了 object 关键字,专门用于单例的创建。

在 Kotlin 中创建单例十分简单:

object Singleton {
  val field = "12312312"

  fun foo() {
    println("Singleton foo invoked")
  }
}

举个例子,Kotlin 的空返回值实质上就是一个名为 Unit 的单例。

public object Unit {
  override fun toString() = "kotlin.Unit"
}

不难看出,单例对象只会初始化一次,实质上只占用一块内存:

@Test
fun `unit singleton`() {
  fun foo(): Unit = println("foo invoked")
  fun bar(): Unit = println("bar invoked")
  assertTrue { foo() === bar() }
}
// Output:
// foo invoked
// bar invoked
// Exit with code 0

不妨看看 Kotlin 在幕后做的:

// 将 .class 文件反编译为 .java
@Metadata(/* ... */)
public final class Singleton {

   @NotNull
   public static final Singleton INSTANCE = new Singleton();
   @NotNull
   private static final String field = "12312312";

   private Singleton() {
   }

   public final void foo() {
      System.out.println("Singleton foo invoked");
   }

   @NotNull
   public final String getField() {
      return field;
   }
}

这正是 Java 创建单例的方式,写一个 class,将构造器私有,定义一个名为 INSTANCE 的属性。Kotlin 帮我们省了不少力。 :)

为了减少开销,所有幕后基本类型字段都是 static 的。(幕后字段这一术语将代理和计算属性排除在外。)

同时,不难发现 object懒加载(lazy initialization)的,直到你使用它,它才会被创建。以下代码可以再次印证这一点:

object Sun {
  init {
    println("Sun appeared in the world")
  }

  fun shine() {
    println("Sun is shining")
  }
}

@Test
fun `lazy load singleton`() {
  println("World is dark now")
  Sun.shine()
  println("World is bright now")
}

// Output:
// World is dark now
// Sun appeared in the world
// Sun is shining
// World is bright now

对于频繁使用或大量创建,但内容不变的类,推荐使用单例模式减少占用。

最经典的莫过于空集合:emptyList() emptyArray() emptyMap() emptySet() emptyFlow() 等等。

请看 emptyList() 的定义:

public fun <T> emptyList(): List<T> = EmptyList

仅仅是对内部 EmptyList 的一层兼容,继续查看 EmptyList

internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
    // ...
    override fun hashCode(): Int = 1
    override fun toString(): String = "[]"
    override val size: Int get() = 0
    override fun isEmpty(): Boolean = true
    // ...
}

可以看出实现了一个空的集合单例。所以,如果你的代码中存在不变且空的数据,也请考虑一下单例模式。

不过上面的例子有一点缺憾,我们的单例并不是线程安全的。在高并发场景下,如果多个类同时访问并尝试创建单例,可能会出现问题。使用 Kotlin 代理(delegate),我们可以轻易实现一个简单、线程安全、懒加载的单例:

class Singleton private constructor() {
  companion object {
    val INSTANCE by lazy(/* LazyThreadSafetyMode.SYNCHRONIZED */) { Singleton() }
  }
}

LazyThreadSafetyMode.SYNCHRONIZED 被注释,因为它是 lazy 代理的默认值。关于代理的细节,可能在后续详细介绍。

然而单例模式有时候会被滥用。例如将 object 用作命名空间以存放方法。实际上这很多余。Kotlin 并不强制你把所有方法都放在某个类下。尽情将它们放到 top-level 并用文件和包区分即可。函数也是一等公民

标准库就是这么做的,also let apply 等通用方法,map filter 等集合方法,都被定义为 top-level 的拓展函数。

comments powered by Disqus