草庐IT

C#中单例模式的实现

MelonSuika 2023-11-13 原文

文章目录

一、引言

算是第一篇和设计模式正式相关的文。
相信写代码的都听说过设计模式,即使没有特意去学过。
而在这些设计模式之中,有一种很基础,也很常用的模式,就是单例模式(Singleton)。
那这篇文就来学习学习它。

二、单例模式

1. 单例模式

1.1. 场景

在一些场景下,在程序中需要一个特定的类,并且该类的数据需要能被所有其它对象访问。而在大多数情况下,系统中该类的数据也是唯一的。例如,界面上只能有一个鼠标指针,并且该鼠标指针能被所有程序访问。同样的还有,企业解决方案可以与管理到特定系统连接的单网关对象进行对接。

1.2. 问题产生

那么怎么去实现这样一个全局可用的对象实例,并且保证它只有一个实例被创建呢?

注意:这里对单例(singleton)的定义有意地比《设计模式:可重用面向对象软件的元素》[Gamma95]中的更窄。

1.3. 应当考虑的

在该场景下,当你考虑这个问题的解决方案时,必须注意并协调以下几点:
许多编程语言(如,VB 6.0或C++)都支持全局范围内的对象定义。这些对象都驻留在命名空间(namespace)的根上,对程序中所有对象而言都是普遍可用的。这种方法为全局可访问性的问题提供了一个简单的解决方案,但没有解决单实例的需求。因为它不会阻止其他对象创建全局对象的其他实例。此外,其他面向对象语言,如VB .NET或C#,并不直接支持全局变量(所以也无法直接用这种方案)。

为了确保一个类只能存在一个实例,必须得控制实例化过程。这意味着你需要通过使用编程语言中固有的实例化机制(例如,使用new操作符)来防止其它对象创建类的实例。控制实例化的另一部分是提供一种中心机制,通过这种机制,所有对象都可以获得对单个实例的引用。

这段话也说明了设计模式的一些特点,它不是与编程语言强相关的,它不是一个函数,一个类,而是更接近一种实现机制、实现思路。你通过任何语言都可以去实现它。

1.4. 解决方案

单例模式通过以下几方面来提供一个全局且单一(唯一)的实例:

  • 让类创建单个实例。
  • 允许其他对象通过返回实例引用的类方法来访问该实例。这个类方法是全局可访问的。
  • 将类构造函数声明为private,为的是其他对象不能创建新实例。

下图展示了该模式的静态结构。该UML类图非常简单,因为Singleton由一个简单的类组成,该类持有对自身单个实例的引用。

该图显示了Singleton类包含一个public的static属性,该属性返回对Singleton类的单个实例的引用。(静态类图中,+表示public,下划线表示static)。右上角的1表示系统中任何时候都只能有一个该类的实例。因为Singleton的默认构造函数是私有的,系统中的任何其他对象都必须通过Instance属性访问Singleton对象。

Singleton模式通常被归类为习惯用法而不是模式,因为解决方案主要取决于你所使用的编程语言的特性(例如,类方法和静态初始化器)。就如这个模式所做的那样,将抽象概念从特定的实现中分离出来,可能会使Singleton的实现看起来非常简单。

2. C#中实现单例模式

2.1. 场景

现在,你构建了一个C#程序。你需要一个仅有一个实例的类,并且需要一个该实例的全局访问点(global point of access)。你想确保你的方案是高效的,并且它能利用了微软.NET CLR的特性。你可能还希望你的方案是线程安全的。

2.2. 实现策略

尽管单例模式是一种相对简单的模式,但它也有许多不同的实现,你需要根据实现的不同,做一些权衡与选择。下面介绍了一系列的实现策略,并讨论了它们的优缺点。

2.2.1. 单例(Singleton)

该单例设计模式的实现遵循了设计模式中提出的解决方案:
可重用的面向对象软件元素[Gamma95],修改它以使得C#中语言特性可用,例如属性的修改:

下面提到的设计模式,有时候是指一本书或一套准则。
至于[Gamma95]以及相似格式的标识符应该是这本书中提到的一种解决方案。

using System;

public class Singleton
{
   private static Singleton instance;

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null)
         {
            instance = new Singleton();
         }
         return instance;
      }
   }
}

该实现有两个主要优点:
因为实例是在instance属性方法中创建的,所以类可以执行额外的功能(例如,实例化一个子类),即使它可能引入一些不受欢迎的依赖项。

直到对象请求实例时才执行实例化;这样的方法称为懒汉实例化(lazy instantiation,也叫延迟、惰性实例化)。懒汉实例化避免了在程序启动时,实例化不必要的单例。

然而,这种实现主要缺点是它在多线程环境下是不安全的(即不是线程安全的)。如果独立的执行线程同时进入Instance属性方法,那么可能会创建多个Singleton对象的实例。每个线程都可以执行下面语句,并创建一个新的实例:

if (instance == null)

