C# 设计模式:单一实例

本文关键字:实例 单一 设计模式 | 更新日期: 2023-09-18 11:28:10

单例设计模式是一种创建性设计模式。

目的(Purpose)

单一实例设计模式的目的是确保类只有一个实例,并在应用程序的整个生命周期中提供对该类的全局访问点。首选访问一个实例以避免意外结果。

用法(Usage)

在某些功能中,您可能希望执行一些同步活动。例如,您想对不希望重复计算并且还错过任何投票计数的内容进行用户投票。在这里,您可以将单一实例模式与 Vote 类一起使用,以确保只创建 Vote 类的一个实例并用于计算每个用户的投票。

在其他情况下,您可以使用单一实例模式。例如,您可以使用单一实例模式在应用程序中实现日志记录功能,其中记录器类的一个全局实例用于记录整个应用程序中的所有信息。

单例类结构(Singleton Class Structure)

对于单例模式,类应具有以下结构:

  • 应该有一个私有或受保护的构造函数。没有公共和参数化的构造函数。
  • 应该有一个静态属性(带有私有支持字段)来返回类的实例。静态方法也可用于返回实例。
  • 对于单一实例操作,至少有一个非静态公共方法。

以下是 C# 中单例类的基本结构。

示例: Singleton Class Structure

public class Singleton
{
    private static Singleton _instance;
    private Singleton()
    {
    }
    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
                _instance = new Singleton();
            return _instance;
        }
    }
    public void DoSingletonOperation()
    {
        Console.WriteLine("singleton operation");
    }
}

上面的单例类使用 static 属性返回类的实例。它有一个私有的无参数构造函数,它将限制使用 new 关键字创建对象。必须使用 Instance 属性来获取其对象。如果要允许构造函数在子类中继承,则可以保护构造函数。

下面检查上述 Singleton 类是否每次都返回单个实例。

示例: Singleton Object

static void Main(string[] args)
{
    Singleton s1 = Singleton.Instance;
    Singleton s2 = Singleton.Instance;
    Console.WriteLine(s1 == s2); // true
}

在上面的示例中,s1s2 是相同的实例。但是,上述单例类不是线程安全的。它可能会在多线程应用程序中给出错误的结果。

现实生活中的单身人士类(Real-life Singleton Class)

让我们看看可以实现单一实例设计模式的真实场景。

假设您正在对应用程序中的某些内容进行用户投票。多个用户从不同的页面注册他们的投票。为此,您可以使用单例设计模式,如下所示。

示例: Singleton Class

public class VoteMachine
{
    private VoteMachine _instance = null;
    private int _totalVotes = 0;
        
    private VoteMachine()
    {
    }
    public static VoteMachine Instance
    {
        get
        {
            if (_instance == null) {
                    
                    _instance = new VoteMachine();
                }
            }
            return _instance;
        }
    }
    public void RegisterVote()
    {
        _totalVotes += 1;
        Console.WriteLine("Registered Vote #" + _totalVotes);
    }
    public int TotalVotes
    {
        get
        {
            return _totalVotes;
        }
    }
}

上面的 VoteMachine 类是一个单一实例类,其中的构造函数是私有的,并且 Instance 属性每次都返回相同的实例。RegisterVote()方法将投票计数增加 1。TotalVotes属性返回注册投票总数。

让我们在控制台应用中测试上述VoteMachine类,如下所示。

示例: Objects of Singleton Class

internal class Program
{
    static void Main(string[] args)
    {
        VoteMachine vm1 = VoteMachine.Instance;
        VoteMachine vm2 = VoteMachine.Instance;
        VoteMachine vm3 = VoteMachine.Instance;
        vm1.RegisterVote();
        vm2.RegisterVote();
        vm3.RegisterVote();
        Console.WriteLine(vm1.TotalVotes);
    }
}

Output:

Registered Vote #1
Registered Vote #2
Registered Vote #3
3

VoteMachine singleton 类将在同步调用中完美运行,每个用户将逐个注册他们的投票。

等一下,如果每个用户都要一个接一个地注册他们的投票,那么为什么我们需要一个单例类?

在现实生活中,可能有多个用户在不知不觉中异步注册投票。让我们看看 VoteMachine 类在并行投票(多线程环境)中的行为。

下面演示了如何使用Parallel class在多线程环境中测试 VoteMachine 类。

示例: Test Singleton Object using Parallel class

internal class Program
{
    static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, 10);
            
        Parallel.ForEach(numbers, i =>
        {                
            var vm = VoteMachine.Instance;
            vm.RegisterVote();
        });
        
        Console.WriteLine(VoteMachine.Instance.TotalVotes);
    }
}

Output:

Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #1
Registered Vote #2
2

上面的代码对 RegisterVote() 函数执行 10 次并行调用。输出将取决于您的本地计算机。每次运行时,输出可能会有所不同。输出在多线程调用中返回错误的结果。

