文章目錄
- 前言
- 一、靜態類
- 1.1 靜態類的特點
- 1.2 靜態類的使用
- 1.3 靜態類的缺點
- 二、單例模式
- 2.1 Lazy延遲初始化
- 2.2 Lazy< T>單例模式的使用
- 2.3 單例模式的特點
- 三、IOC的Singleton
- 總結
前言
編寫程序的時候,常常能碰到當某些數據或方法需要被整個程序共享,且不需要多個獨立副本的場景。比如說一個系統配置信息,字符串處理、數據格式化等工具類擴展方法。我們期許這類共享信息,工具類擴展方法能夠開箱即用。常用的解決方案一般有:靜態類,單例模式,IOC容器中的Singleton服務。
-
比較直接的方法有通過靜態類。靜態屬性存放共享信息,靜態方法實現這類公用的共享方法。
-
使用普通類的話,也可也通過結合 Lazy< T>實現單例模式,類加載時不創建實例,第一次用訪問點屬性時才創建,多次訪問也不會創建多個實例,保證線程安全。
-
在現代開發中,可以借用IOC容器實現資源共享。項目啟動的時候向容器里注冊服務,并且將其生命周期設置為singleton,容器第一次解析服務會創建實例并緩存,后續請求容器獲取時直接返回緩存的實例,這樣也是全局唯一。
本文作為一個總結,記錄并對比對這類資源共享和實例管理時采取的解決方案,希望能幫助到大家。
一、靜態類
1.1 靜態類的特點
靜態類作為一種特殊的類類型,定義的時候通過static關鍵詞修飾。本身是不能夠被不能實例化,無法通過new關鍵字創建實例對象。靜態類不能被繼承,也不能實現接口。比起普通類,失去了多態的能力。
靜態類只能包含只能包含靜態成員,一個類被static修飾為靜態類,其內部不能出現非靜態字段,非靜態方法,非靜態屬性。但是普通類里是可以包含靜態字段、靜態方法或者靜態屬性的。
靜態類的生命周期和它所在的AppDomain生命周期一致,在程序加載類的時候被分配到靜態存儲區里(普通類的靜態成員也是被分配到靜態存儲),全局會調用這個唯一對象。
這種全局共享,生命周期與程序一致的特性,并且簡潔的調用方式使其天然容易實現信息共享,和工具擴展方法。
1.2 靜態類的使用
比如這個系統信息配置類,記錄App名稱和版本,在AppDomain內,全局會調用這個唯一對象。
系統信息配置類
public static class AppConfig
{public static string AppName = "我的應用";public static string Version = "1.0.0";
}
系統信息配置信息的調用
var appName = AppConfig.AppName;
比方說接下來的這個靜態工具方法,它要實現的是基于Newtonsoft相關的json序列化和反序列化。通過在靜態類JsonTransfer下面添加靜態擴展方法,其第一個參數指定調用該靜態擴展方法參數的類型,用this指定。
這樣在使用中,就能通過指定類型的對象后面直接調用定義的擴展方法十分的方便。
基于Newtonsoft的工具方法
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;}}
}
工具擴展方法調用
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服務類
/// <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服務類
services.AddSingleton<IRedisService, RedisService>();
總結
- 簡單工具類/配置,這些可以考慮靜態類,編寫和使用起來很簡潔性。
- 復雜的創建邏輯,并且要考慮到擴展,可以選擇 Lazy< T>單例模式,兼顧靈活性與性能。
- .NET Core開發環境下首選IOC Singleton,符合面向接口編程和低耦合原則。