有许多方法可以解决这个问题。其中一种是使用叫双重检查锁(double-check locking)的惯用法。然而,C#结合CLR(公共语言运行库)提供了一种静态初始化(static initialization)方法,它可以避免这些问题,而不需要开发者显式地编写线程安全代码。

事实上,假如没有研究过单例模式,我觉得这种方法已经很不错了。
public提供属性的外部访问。
static保证只有一份。
私有构造函数又使外界不能实例化它。
唯一缺点正如上面所说,它不是线程安全的。因为if(instance==null)这条语句不是原子的。
许多简单的场景下,我觉得这种写法完全够用。

2.2.2. 静态初始化

《设计模式[Gamma95]》避免静态初始化的原因之一是(说明该书中还是抵制这种静态初始化的方法的),C++规范在静态变量的初始化顺序上留下了一些模糊性。幸运的是,.NET框架通过对变量初始化的处理解决了这种模糊性。

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();
   
   private Singleton(){}

   public static Singleton Instance
   {
      get 
      {
         return instance; 
      }
   }
}

在这种策略中,当类中任何成员第一次被引用时,实例会被创建。CLR负责变量初始化。该类被标记为sealed以防止派生,因为派生可能会添加实例。有关将类标记为sealed的优缺点的讨论,参考[Sells03]。此外,变量被标记为readonly,这意味着只能在静态初始化期间(此处所显示的)或在类构造函数中对其赋值。

这种实现与前面示例相似,不同之处在于它依赖于CLR来初始化变量。它仍然处理了Singleton模式试图解决的两个基本问题:全局访问(global access)和初始化控制(initialization control)。这个公有静态属性(public static property)提供了实例的全局访问点。同时因为构造函数是私有的(private),Singleton类无法在类之外被实例化;因此,该变量就是系统中唯一存在的实例。

因为Singleton实例是由私有静态成员变量引用的,所以直到对Instance属性的调用第一次引用该类时,实例化才会发生。因此,该解决方案是延迟实例化属性的一种实现形式,正如《单例的设计模式形式》那样。

这种方法唯一的潜在缺点是那你对实例化机制的控制较少。在《设计模式形式》中介绍到,你能够在实例化之前使用非默认的构造函数或执行其他任务。因为.NET框架在这种解决方案中执行了初始化,所以你无法进行这些选项。在大多数情况下,静态初始化是.NET中实现一个单例的首选方法

2.2.3. 多线程单例

静态初始化对于大多数情况是可行的。当程序必须延迟实例化、使用非默认构造函数或在实例化之前执行其他任务,和在多线程环境中工作时,你就需要不同的解决方案了。但是,也存在着不能依赖CLR来确保线程安全的情况,例如在静态初始化示例中。在这种情况下,你必须使用特定的语言功能来确保在多线程环境下只创建一个实例。一种比较创建的解决方案是使用双重检查锁(Double-Check Locking)[Lea99]来隔离线程,避免同时创建单例的新实例。

注意:CLR解决了在其他环境中常见的与使用Double-Check Locking相关的问题。但也存在锁被打破的情况,相关更多信息,可以查看该网址

下面实现只允许一个线程进入临界区(critical area),这是锁块标识的,在还没有创建Singleton的实例时:

using System;

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

这种方法确保只创建一个实例,并且只在需要实例时创建。另外,将变量声明为volatile,以确保在访问实例变量之前完成对实例变量的赋值。最后,这种方法使用syncRoot实例来上锁,而不是锁住类型本身,以避免死锁。

这种双重检查锁的方法解决了线程并发性问题,同时避免了在每次调用Instance属性方法时使用独占锁。它还运行你延迟实例化,直到对象第一次被访问。事实上,程序很少使用这种实现。在大多数情况下,静态初始化方法就足矣。

2.3. 导致的一些问题

在C#中实现单例有以下好处和问题:

2.3.1. 好处

静态初始化方法是可行的,因为.NET框架显式地定义了静态变量初始化的方式和时间。

在“多线程单例”中描述的双重检查锁用法在CLR中得到了正确的实现。

2.3.2. 存在的问题

如果你的多线程程序需要显式初始化,则必须采取预防措施以避免线程问题。

2.4. 鸣谢

  • [Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

  • [Lea99] Lea, Doug. Concurrent Programming in Java, Second Edition. Addison-Wesley, 1999.

  • [Sells03] Sells, Chris. “Sealed Sucks.”

三、结尾语

虽然是个基础的单例模式,但涉及的知识并不简单。
其中明显就涉及了平时业务代码中不太会遇到的操作系统(Operating System)这门课的并发相关知识。尤其是多线程单例中,锁的示例,如果OS零基础或对并发没个概念还真难啃下来。
不过若只是应用单例的话,就如文中所说,理解静态初始化那段代码,然后把你的类套进去(即把静态初始化的Singleton类名换成你自己的类)就行了。

有关C#中单例模式的实现的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  5. c# - 如何在 ruby​​ 中调用 C# dll? - 2

    如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

  6. C# 到 Ruby sha1 base64 编码 - 2

    我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

  7. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  8. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

随机推荐