文章目录
- 前言
- 一、静态类
-
- [1.1 静态类的特点](#1.1 静态类的特点)
- [1.2 静态类的使用](#1.2 静态类的使用)
- [1.3 静态类的缺点](#1.3 静态类的缺点)
- 二、单例模式
-
- [2.1 Lazy延迟初始化](#2.1 Lazy延迟初始化)
- [2.2 Lazy< T>单例模式的使用](#2.2 Lazy< T>单例模式的使用)
- [2.3 单例模式的特点](#2.3 单例模式的特点)
- 三、IOC的Singleton
- 总结
前言
编写程序的时候,常常能碰到当某些数据或方法需要被整个程序共享,且不需要多个独立副本的场景。比如说一个系统配置信息,字符串处理、数据格式化等工具类扩展方法。我们期许这类共享信息,工具类扩展方法能够开箱即用。常用的解决方案一般有:静态类,单例模式,IOC容器中的Singleton服务。
-
比较直接的方法有通过静态类。静态属性存放共享信息,静态方法实现这类公用的共享方法。
-
使用普通类的话,也可也通过结合 Lazy< T>实现单例模式,类加载时不创建实例,第一次用访问点属性时才创建,多次访问也不会创建多个实例,保证线程安全。
-
在现代开发中,可以借用IOC容器实现资源共享。项目启动的时候向容器里注册服务,并且将其生命周期设置为singleton,容器第一次解析服务会创建实例并缓存,后续请求容器获取时直接返回缓存的实例,这样也是全局唯一。
本文作为一个总结,记录并对比对这类资源共享和实例管理时采取的解决方案,希望能帮助到大家。
一、静态类
1.1 静态类的特点
静态类作为一种特殊的类类型,定义的时候通过static关键词修饰。本身是不能够被不能实例化,无法通过new关键字创建实例对象。静态类不能被继承,也不能实现接口。比起普通类,失去了多态的能力。
静态类只能包含只能包含静态成员,一个类被static修饰为静态类,其内部不能出现非静态字段,非静态方法,非静态属性。但是普通类里是可以包含静态字段、静态方法或者静态属性的。
静态类的生命周期和它所在的AppDomain生命周期一致,在程序加载类的时候被分配到静态存储区里(普通类的静态成员也是被分配到静态存储),全局会调用这个唯一对象。
这种全局共享,生命周期与程序一致的特性,并且简洁的调用方式使其天然容易实现信息共享,和工具扩展方法。
1.2 静态类的使用
比如这个系统信息配置类,记录App名称和版本,在AppDomain内,全局会调用这个唯一对象。
系统信息配置类
csharp
public static class AppConfig
{
public static string AppName = "我的应用";
public static string Version = "1.0.0";
}
系统信息配置信息的调用
csharp
var appName = AppConfig.AppName;
比方说接下来的这个静态工具方法,它要实现的是基于Newtonsoft相关的json序列化和反序列化。通过在静态类JsonTransfer下面添加静态扩展方法,其第一个参数指定调用该静态扩展方法参数的类型,用this指定。
这样在使用中,就能通过指定类型的对象后面直接调用定义的扩展方法十分的方便。
基于Newtonsoft的工具方法
csharp
public static class JsonTransfer
{
/// <summary>
/// 对象 => JSON字符串
/// </summary>
/// <param name="obj">对象</param>
/// <returns></returns>
public static string ToJSON(this object obj, List<JsonSerializedConfig> jsonSerializedConfigs)
{
if (jsonSerializedConfigs.Contains(JsonSerializedConfig.CustomFormatDateTime))
{
return obj == null ? null : JsonConvert.SerializeObject(obj, Formatting.Indented);
}
else
{
return null;
}
}
}
工具扩展方法调用
csharp
WebUser webUser = new WebUser()
{
UserID = 1,
LoginName = "admin",
};
string? webUserStr = webUser.ToJSON();
1.3 静态类的缺点
但是静态成员不支持多态,静态类无法继承,也无法实现接口,扩展性很差。
二、单例模式
单例模式的核心是一个类仅仅有唯一实例,并且通过一个pulic的实例属性这个全局访问点来供外部函数调用, 它的出现解决了是普通类的对象如何实现仅实例化一次。这一点看上去于静态类相似,但是单例模式不拘泥于静态类,可以使普通类也能拥有一个唯一的实例,这点与静态类截然不同。
2.1 Lazy延迟初始化
前面我们了解到单例模式使普通类也能仅存在一个实例,但是考虑到程序运行的性能更倾向于需要这个类的实例的时候才去实例化这个类,而且在多线程环境下,两个线程同时请求这个类的实例,如果不加以锁处理,多线程访问可能创建多个实例。
这里我们使用.NET提供的延迟初始化的泛型类型Lazy< T>。使用Lazy< T>能将类的实例化延迟到首次需要使用这个对象实例的时候,而不是在声明这个对象的时候就创建,节省资源,节省不必要的加载时间,提供程序运行性能。
Lazy< T>是通过其Value属性访问对象,仅在首次访问Value的时候,才会实例化对象。 而且Lazy< T> 内置了线程安全支持,多线程同时访问 Value 时,只有一个线程会执行初始化,其他线程阻塞等待,最终所有线程获取同一个实例。
2.2 Lazy< T>单例模式的使用
使用Lazy< T>能将类的实例化延迟到首次需要使用这个对象实例的时候,我们可以将一些比如数据库服务类相关的天然是单例类型的对象,使用Lazy< T>延迟加载,这里拿一个Redis服务类举例。
我们通过一个Lazy< RedisServer>类型的私有静态只读字段存储唯一实例,并且该RedisServer的构造函数私有,仅允许创建Lazy< T>实例的时候调用。
并且通过一个静态实例讲lazy.Value暴露出去,作为一个访问点,调用该类实例的各种方法。
Redis服务类
csharp
/// <summary>
/// Redis单例服务
/// </summary>
public sealed class RedisServer
{
//私有静态字段,存储唯一实例
private static readonly Lazy<RedisServer> lazy = new Lazy<RedisServer>(() => new RedisServer());
/// <summary>
/// 静态实例
/// </summary>
public static RedisServer Instance { get { return lazy.Value; } }
/// <summary>
/// Redis连接对象
/// </summary>
private readonly ConnectionMultiplexer _redis;
/// <summary>
/// Redis数据库对象
/// </summary>
private readonly IDatabase _db;
/// <summary>
/// Redis键前缀
/// </summary>
private readonly string _keyPrefix = "AlarmCenter_";
/// <summary>
/// Redis单例服务
/// </summary>
public RedisServer()
{
try
{
ConfigurationOptions config = new ConfigurationOptions()
{
EndPoints = { "127.0.0.1:6379" }, // Redis服务器地址和端口
Password = "eastf@R123", // Redis密码
AbortOnConnectFail = false, // 连接失败时不终止
ConnectTimeout = 5000, // 连接超时时间(毫秒)
SyncTimeout = 5000 // 同步操作超时时间(毫秒)
};
// 创建Redis连接
_redis = ConnectionMultiplexer.Connect(config);
if (_redis.IsConnected)
{
// 获取默认数据库(0)
_db = _redis.GetDatabase();
}
else
{
Logger.Trace("无法连接到Redis服务器");
throw new Exception("无法连接到Redis服务器");
}
}
catch (Exception ex)
{
// 处理连接异常
Logger.Trace($"Redis连接错误: {ex.Message}");
throw;
}
}
/// <summary>
/// 获取Redis数据库对象
/// </summary>
public IDatabase GetDatabase()
{
return _db;
}
/// <summary>
/// 获取Redis键前缀
/// </summary>
/// <returns></returns>
public string GetKeyPrefix()
{
return _keyPrefix;
}
public string GetKey(string key)
{
return $"{_keyPrefix}{key}";
}
/// <summary>
/// 关闭Redis连接
/// </summary>
public void CloseConnection()
{
_redis?.Close();
_redis?.Dispose();
}
}
2.3 单例模式的特点
由于单例模式的对象类型并不局限于静态类,它可以通过接口实现扩展,相比静态类更加的灵活。
三、IOC的Singleton
在现代的软件开发框架中,和IOC容器打交道真是家常便饭,其服务的注入分三种生命周期,分别是:Singleton,Socpe,Transient。
往IOC容器里注册一个Singleton生命周期的服务时,在首次解析到该服务的时候,会创建一个这个服务的实例,并且缓存下来。后续所有对该服务的请求都会返回这个缓存,并且会随着容器的销毁自动实现IDisposable,一并被GC回收。
比如我们还是编写一个Redis服务类,这次不需要编写额外的单例相关的代码。该服务类不需要关心类的实例化,线程安全,性能开销。
Redis服务类
csharp
services.AddSingleton<IRedisService, RedisService>();
总结
- 简单工具类/配置,这些可以考虑静态类,编写和使用起来很简洁性。
- 复杂的创建逻辑,并且要考虑到扩展,可以选择 Lazy< T>单例模式,兼顾灵活性与性能。
- .NET Core开发环境下首选IOC Singleton,符合面向接口编程和低耦合原则。