C# 和Unity 中的单例模式 详解
·
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. 典型应用场景
-
配置管理类:
- 应用程序配置
- 全局设置管理
-
资源管理类:
- 数据库连接池
- 线程池
- 缓存系统
-
日志系统:
- 全局日志记录器
- 审计跟踪
-
硬件访问:
- 打印机后台处理程序
- 硬件设备控制器
-
服务定位:
- 依赖注入容器
- 服务总线
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>();
优点:
- 更易于测试
- 明确的生命周期管理
- 遵循依赖倒置原则
六、单例模式的最佳实践
- 谨慎使用:单例模式本质上是全局状态,可能导致代码难以测试和维护
- 优先使用依赖注入:现代框架(如ASP.NET Core)的DI容器是更好的选择
- 考虑线程安全:确保多线程环境下也能保持单例
- 延迟初始化权衡:根据场景决定是否需要延迟初始化
- 避免过度使用:只在真正需要全局唯一实例时使用
- 单元测试考虑:设计单例类时应考虑如何使其可测试
七、单例模式的替代方案
-
依赖注入的单例生命周期:
- ASP.NET Core中的
AddSingleton - 更灵活且易于测试
- ASP.NET Core中的
-
静态类:
- 当不需要实例化且无状态时可用
- 但不能实现接口或继承
-
Monostate模式:
- 所有成员静态,实例共享相同状态
- 表面是多例,实质是单例
八、经典面试问题
-
如何破坏单例模式?如何防御?
- 破坏方式:反射、序列化、克隆
- 防御方法:私有构造函数、异常抛出、实现ISerializable
-
双重检查锁定为什么要两次检查?
- 第一次检查避免不必要的加锁
- 第二次检查确保在等待锁时未被其他线程初始化
-
单例模式的优缺点是什么?
- 优点:全局访问、资源控制
- 缺点:测试困难、隐藏依赖、潜在内存泄漏
-
单例模式与静态类的区别?
- 单例可以实现接口、继承类、延迟初始化
- 静态类不能实例化、不能实现接口
九、总结
单例模式是C#中最常用的设计模式之一,但也是最容易被误用的模式。正确实现单例模式需要注意:
- 线程安全:确保多线程环境下也能保持单例
- 延迟初始化:根据需求选择合适的初始化时机
- 防御性编程:防止反射、序列化等方式破坏单例
- 现代替代方案:优先考虑依赖注入容器管理单例
在以下情况考虑使用单例模式:
- 需要严格控制实例数量
- 全局访问点确实必要
- 对象创建成本高且频繁使用
在以下情况避免使用单例模式:
- 只是为了方便访问而使用
- 需要多态行为
- 需要频繁测试的组件
记住:单例模式是一种强大的工具,但强大的工具需要负责任地使用。
九、在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中的实现建议
- 继承MonoBehaviour:Unity单例通常继承MonoBehaviour以便使用Unity功能
- DontDestroyOnLoad:跨场景不销毁的单例需要使用DontDestroyOnLoad
- 线程安全:Unity主线程是单线程,通常不需要考虑线程安全
- 避免过度使用:只在真正需要全局访问点时使用单例
- 替代方案:考虑使用依赖注入或ScriptableObject作为替代
单例模式的变体
- 懒汉式:第一次使用时才初始化
- 饿汉式:类加载时就初始化
- 双重检查锁:多线程环境下安全的单例
- 静态内部类:利用类加载机制保证线程安全
- 枚举单例:Java中利用枚举特性实现
更多推荐

所有评论(0)