Tutorialsteacher

关注我们

文章
  • C#
  • C# 面向对象编程
  • ASP.NET Core
  • ASP.NET MVC
  • LINQ
  • 控制反转 (IoC)
  • Web API
  • JavaScript
  • TypeScript
  • jQuery
  • Angular 11
  • Node.js
  • D3.js
  • Sass
  • Python
  • Go 语言
  • HTTPS (SSL)
  • 正则表达式
  • SQL
  • SQL Server
  • PostgreSQL
  • MongoDB
  • C# - 入门
  • C# - 版本历史
  • C# - 第一个程序
  • C# - 关键词
  • C# - 类和对象
  • C# - 命名空间
  • C# - 变量
  • C# - 隐式类型变量
  • C# - 数据类型
  • 数字
  • 字符串
  • DateTime
  • 结构体
  • 枚举
  • StringBuilder
  • 匿名类型
  • 动态类型
  • 可空类型
  • C# - 值类型和引用类型
  • C# - 接口
  • C# - 运算符
  • C# - if else 语句
  • C# - 三元运算符 ?
  • C# - Switch 语句
  • C# - For 循环
  • C# - While 循环
  • C# - Do-while 循环
  • C# - 分部类
  • C# - Static 关键字
  • C# - 数组
  • 多维数组
  • 交错数组
  • C# - 索引器
  • C# - 泛型
  • 泛型约束
  • C# - 集合
  • ArrayList
  • List
  • SortedList
  • Dictionary
  • Hashtable
  • Stack
  • Queue
  • C# - 元组 (Tuple)
  • C# - 值元组 (ValueTuple)
  • C# - 内置异常
  • 异常处理
  • throw 关键字
  • 自定义异常
  • C# - 委托
  • Func 委托
  • Action 委托
  • Predicate 委托
  • 匿名方法
  • C# - 事件
  • C# - 协变
  • C# - 扩展方法
  • C# - 流 I/O
  • C# - File 类
  • C# - FileInfo 类
  • C# - 对象初始化器
  • OOP - 概述
  • 面向对象编程
  • 抽象
  • 封装
  • 关联与组合
  • 继承
  • 多态
  • 方法重写
  • 方法隐藏
  • C# - SOLID 原则
  • 单一职责原则
  • 开闭原则
  • 里氏替换原则
  • 接口隔离原则
  • 依赖倒置原则
  • 设计模式
  • 单例模式
  • 抽象工厂模式
  • 工厂方法模式
Entity Framework Extensions - 提升 EF Core 9
  批量插入
  批量删除
  批量更新
  批量合并

C# 设计模式:单例 (Singleton)

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

目的

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

用法

可能有些功能需要你进行同步活动。例如,你想获取用户的投票,你不想出现重复计数,也不想遗漏任何投票计数。在这里,你可以使用带有 Vote 类的单例模式,以确保 Vote 类只创建一个实例,并用于计算每个用户的投票。

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

单例类结构

一个类要实现单例模式,应具有以下结构

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

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

示例:单例类结构
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");
    }
}

上述单例类使用静态属性返回类的实例。它有一个私有的无参数构造函数,这将限制使用 new 关键字创建对象。你必须使用 Instance 属性来获取它的对象。如果你想允许它在子类中被继承,可以将构造函数设置为 protected。

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

示例:单例对象
static void Main(string[] args)
{
    Singleton s1 = Singleton.Instance;

    Singleton s2 = Singleton.Instance;

    Console.WriteLine(s1 == s2); // true
}

在上面的例子中,s1 和 s2 是同一个实例。然而,上述单例类不是线程安全的。它在多线程应用程序中可能会给出错误的结果。

实际应用中的单例类

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

假设你在应用程序中获取用户的投票。多个用户从不同页面注册他们的投票。为此,你可以使用单例设计模式,如下所示。

示例:单例类
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 类,如下所示。

示例:单例类对象
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);
    }
}
输出
Registered Vote #1 Registered Vote #2 Registered Vote #3 3

VoteMachine 单例类将在同步调用中完美工作,每个用户将逐个注册他们的投票。

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

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

以下演示了使用 Parallel 类在多线程环境中测试 VoteMachine 类。

示例:使用 Parallel 类测试单例对象
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);
    }
}
输出
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

上述代码执行 10 次并行调用 RegisterVote() 函数。输出将取决于你的本地机器。每次运行输出可能都不同。输出在多线程调用中返回错误的结果。

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

线程安全的单例类

在创建单例类的对象之前使用线程锁,使其成为线程安全的。

示例:线程安全的单例类
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 类,如下所示。

示例:测试线程安全的单例类
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);
    }
}
尝试一下
输出
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,如下所示。

示例:线程安全的单例类
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 规范存在一些问题。

使用静态构造函数创建单例类

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

示例:单例类
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 关键字创建实例。

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

带有惰性实例化的单例类

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

示例:带有惰性实例化的单例类
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 属性检查实例是否已创建。

TUTORIALSTEACHER.COM

TutorialsTeacher.com 是您权威的技术教程来源,旨在通过循序渐进的方法,指导您掌握各种网络和其他技术。

我们的内容帮助所有水平的学习者轻松快速地学习技术。访问此平台即表示您已阅读并同意遵守我们的使用条款和隐私政策,这些条款旨在保护您的体验和隐私权。

[email protected]

关于我们使用条款隐私政策
copywrite-symbol

2024 TutorialsTeacher.com. (v 1.2) 版权所有。