如何在C#中实现单子模式

99 阅读9分钟

在C#中实现单子模式

设计模式是一组编码约定,用于解决软件开发中反复出现的问题。顾名思义,单子模式创建了一个单独的类的实例。

当一个类的实例只能存在一个的时候,就应该应用这个限制条件。建议缓存、线程池和注册表只有一个实例。

创建Singleton模式是为了提供一个标准的手段,在应用程序的整个生命周期内提供一个特定的元素实例。用另一种方式来说,如果应用程序没有重新启动,该模式将保持不变。

什么是Csharp单例设计模式的概述

C#中常用的设计模式之一是单子模式。这种设计模式使用一个类的单一实例来实现对该类成员的全局访问。

与其说同一个类有几个实例,不如说单子只有一个实例,并提供对该单体的方便访问。

单子模式可以在C#中以多种方式实现,它们有以下的共同特点。

  • 获取实例引用的静态和开放方法。
  • 在公共领域中没有参数的单一构造函数。
  • 在一个静态变量中保存对新产生的单一实例的引用。
  • 类是保密的。

使用Csharp单体模式的优点和缺点

让我们来看看使用单子模式的好处和坏处。

单身模式设计的好处

  1. 可以使用单子方法实现接口。
  2. 懒惰的加载是可能的,并且使用静态初始化。
  3. 它有助于掩盖底层的依赖性。
  4. 这样一来,由于它只有一个访问点,所以维护起来很简单。

单子模式的问题

以下是使用单子模式的一些缺点。

  1. 由于单元测试给应用程序带来了一个全局状态,它可能是一个挑战。
  2. 锁定减少了程序中可能存在的并行性。

静态方法与单子类。

单身类和静态方法的比较如下。

  • 不可能扩展一个静态类,尽管有可能以单子形式开发一个类。
  • 即使静态类不能被创建,但它可以被单子类初始化。
  • 当一个包含静态类的程序被加载时,公共语言运行时(CLR)会自动为你将该类加载到内存中。

我们可以用不同的方式实现单子模式,其中包括以下几种。

  1. 没有线程的安全单子。
  2. 用于线程安全的单子。
  3. 使用双重检查锁来确保单子的线程安全。
  4. 无锁的线程安全
  5. 使用.NET 4中的Lazytype。

1.使用没有线程的安全单子

public closed class OurSingleton01 {
    private OurSingleton01() {

    }
    private static OurSingleton01 instances = null;
    public static OurSingleton01 Instances {
        get {
            if (instances == null)
             {
                instances = new OurSingleton01();
            }
            return instances;
        }
    }
}

解释

上面的代码不是为在多线程环境中运行而设计的。

每当两个并发的线程确定条件(if instance == null)为真时,它们都会构造该类的实例,这违反了单例框架模式。

没有办法保证一个新的实例变量会被其他线程观察到,即使表达式已经被运行了。这是由内存架构的工作方式决定的。

2.为线程安全使用单子

public closed class OurSingleton01
{
    private static OurSingleton01 instances = null;
    protected static readonly obj1 codelock = new obj1();

    OurSingleton01()
    {
    }

    public static OurSingleton01 Instances
    {
        get
        {
            lock (codelock)
            {
                if (instances == null)
                {
                    instances = new OurSingleton01();
                }
                return instances;
            }
        }
    }
}

解释

这个版本是线程安全的。在生成一个实例之前,会获得一个共享实体的锁并进行重复检查。

有了锁,就可以禁止不同的线程生成相同的类对象,因为锁确保所有的读操作都是在特定的项目上获得锁之后逻辑地发生的。

解锁可以确保所有的写操作在锁释放之前合乎逻辑地发生。

由于只有一个线程可以在代码的那个特定区域,第一个线程将已经创建了这个实例;因此表达式的评估结果为假。

每次请求一个实例时获得一个锁会降低性能。

这个方法不是锁定类型(Singleton),而是锁定一个类变量的静态值。

如果你关闭了一个其他类可以获取和锁定的元素,那么一个人可能会陷入僵局。

类的特定对象,作为一般规则,应该保持在公众视线之外。因此,现在编写线程安全的程序要容易得多。

3.使用双检查锁来确保单子的线程安全

public closed class OurSingleton01
{
    protected static OurSingleton01 instances = null;
    protected static readonly obj codelock = new obj();

    OurSingleton01()
    {
    }

    public static OurSingleton01 Instances
    {
        get
        {
            if (instances == null)
            {
                lock (codelock)
                {
                    if (instances == null)
                    {
                        instances = new v();
                    }
                }
            }
            return instances;
        }
    }
}

解释

通过不使用锁,这种方法旨在比之前的迭代更具有线程安全。

这种模式似乎有以下缺点。

  1. 在Java中,这是个不可能的事。Java中的单子模式对C#程序员和Java程序员都有帮助。

