C# 中的单例设计模式(附示例)

2025年2月5日 | 阅读 8 分钟

C# 中的单例模式是什么?

单例模式是一种设计技术,它在保持对象访问的同时,限制了在一个类中可以实例化的对象的数量。

当系统需要协调操作,并且需要精确地只有一个对象时,这种模式就很有用。因此,为了确保一个给定的类只会被创建一个实例,并在程序运行期间提供对该实例的轻松全局访问,我们必须使用 C# 中的单例设计模式。

Singleton Design Pattern in C# with an example

上图展示了多个客户端 - 客户端 A、客户端 B 和客户端 C - 如何争夺单例实例。在获得单例实例后,客户端可以使用同一个实例调用方法 1、方法 2 和方法 n。如果这看起来还不清楚,不用担心;我们将在实践中进行讨论。

C# 中单例设计模式的实现指南

  • 必须声明一个私有且无参数的构造函数。
  • 这是必需的,因为它阻止了在类外部实例化该类。它只能在该类内部被实例化。
  • 类应标记为 sealed (密封),以防止其被继承。这在处理嵌套类时会很有用。
  • 我们必须构建一个私有的静态变量来引用该类的单例实例。
  • 还必须创建一个公共静态属性或方法来返回该类的单例实例。该方法或属性首先检查是否已创建了单例类的实例。
  • 如果已创建,则返回单例实例;如果没有,则会创建一个实例然后返回。

使用 C# 的单例设计模式示例

让我们通过一些示例来更好地理解 C# 的单例设计模式。C# 中的单例设计模式有多种实现方式。它们列在下面。

  • 使用 lock 在 C# 中实现线程安全单例缺少了线程安全单例设计模式。
  • 双重检查锁定用于实现线程安全单例设计模式。
  • 使用饿汉式加载实现线程安全单例设计模式 使用 Lazy 类实现懒汉式加载单例设计模式。

C# 中不存在线程安全单例设计模式

本教程将介绍 C# 中非线程安全单例设计模式的实现。

在这种情况下,Singleton 类被声明为 sealed,这意味着禁止从派生类继承。为了防止在类外部实例化该类,在创建类时使用了私有无参构造函数。但是,我们能够创建该类的实例。为了确保仅根据 null 条件创建一个类实例,我们再次将实例变量声明为 private 并将其初始化为 null 值。

尽管我们调用了两次 GetInstance 方法并报告计数器值为 1,但上面的输出清楚地表明私有构造函数只被调用了一次。因此,它证明了单例实例只被创建一次。在上面的示例中,我们没有以线程安全的方式实现单例设计模式。这意味着它可以在多线程环境中创建单例类的多个实例。

使用 C# 单例模式有什么优点?

在 C# 中应用单例设计模式有一些优点。它们如下。

  • 使用 C# 的单例设计模式有几个好处,最主要的是它管理对共享资源的并发访问。
  • 这意味着,如果单例设计模式被多个客户端同时访问,它能有效地管理对资源的并发访问。
  • 它具有静态初始化,能够懒加载并共享主数据和配置数据,这些数据在应用程序中很常见且不经常更新。
  • 在这种情况下,我们必须缓存内存对象。由于它提供了一个到特定实例的单一全局访问点,因此易于维护。
  • 以减少重复实例化大型对象的开销。

使用 C# 单例模式有什么缺点?

以下是缺点:

  • 单元测试是一个重大挑战,因为它为程序添加了全局状态。
  • 由于您必须使用锁来序列化对象以访问多线程环境中的单例实例,因此它降低了程序内并行的可能性。

何时使用 C# 单例设计模式?

C# 中的单例模式可能在以下场景中很有用:

处理共享资源:单例可用于处理共享资源,例如:

数据库连接池。能够管理所有文件相关任务的文件管理器。

当多个应用程序组件需要存储和检索对象时使用的缓存。

配置设置:当应用程序范围内的配置对象需要从多个位置访问时,可以使用单例来存储和检索其配置设置。

日志记录:可以通过单例日志记录器类记录应用程序中发送的消息。使用单例是有意义的,以避免冲突并确保一致性,因为日志记录通常涉及写入单个资源(如文件或控制台)。

硬件接口访问:当与打印机或串行端口等硬件资源交互时,单例可以确保协调访问,从而避免多个访问点可能引起的冲突。

