C# 中的单例模式详解

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。单例模式是设计模式中最简单但应用最广泛的模式之一。

一、单例模式的核心概念

1. 单例模式的主要特点

  • 唯一实例:保证一个类只有一个实例存在
  • 全局访问:提供全局访问点,通常通过静态属性或方法实现
  • 延迟初始化:实例通常在第一次被请求时才创建(可选)
  • 线程安全:在多线程环境下保证单例的唯一性

2. 单例模式的 UML 类图

在这里插入图片描述

二、单例模式的实现方式

1. 基础实现(非线程安全)

public sealed class Singleton
{
    private static Singleton _instance;
  
    // 私有构造函数防止外部实例化
    private Singleton() { }
  
    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }
  
    public void SomeBusinessLogic()
    {
        // 业务逻辑代码
    }
}

问题:在多线程环境下可能创建多个实例。

2. 简单线程安全实现(加锁)

public sealed class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();
  
    private Singleton() { }
  
    public static Singleton Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new Singleton();
                }
                return _instance;
            }
        }
    }
}

优点:线程安全
缺点:每次访问都要加锁,性能较差

3. 双重检查锁定(Double-Check Locking)

public sealed class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();
  
    private Singleton() { }
  
    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

优点:线程安全且性能较好(只在第一次创建时加锁)
注意:需要 C# 内存模型的支持(volatile 关键字)

4. 使用 Lazy<T>(推荐方式)

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _lazy = 
        new Lazy<Singleton>(() => new Singleton());
  
    private Singleton() { }
  
    public static Singleton Instance => _lazy.Value;
}

优点:

  • 线程安全
  • 延迟初始化
  • 代码简洁
  • .NET Framework 4.0+ 内置支持

5. 静态初始化(饿汉式)

public sealed class Singleton
{
    private static readonly Singleton _instance = new Singleton();
  
    // 显式静态构造函数告诉C#编译器不要标记类型为beforefieldinit
    static Singleton() { }
  
    private Singleton() { }
  
    public static Singleton Instance => _instance;
}

特点:

  • 线程安全
  • 在程序启动时就创建实例(非延迟初始化)
  • 最简单的线程安全实现

三、单例模式的特点

1. 优点

  • 严格控制实例数量:确保全局唯一实例
  • 全局访问点:方便对实例的管理和控制
  • 延迟初始化:节省资源(某些实现方式)
  • 避免频繁创建销毁对象:提高性能

2. 缺点

  • 违反单一职责原则:既管理自身生命周期又包含业务逻辑
  • 单元测试困难:难以模拟替代
  • 隐藏依赖关系:通过全局访问点使用,不易发现类之间的依赖
  • 可能引发内存泄漏:长期持有实例不释放

四、单例模式的使用场景

1. 典型应用场景

  1. 配置管理类:

    • 应用程序配置
    • 全局设置管理
  2. 资源管理类:

    • 数据库连接池
    • 线程池
    • 缓存系统
  3. 日志系统:

    • 全局日志记录器
    • 审计跟踪
  4. 硬件访问:

    • 打印机后台处理程序
    • 硬件设备控制器
  5. 服务定位:

    • 依赖注入容器
    • 服务总线

2. 具体案例

案例1:配置管理器
public sealed class ConfigManager
{
    private static readonly Lazy<ConfigManager> _instance = 
        new Lazy<ConfigManager>(() => new ConfigManager());
  
    private readonly Dictionary<string, string> _settings;
  
    private ConfigManager()
    {
        // 模拟从配置文件加载配置
        _settings = new Dictionary<string, string>
        {
            ["ConnectionString"] = "Server=myServer;Database=myDB;",
            ["Timeout"] = "30",
            ["LogLevel"] = "Debug"
        };
    }
  
    public static ConfigManager Instance => _instance.Value;
  
    public string GetSetting(string key)
    {
        return _settings.TryGetValue(key, out var value) ? value : null;
    }
  
    public void UpdateSetting(string key, string value)
    {
        _settings[key] = value;
    }
}