因此,值得理解的是,如果我们曾经使用过它,就不能保证构造函数会在新对象的引用被分配到实例之前完成它;在Java内存模型中。

尽管在1.5版本中对Java内存框架进行了修订,但由于缺乏易失性变量(如C#),双重检查锁定仍然存在问题。

  1. ECMA CLI协议也有同样的问题,因为没有内存边界。使用.NET内存架构(恰好比ECMA规范更好)可能会使其安全。如果我们想让它工作,我们可以使实例变量易失,或者使用显式内存阻塞调用。

  2. 犯错误是相对容易的。如果模式没有遵循指定的内容,任何实质性的改变都会影响性能或准确性。

  3. 就性能而言,它仍然达不到后来者的要求。

4.使用无锁线程安全函数

public closed class OurSingleton01
{
    private static readonly OurSingleton01 instances = new OurSingleton01();


    static OurSingleton01()
    {
    }

    private OurSingleton01()
    {
    }

    public static OurSingleton01 Instances
    {
        get
        {
            return instances;
        }
    }
}

解释

虽然看的很简单,但为什么是线程安全的呢?而且它在后台启动的时候有多懒?

每个AppDomain有一次,当对象的新实例或静态组件被实例化或链接时,C#的静态构造函数会运行。

必须执行一个新的类型检查,与其他事情无关。然后,这个检查将比其前身实例的额外检查更快,因为它是在检查一个新创建的类型。

然而,有几点需要注意。

  • 这与其他的实现方式不同,它的工作难度更大。除了实例之外的静态元素在使用之前必须首先被创建。
  • 如果一个静态构造函数调用了另一个静态构造函数,而后者又调用了原来的构造函数,事情就会变得很困难。静态构造函数有影响,必须意识到它们。例如,在一个循环中,它们不断地相互引用。
  • .NET保证了懒惰的类别初始化器,如果确实,类型没有被标记为beforefieldinit 唯一标志。然而,C#编译器将那些没有静态构造函数的类型标记为beforefieldinit

你可以通过使实例成为公共静态只读变量来消除这个属性。现在实际的骨架代码已经很小了。虽然Just-in-time内联可能会提高速度,但许多人更喜欢一个变量,以备后续行动的需要。请注意,lazy需要静态构造函数。

5.使用.NET 4中的Lazy类型

System.Lazym使.NET 4(和更高版本)应用程序中的懒惰变得超级简单。你必须使用lambda表达式将一个委托传递给引用Singleton构造函数的构造函数。

public closed class OurSingleton01
{
    private static readonly Lazy<OurSingleton01> lazy =
        new Lazy<OurSingleton01>(() => new OurSingleton01());

    public static OurSingleton01 Instances { get { return lazy.Values; } }

    private OurSingleton01()
    {
    }
}

解释

这样做会很容易使用,也很可靠。你可能还需要使用IsValueCreated 属性来查看实例是否已经被创建--如果有必要的话。

对于LazySingleton, ,默认的线程安全模式是LazyThreadSafetyMode.ExecutionAndPublication 。你可能想尝试几种方法,看看它们是否满足你的需求。

完全懒惰的实例化

public closed class OurSingleton01
{
    private OurSingleton01()
    {
     }

          public static OurSingleton01 Instances { get { return Nested.instances; }
          }

             protected class Nested
            {

             static Nested()
           {
        }

        static readonly OurSingleton01 instances = new OurSingleton01();
    }
}

解释

最开始的一个实例似乎是唯一可以由嵌套对象启动实例化的地方;一个静态元素。

版本的原始概念将以一种懒惰的方式,并有所有的性能收益。虽然嵌套类可以访问其父类的秘密成员,但它们仍然可以以另一种方式(反向)访问它们,这不一定是真的。

一个已经被私有化的类并不是一个问题,因为要使实例化变得懒惰,代码就有些复杂了。

性能与懒惰的关系

除非你的类设置非常耗时,或者在其他地方有不可预见的后果,否则我们可能不需要显式静态构造函数。

通过允许Just-in-time编译器提出一个请求(例如,在一个方法的开始),我们可以证明这个类别已经被初始化。

使用单子实例和紧缩循环创建一个应用程序会对性能产生很大影响。它决定了懒惰实例化是否有必要。

异常情况

单子构造函数必须用于那些可能引发异常但对应用程序不致命的任务。

应用程序有可能修复这个问题,所以你可以想再试一下

在这一点上,使用初始化器的类别来构建单子是很麻烦的。然而,预期的动作(重新运行类型初始化器)并不总是由不同的运行时完成,当它完成时,代码在其他运行时就会变得错误。

总结

在本教程中,我们已经看到,单子模式指的是只允许开发一个实例的类,并且通常提供对该实例的简单访问。

我们还讨论了单体模式在C#语言中的作用,以及如何以不同方式实现它。

我希望你觉得这个教程很有帮助。