性能考虑:某些类在实例化时需要大量资源。如果这种实例经常使用,单例是一个很好的选择,因为一次创建并重复使用它们效率更高。

何时不使用 C# 单例设计模式?

虽然了解何时不应用单例模式也很重要,但

测试困难:由于单例在测试之间保持状态,因此单元测试可能具有挑战性。由于测试不独立,这种状态的持续存在可能导致难以查明问题。

全局状态管理:使用单例会在您的程序中创建全局状态,这可能导致不同类之间存在看不见的依赖关系,并增加系统的复杂性和不可预测性。

并发问题:在多线程应用程序中使用单例时,它们可能会导致同步和并发问题,特别是如果单例状态是可变的。

可伸缩性和灵活性的限制:单例可能会限制可伸缩性和灵活性。随着应用程序的扩展,它们经常成为瓶颈,并且由于其广泛的影响,更改单例实现可能很困难。

过度使用和滥用:开发人员在寻找确保类只有一个实例的简单方法时,常常过度使用单例,而没有充分理解其影响。不正确使用它可能导致设计问题,直到后期才显现。

我们使用 C# 单例设计模式的实时场景

以下是一些 C# 单例设计模式可能很有用的现实世界情况:

  • 配置管理:应用程序的配置设置通常使用单例进行管理。单例确保应用程序的每个组件都对配置参数有统一的视图,这些参数在应用程序的生命周期中通常是恒定的,并且从多个位置访问。
  • 日志记录:日志记录框架经常使用单例。无论程序中的哪个区域生成日志条目,单例日志记录器都能确保它们集中且格式一致。此外,它还控制日志文件访问,避免冲突并确保线程安全。
  • 数据库连接:通过使用单例来管理数据库连接,您可以确保应用程序使用一致的共享连接池。这种方法可以避免频繁创建和关闭连接的开销,同时提高性能。
  • 硬件接口通信:对于与打印机、摄像机或条形码扫描仪等硬件设备通信的应用程序,单例可以提供集中的管理点。这避免了冲突和重复工作,并确保了协调访问。
  • 缓存:单例对于缓存很有用,缓存是指存储和重用难以获取的数据(例如复杂数据库查询的答案)。单例缓存可确保在整个程序中缓存数据的全局可访问性和一致性。

当然!让我们更深入地探讨 C# 中单例模式的某些方面。

懒惰初始化

在先前提供的实现中,通过 Lazy 类实现了懒惰初始化。通过使用懒惰初始化,单例对象仅在首次访问时创建。这在创建单例对象可能需要大量资源的情况下,或者当您想等到实际需要时才创建它时特别有用。

静态属性

由于 Instance 字段被标记为 static,因此无需创建 Singleton 类的实例即可访问 Singleton 实例。此属性提供了对 Singleton 实例的全局可访问点。

私有构造函数

由于 Singleton 类的构造函数是私有的,因此其他程序无法直接创建其实例。这维护了单例模式的原则,即一个类应该只有一个实例。

其他需要考虑的事项

饿汉式初始化:如果不需要懒惰初始化,您可以在类加载时或在实际需要之前随时预先创建单例实例。

序列化:如果您的单例类必须能够被序列化,您可能需要执行特定的处理,以防止反序列化创建额外的实例。

内存泄漏:在长期运行的应用程序中应谨慎使用单例模式,因为它可能导致内存泄漏,特别是如果它在程序整个运行期间持有资源。

牢记这些要点,您可以编写一个健壮的 C# 单例实现,有效地完成其工作。通过采用封锁类以防止子类继承、确保私有构造函数以防止直接实例化、提供静态属性以进行全局访问以及使用 Lazy 类进行懒惰初始化等策略,您可以设计一个可靠的 C# 单例实现。此外,根据程序的需要,您应该考虑预先初始化、序列化和潜在的内存泄漏。

结论

总而言之,C# 中的单例模式可确保一个类只有一个实例,并提供对其的全局访问点。在实现单例设计时,应考虑懒惰初始化、线程安全、禁止子类继承以及管理实例创建等因素。

总的来说,当少量应用并仔细考虑其后果时,单例模式可以成为管理 C# 应用程序中的全局数据和资源的有用工具。