// 使用
var connectionString = ConfigManager.Instance.GetSetting("ConnectionString");
案例2:日志记录器
public sealed class Logger
{
    private static readonly Lazy<Logger> _instance = 
        new Lazy<Logger>(() => new Logger());
  
    private readonly StreamWriter _logWriter;
  
    private Logger()
    {
        _logWriter = new StreamWriter("application.log", append: true);
    }
  
    public static Logger Instance => _instance.Value;
  
    public void Log(string message)
    {
        var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}";
        _logWriter.WriteLine(logEntry);
        _logWriter.Flush();
    }
  
    ~Logger()
    {
        _logWriter?.Close();
    }
}

// 使用
Logger.Instance.Log("Application started");

五、单例模式的进阶话题

1. 单例模式的线程安全问题

问题示例(错误的延迟初始化)
public sealed class Singleton
{
    private static Singleton _instance;
  
    private Singleton() { }
  
    public static Singleton Instance
    {
        get
        {
            if (_instance == null) // 线程不安全
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }
}

问题:在多线程环境下,多个线程可能同时通过_instance == null检查,导致创建多个实例。

解决方案比较
解决方案线程安全延迟初始化性能复杂度
简单加锁是是差低
双重检查锁定是是好中
Lazy<T>是是好低
静态初始化是否最好最低

2. 单例模式的破坏与防御

通过反射破坏单例
var constructor = typeof(Singleton).GetConstructor(
    BindingFlags.NonPublic | BindingFlags.Instance, 
    null, Type.EmptyTypes, null);
  
var instance1 = constructor.Invoke(null);
var instance2 = constructor.Invoke(null);
// instance1 != instance2

防御方法:

private Singleton()
{
    if (_instance != null)
    {
        throw new InvalidOperationException("Singleton instance already exists.");
    }
}
通过序列化/反序列化破坏单例

防御方法:
实现ISerializable并重写GetObjectData和反序列化构造函数,或使用[Serializable]和protected Singleton(SerializationInfo info, StreamingContext context)

3. 单例模式与依赖注入

现代应用程序通常使用依赖注入容器管理单例生命周期:

// 在ASP.NET Core中注册单例服务
services.AddSingleton<ILogger, FileLogger>();

优点:

  • 更易于测试
  • 明确的生命周期管理
  • 遵循依赖倒置原则

六、单例模式的最佳实践

  1. 谨慎使用:单例模式本质上是全局状态,可能导致代码难以测试和维护
  2. 优先使用依赖注入:现代框架(如ASP.NET Core)的DI容器是更好的选择
  3. 考虑线程安全:确保多线程环境下也能保持单例
  4. 延迟初始化权衡:根据场景决定是否需要延迟初始化
  5. 避免过度使用:只在真正需要全局唯一实例时使用
  6. 单元测试考虑:设计单例类时应考虑如何使其可测试

七、单例模式的替代方案

  1. 依赖注入的单例生命周期:

    • ASP.NET Core中的AddSingleton
    • 更灵活且易于测试
  2. 静态类:

    • 当不需要实例化且无状态时可用
    • 但不能实现接口或继承
  3. Monostate模式:

    • 所有成员静态,实例共享相同状态
    • 表面是多例,实质是单例

八、经典面试问题

  1. 如何破坏单例模式?如何防御?

    • 破坏方式:反射、序列化、克隆
    • 防御方法:私有构造函数、异常抛出、实现ISerializable
  2. 双重检查锁定为什么要两次检查?

    • 第一次检查避免不必要的加锁
    • 第二次检查确保在等待锁时未被其他线程初始化
  3. 单例模式的优缺点是什么?

    • 优点:全局访问、资源控制
    • 缺点:测试困难、隐藏依赖、潜在内存泄漏
  4. 单例模式与静态类的区别?

    • 单例可以实现接口、继承类、延迟初始化
    • 静态类不能实例化、不能实现接口

九、总结

单例模式是C#中最常用的设计模式之一,但也是最容易被误用的模式。正确实现单例模式需要注意:

  1. 线程安全:确保多线程环境下也能保持单例
  2. 延迟初始化:根据需求选择合适的初始化时机
  3. 防御性编程:防止反射、序列化等方式破坏单例
  4. 现代替代方案:优先考虑依赖注入容器管理单例

在以下情况考虑使用单例模式:

  • 需要严格控制实例数量
  • 全局访问点确实必要
  • 对象创建成本高且频繁使用

在以下情况避免使用单例模式:

  • 只是为了方便访问而使用
  • 需要多态行为
  • 需要频繁测试的组件

记住:单例模式是一种强大的工具,但强大的工具需要负责任地使用。

九、在Unity中的应用

示例1:基础单例实现

using UnityEngine;

// 基础单例模板类
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;  // 静态实例
  
    // 公共访问属性
    public static T Instance
    {
        get
        {
            // 如果实例不存在,尝试在场景中查找
            if (_instance == null)
            {
                _instance = FindObjectOfType<T>();
          
                // 如果场景中也没有,创建新的GameObject并添加组件
                if (_instance == null)
                {
                    GameObject singletonObject = new GameObject(typeof(T).Name);
                    _instance = singletonObject.AddComponent<T>();
                }
            }
            return _instance;
        }
    }
  
    // 虚方法,子类可以重写
    protected virtual void Awake()
    {
        // 确保只有一个实例存在
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);  // 销毁重复实例
        }
        else
        {
            _instance = this as T;
            DontDestroyOnLoad(gameObject);  // 跨场景不销毁
        }
    }
}

// 具体单例类 - 游戏管理器
public class GameManager : Singleton<GameManager>
{
    public int currentLevel = 1;
    public int playerScore = 0;
  
    // 可以添加其他游戏管理逻辑
    public void LevelCompleted()
    {
        currentLevel++;
        Debug.Log($"进入下一关: {currentLevel}");
    }
  
    public void AddScore(int points)
    {
        playerScore += points;
        Debug.Log($"当前分数: {playerScore}");
    }
}

// 测试代码
public class SingletonTest : MonoBehaviour
{
    private void Start()
    {
        // 访问单例实例
        GameManager.Instance.AddScore(100);
        GameManager.Instance.LevelCompleted();
  
        // 验证单例
        GameManager anotherRef = GameManager.Instance;
        Debug.Log($"两个引用是否相同: {ReferenceEquals(GameManager.Instance, anotherRef)}");
    }
}

示例2:音频管理器单例

using UnityEngine;

// 音频管理器单例
public class AudioManager : Singleton<AudioManager>
{
    [System.Serializable]
    public class Sound
    {
        public string name;
        public AudioClip clip;
        [Range(0f, 1f)] public float volume = 1f;
        [Range(0.1f, 3f)] public float pitch = 1f;
        public bool loop = false;
  
        [HideInInspector] public AudioSource source;
    }
  
    public Sound[] sounds;  // 可在Inspector中配置的音效
  
    protected override void Awake()
    {
        base.Awake();  // 调用基类Awake确保单例
  
        // 为每个音效创建AudioSource
        foreach (Sound s in sounds)
        {
            s.source = gameObject.AddComponent<AudioSource>();
            s.source.clip = s.clip;
            s.source.volume = s.volume;
            s.source.pitch = s.pitch;
            s.source.loop = s.loop;
        }
    }
  
    // 播放音效
    public void Play(string soundName)
    {
        Sound s = System.Array.Find(sounds, sound => sound.name == soundName);
        if (s == null)
        {
            Debug.LogWarning($"音效 {soundName} 未找到!");
            return;
        }
        s.source.Play();
    }
  
    // 停止音效
    public void Stop(string soundName)
    {
        Sound s = System.Array.Find(sounds, sound => sound.name == soundName);
        if (s == null)
        {
            Debug.LogWarning($"音效 {soundName} 未找到!");
            return;
        }
        s.source.Stop();
    }
  
    // 设置全局音量
    public void SetMasterVolume(float volume)
    {
        AudioListener.volume = Mathf.Clamp01(volume);
    }
}

// 测试代码
public class AudioTest : MonoBehaviour
{
    public string soundToPlay = "Background";
  
    private void Start()
    {
        // 播放背景音乐
        AudioManager.Instance.Play(soundToPlay);
    }
  
    private void Update()
    {
        // 测试音量控制
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            float newVolume = AudioListener.volume + 0.1f;
            AudioManager.Instance.SetMasterVolume(newVolume);
            Debug.Log($"音量增加到: {newVolume}");
        }
  
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            float newVolume = AudioListener.volume - 0.1f;
            AudioManager.Instance.SetMasterVolume(newVolume);
            Debug.Log($"音量减少到: {newVolume}");
        }
  
        // 测试音效播放
        if (Input.GetMouseButtonDown(0))
        {
            AudioManager.Instance.Play("Click");
        }
    }
}

示例3:场景加载器单例

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

// 场景加载器单例
public class SceneLoader : Singleton<SceneLoader>
{
    [SerializeField] private GameObject loadingScreen;  // 加载界面
    [SerializeField] private UnityEngine.UI.Slider progressBar;  // 进度条
  
    // 加载场景(同步)
    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }
  
    // 异步加载场景
    public void LoadSceneAsync(string sceneName)
    {
        StartCoroutine(LoadAsync(sceneName));
    }
  
    private IEnumerator LoadAsync(string sceneName)
    {
        // 显示加载界面
        if (loadingScreen != null)
        {
            loadingScreen.SetActive(true);
        }
  
        // 开始异步加载
        AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
        operation.allowSceneActivation = false;  // 禁止自动切换场景
  
        // 更新进度条
        while (!operation.isDone)
        {
            float progress = Mathf.Clamp01(operation.progress / 0.9f);  // 0-0.9映射到0-1
      
            if (progressBar != null)
            {
                progressBar.value = progress;
            }
      
            Debug.Log($"加载进度: {progress * 100}%");
      
            // 加载完成但等待激活
            if (operation.progress >= 0.9f)
            {
                if (progressBar != null)
                {
                    progressBar.value = 1f;
                }
          
                Debug.Log("按空格键激活场景");
          
                // 等待玩家输入
                if (Input.GetKeyDown(KeyCode.Space))
                {
                    operation.allowSceneActivation = true;
                }
            }
      
            yield return null;
        }
  
        // 隐藏加载界面
        if (loadingScreen != null)
        {
            loadingScreen.SetActive(false);
        }
    }
  
    // 重新加载当前场景
    public void ReloadCurrentScene()
    {
        LoadSceneAsync(SceneManager.GetActiveScene().name);
    }
  
    // 退出游戏
    public void QuitGame()
    {
#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
#else
        Application.Quit();
#endif
    }
}

// 测试代码
public class SceneTest : MonoBehaviour
{
    public string sceneToLoad = "Level2";
  
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.L))
        {
            // 同步加载场景
            SceneLoader.Instance.LoadScene(sceneToLoad);
        }
  
        if (Input.GetKeyDown(KeyCode.A))
        {
            // 异步加载场景
            SceneLoader.Instance.LoadSceneAsync(sceneToLoad);
        }
  
        if (Input.GetKeyDown(KeyCode.R))
        {
            // 重新加载当前场景
            SceneLoader.Instance.ReloadCurrentScene();
        }
  
        if (Input.GetKeyDown(KeyCode.Q))
        {
            // 退出游戏
            SceneLoader.Instance.QuitGame();
        }
    }
}

在Unity中的实现建议

  1. 继承MonoBehaviour:Unity单例通常继承MonoBehaviour以便使用Unity功能
  2. DontDestroyOnLoad:跨场景不销毁的单例需要使用DontDestroyOnLoad
  3. 线程安全:Unity主线程是单线程,通常不需要考虑线程安全
  4. 避免过度使用:只在真正需要全局访问点时使用单例
  5. 替代方案:考虑使用依赖注入或ScriptableObject作为替代

单例模式的变体

  1. 懒汉式:第一次使用时才初始化
  2. 饿汉式:类加载时就初始化
  3. 双重检查锁:多线程环境下安全的单例
  4. 静态内部类:利用类加载机制保证线程安全
  5. 枚举单例:Java中利用枚举特性实现

更多推荐