65.9K
CodeProject 正在变化。 阅读更多。
Home

理解和实现单例模式的绝对初学者教程 (C#)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2016年1月20日

CPOL

5分钟阅读

viewsIcon

19831

本文是关于单例模式的又一次解释和实现。

引言

本文是关于单例模式的又一次解释和实现。我们将了解什么是单例模式以及如何在 C# 中实现它。这绝不是在 C# 中实现单例模式的最佳且推荐的方式。本文的主要目的是向读者介绍单例模式的概念。

背景

当我们的应用程序遇到以下场景时:

  • 应用程序中需要一个类的单个实例,并且该类本身能够强制执行其自身的单个实例。系统的其余部分无需担心管理该单个实例。
  • 这个单例类应该可以被整个系统或大部分系统访问。
  • 这个单例子系统应该在需要时才被创建和初始化(延迟初始化)。

现在让我们看看如何创建一个类,让调用者只能创建一个它的单个实例。为了做到这一点,我们首先应该问自己的是类的实例是如何创建的。这个问题的答案是通过调用类上的 new 来创建实例,这反过来又会调用类的构造函数。所以,如果我们想让应用程序不能创建多个实例,我们应该首先限制对构造函数的访问,即,将构造函数设为 private

现在,如果我们把构造函数设为 private,下一个问题是我们如何创建这个类的实例。因为 private 构造函数仍然可以从类本身内部调用,所以我们可以在类内部创建这个类的实例。为此,我们需要在类内部创建一个 static 方法(static 方法不需要调用者拥有实例就可以被调用),即 GetInstance,它将创建这个类的对象并将其返回给应用程序。

Using the Code

现在让我们看看这个类是如何使用上面讨论的方法来实现实例创建并将其返回给调用者的。

public sealed class Singleton
{
	// A private constructor to restrict the object creation from outside
	private Singleton()
	{

	}

	// A private static instance of the same class
	private static Singleton instance = null;

	public static Singleton GetInstance()
	{
		// create the instance only if the instance is null
		if (instance == null)
		{
			instance = new Singleton();
		}

		// Otherwise return the already existing instance
		return instance;
	}
}

现在,上面的类将只允许用户创建这个类的一个实例,即最简单的单例形式。

你好,多线程

这个类存在一个问题。它不是线程安全的。所以为了使其线程安全,我们需要确保实例化代码在任何给定时间只能被一个线程访问。让我们这样做,并在我们的类中引入一个锁来保护 GetInstance 的调用。

public sealed class Singleton
{
	// A private constructor to restrict the object creation from outside
	private Singleton()
	{

	}

	// A private static instance of the same class
	private static Singleton instance = null;
	private static readonly object _lock = new object();

	public static Singleton GetInstance()
	{
		lock (_lock)
		{
			// create the instance only if the instance is null
			if (instance == null)
			{
				instance = new Singleton();
			}

			// Otherwise return the already existing instance
			return instance;
		}
	}
}

现在,任何给定时间只有一个线程可以访问实例检索代码。但这会带来性能问题。我们的想法是不让多个线程能够创建多个实例,所以我们需要将实例创建部分放在锁中进行保护。但我们所做的却是将整个方法用 lock 保护起来。这意味着即使实例已经创建并且只需要返回,也会获取 lock。所以为了规避这个问题,我们只需要将实例创建部分放在 lock 下进行保护,而不是实例返回部分。

public sealed class Singleton
{
	// A private constructor to restrict the object creation from outside
	private Singleton()
	{

	}

	// A private static instance of the same class
	private static Singleton instance = null;
	private static readonly object _lock = new object();

	public static Singleton GetInstance()
	{
		if (instance == null)
		{
			lock (_lock)
			{
				// create the instance only if the instance is null
				if (instance == null)
				{
					instance = new Singleton();
				}
			}
		}

		// Otherwise return the already existing instance
		return instance;
	}
}

现在我们拥有一个类,它只在第一次创建实例时获取 lock,其余时间,它将返回已创建的实例。

利用 .NET CLR 功能实现线程安全

当我们使用 C# 时,我们可以利用 CLR 的行为来实现线程安全,而无需复杂的锁定。我们可以做的是在类中包含一个 static 构造函数,它将负责实例的创建。类的 static 构造函数将在访问类的第一个 static 成员(在本例中是 GetInstance 方法)或创建类的第一个实例(在这里不是有效场景,因为构造函数是 private)时被调用。所以,让我们看看带有以下更改的实现:

public sealed class Singleton
{
	// A private constructor to restrict the object creation from outside
	private Singleton()
	{

	}

	// A private static instance of the same class
	private static readonly Singleton instance = null;

	static Singleton()
	{
		// create the instance only if the instance is null
		instance = new Singleton();
	}

	public static Singleton GetInstance()
	{
		// return the already existing instance
		return instance;
	}
}

所以我们在这里所做的是,我们将实例化移到了 static 构造函数中,当调用 GetInstance 方法时,它将被调用。Static 构造函数将创建实例,并且该实例将被 GetInstance 方法返回。

instance 字段被标记为 readonly,这样它只能在 static 初始化期间实例化。现在我们有了一个功能相当完善的单例类,它具有线程安全性,并且没有 lock 的性能开销。

注意:有人可能会争辩说,在我们最后一个版本中,初始化不是延迟的,因为如果这个类有任何其他 static 成员,那么即使没有请求该实例,它也会被创建。还有一种可能的单例版本,可以通过嵌套类或 Lazy<T> 来规避这个问题。但我在这里不会讨论它们。在大多数情况下,单例不会有其他 static 方法。如果我们发现自己处于一个同时拥有单例和其他 static 方法的场景,也许我们正在违反单一职责原则,应该重新审视我们的类设计。

看点

尽管这些内容非常基础,并且被许多文本反复讨论。你会发现所有单例实现中的代码片段也是相同的,因为使用 C# 实现的单例模式的可能性不多。不过,我仍然认为将这个模式用我自己的话来解释,对于那些刚刚开始接触这个模式的开发者来说,可能会非常有帮助。对于更高级的读者,我推荐阅读这篇博客,以获得关于这个模式更详细的解释:C# in Depth: Implementing the Singleton Pattern[^]

历史

  • 2016年1月20日 - 已恢复 - 有人可能会从这个解释中受益
  • 2013年 - 已删除 - 所有单例文档中都存在类似的实现/代码
  • 2012年 - 第一个版本
© . All rights reserved.