让我们看看如何创建一个线程安全的单一实例类。

线程安全单例类(Thread-safe Singleton Class)

在创建单一实例类的对象之前使用线程锁定,以使其线程安全。

示例: Thread-safe Singleton Class

public class VoteMachine
{
    private static VoteMachine _instance = null;
    private int _totalVotes = 0;
    private static readonly object lockObj = new object();
    private VoteMachine()
    {
    }
    public static VoteMachine Instance
    {
        get
        {
            lock (lockObj)
            {
                if (_instance == null)
                {
                    _instance = new VoteMachine();
                }
            }
                
            return _instance;
        }
    }
    public void RegisterVote()
    {
        _totalVotes += 1;
        Console.WriteLine("Registered Vote #" + _totalVotes);
    }
    public int TotalVotes
    {
        get
        {
            return _totalVotes;
        }
    }
}

在上面的VoteMachine类中,我们锁定了创建VoteMachine类实例的代码。这意味着只有一个线程可以进入锁并执行代码并创建实例。请注意,每次请求实例时都会获取锁,性能会降低。

现在,让我们在多线程场景中测试上述VoteMachine类,如下所示。

示例: Test Thread-safe Singleton Class

public class Program
{
    public static void Main(string[] args)
    {
        var numbers = Enumerable.Range(0, 10);
            
        Parallel.ForEach(numbers, i =>
        {                
            var vm = VoteMachine.Instance;
            vm.RegisterVote();
        });
            
        Console.WriteLine(VoteMachine.Instance.TotalVotes);
    }
}

Output:

Registered Vote #3
Registered Vote #9
Registered Vote #5
Registered Vote #7
Registered Vote #8
Registered Vote #6
Registered Vote #2
Registered Vote #3
Registered Vote #10
Registered Vote #4
10

输出可能会有所不同,但它将显示正确的总票数。尝试多次运行它以确保总票数正确。

为了提高性能,我们可以仔细检查锁定前后_instance == null,如下所示。

示例: Thread-safe Singleton Class

public class VoteMachine
{
    private static VoteMachine _instance = null;
    private int _totalVotes = 0;
    private static readonly object lockObj = new object();
    
    private VoteMachine()
    {
    }
    public static VoteMachine Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (lockObj)
                {
                    if (_instance == null)
                    {
                        _instance = new VoteMachine();
                    }
                }
            }
            return _instance;
        }
    }
    public void RegisterVote()
    {
        _totalVotes += 1;
        Console.WriteLine("Registered Vote #" + _totalVotes);
    }
    public int TotalVotes
    {
        get
        {
            return _totalVotes;
        }
    }
}

上面的代码在没有任何内存障碍的 ECMA CLI 规范中存在一些问题。

使用静态构造函数的单例类(Singleton Class using Static Constructor)

可以使用静态构造函数创建单一实例类。当访问类的任何静态成员时,静态构造函数在每个应用程序域中仅运行一次。

示例: Singleton Class

public class VoteMachine
{
	private static readonly VoteMachine _instance = new VoteMachine();
	private int _totalVotes = 0;
	static VoteMachine()
	{
	}
	private VoteMachine()
	{
	}
	public static VoteMachine Instance
	{
		get
		{
			return _instance;
		}
	}
	public void RegisterVote()
	{
		_totalVotes += 1;
		Console.WriteLine("Registered Vote #" + _totalVotes);
	}
	public int TotalVotes
	{
		get
		{
			return _totalVotes;
		}
	}
}

上面的VoteMachine是带有静态构造函数的单例类。私有构造函数阻止使用 new 关键字创建实例。

上面的类会在我们访问任何静态属性或方法后立即创建一个实例。如果由于某种原因有多个静态属性或方法,那么即使我们不打算使用它,也会立即创建一个实例。我们需要延迟实例化,它只会在必要时创建实例。

具有延迟实例化的单例类 (Singleton Class with Lazy Instantiation )

如果使用 .NET 4 或更高版本,请仅在需要时使用 Lazy<T> 创建实例。

示例: Singleton Class with Lazy Instantiation

public sealed class VoteMachine
{
	private static readonly Lazy<VoteMachine> _instance = new Lazy<VoteMachine>(() => new VoteMachine());
	private int _totalVotes = 0;
	
    private VoteMachine()
	{
	}
	public static VoteMachine Instance
	{
		get
		{
			return _instance.Value;
		}
	}
	public void RegisterVote()
	{
		_totalVotes += 1;
		Console.WriteLine("Registered Vote #" + _totalVotes);
	}
	public int TotalVotes
	{
		get
		{
			return _totalVotes;
		}
	}
}

上面的代码隐式使用 LazyThreadSafetyMode.ExecutionAndPublication 作为Lazy<VoteMachine> >的线程安全模式。Lazy<T>使延迟实例化变得简单且性能良好。它还允许您使用 IsValueCreated 属性检查是否已创建实例。

本文内容总结: