Logo
Section 1 / 6

单例设计模式

学习单例设计模式及其在 Kotlin 中的实现。

导入

单例设计模式

学习单例设计模式及其在 Kotlin 中的实现。

单例模式——即使那些不喜欢使用设计模式的人,也一定听说过单例模式。曾经有人甚至将其称为反模式,但那仅仅是因为它过于流行。

...
单例设计模式

那么,对于第一次接触这个模式的人来说,它到底是什么?

通常情况下,当我们有一个类时,可以随意创建多个实例。例如,假设我们要列出我们最喜欢的电影:

1val myFavoriteMovies = listOf("黑鹰坠落", "银翼杀手") 2val yourFavoriteMovies = listOf(...)

注意,我们可以创建任意多个 List 实例,这没有任何问题。大多数类都可以有多个实例。

接下来,如果我们两人都想列出“速度与激情”系列的最佳电影呢?

1val myFavoriteQuickAndAngryMovies = listOf() 2val yourFavoriteQuickAndAngryMovies = listOf()

注意,这两个列表完全相同,因为它们都是空的。而且由于它们是不可变的,所以会一直保持为空。

既然这两个类的实例完全相同,根据 equals 方法判断,就没有必要在内存中保存多个副本。理想情况下,所有指向空列表的引用都应该指向同一个对象实例。事实上,null 就是这种情况——所有的 null 都是相同的。 这正是单例设计模式的核心思想。

要求

单例设计模式有以下几个基本要求:

  • 系统中必须只有一个实例。
  • 该实例应能从系统的任何地方访问。

在 Java 或其他一些语言中,实现这些要求非常复杂。首先,我们需要将类的构造函数设为 private,以禁止外部创建新实例;其次,还必须确保实例化过程是懒加载、线程安全且性能良好的,具体包括:

  • **懒加载**:我们可能不希望程序启动时就创建单例对象,因为这可能是一个开销较大的操作。我们希望只在首次需要时才进行实例化。
  • **线程安全**:如果两个线程同时尝试创建单例对象,它们都应获得同一个实例,而不是两个不同的实例。
  • **高性能**:如果有大量线程同时尝试创建单例对象,我们不应长时间阻塞它们,否则会严重影响执行效率。
...

在 Java 或 C++ 中满足所有这些要求非常困难,或者至少代码会显得冗长繁琐。

Kotlin 通过引入一个关键字 object,让创建单例变得极为简单。使用这个关键字,我们可以轻松实现一个满足所有要求的单例对象。

注意: object 关键字不仅仅用于创建单例。我们将在本章后续部分深入探讨它的其他用途。

声明单例对象的方式类似于定义普通类,但不需要构造函数,因为单例对象不能由我们手动实例化:

1object NoMoviesList

从现在起,我们可以在代码的任何位置访问 NoMoviesList,并且系统中只会存在一个该对象的实例:

1val myFavoriteQuickAndAngryMovies = NoMoviesList 2val yourFavoriteQuickAndAngryMovies = NoMoviesList 3println(myFavoriteQuickAndAngryMovies === yourFavoriteQuickAndAngryMovies) // true

注意这里使用的引用相等运算符 ===,用于检查两个变量是否指向内存中的同一对象。但这真的算是一个列表吗?

让我们写一个函数来打印电影列表:

1fun printMovies(movies: List<String>) { 2 for (m in movies) { 3 println(m) 4 } 5}

当我们传入一个初始的电影列表时,代码可以正常编译:

1// 每部电影单独一行输出 2printMovies(myFavoriteMovies)

但如果我们传入我们的空电影列表,代码将无法编译:

1printMovies(myFavoriteQuickAndAngryMovies) 2// 类型不匹配:推断出的类型是 NoMoviesList,但期望的是 List<String>

原因是我们的函数只接受 String 类型列表作为参数,而没有告诉编译器 NoMoviesList 也是这种类型(尽管名字暗示了这一点)。

幸运的是,在 Kotlin 中,单例对象可以实现接口,而 List 是一个泛型接口:

1object NoMoviesList : List<String>

现在,编译器会提示我们实现所需的方法。我们只需为 object 添加一个主体即可:

1object NoMoviesList : List<String> { 2 override val size = 0 3 override fun contains(element: String) = false 4 ... 5}

现在,Kotlin 已经提供了一个用于创建任意类型空列表的函数:

1printMovies(emptyList())

该函数返回一个实现了 List 接口的单例对象。这是一个现代软件中依然活跃应用设计模式的绝佳例子。

Kotlin 的 object 与普通类有一个重要区别——它不能有构造函数。如果需要为单例添加初始化逻辑,比如首次加载配置文件中的数据,可以使用 init 块:

1object Logger { 2 init { 3 println("我第一次被访问了") 4 // 初始化逻辑放在这里 5 } 6 // 更多代码放在这里 7}

完整实现如下:

1fun main() { 2 Logger.log("Hello World") 3} 4 5/** 6 * object 关键字将创建一个单例 7 */ 8object Logger { 9 init { 10 println("我第一次被访问了") 11 12 // 初始化逻辑放在这里 13 } 14 15 fun log(message: String) { 16 println("正在记录 $message") 17 } 18 // 更多代码放在这里 19}

需要注意的是,如果从未调用过该单例,它的初始化逻辑将完全不会执行,从而节省资源。这被称为懒初始化。

Kotlin入门

工厂设计模式