C# 靜態內部類單例模式-靜態變數何時初始化
對於一個類的靜態變數何時初始化,大家都有一個普遍的共識,那就是第一次使用該類時,初始化該類的所有靜態變數和靜態方法。
/// <summary>
/// 只有在第一次使用到Test1的時候,才會初始化Test1.x
/// </summary>
class Test1
{
public static string x = EchoAndReturn("In type initializer");
public static string EchoAndReturn(string s)
{
Console.WriteLine(s);
return s;
}
}
但是當我看到單例模式的時候,卻看到一個矛盾,那就是餓漢模式下的單例模式,不是懶載入。
/// <summary>
/// 餓漢單例模式,無論是否呼叫,都會佔用系統資源
/// </summary>
public class HungrySingleton
{
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton()
{
}
public static HungrySingleton getInstance()
{
return instance;
}
}
帶著這個疑問,我繼續探究下去,到網上查到了基於靜態內部類的單例模式。網上的說法是,該方式能夠實現延遲載入。
public class Singleton6
{
private Singleton6() { }
private static class SingletonInstance
{
public static Singleton6 Instance = new Singleton6();
}
public static Singleton6 Instance()
{
return SingletonInstance.Instance;
}
}
如果說靜態成員是在類第一次被使用時進行初始化,那餓漢模式和靜態內部類的方式並沒有區別,都會實現延遲載入。
帶著這個疑問,我進行了實驗,發現了一個神奇的現象
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Starting Main");
Test1.EchoAndReturn("Echo!");
Console.WriteLine("After echo");
//Reference a static field in Test
string y = Test1.x;
//Use the value just to avoid compiler cleverness
if (y != null)
{
Console.WriteLine("After field access");
}
Console.ReadKey();
}
}
/// <summary>
/// 理論上只有在第一次使用到Test1的時候,才會初始化Test1.x
/// 也就是In type initializer出現在Starting Main之後
/// </summary>
class Test1
{
public static string x = EchoAndReturn("In type initializer");
public static string EchoAndReturn(string s)
{
Console.WriteLine(s);
return s;
}
}
按理來說,In type initializer這句話應該是在Starting Main之後才會出現,卻出現在了最前面,難道說靜態成員不是在類第一次被使用時進行初始化,而是在程式啟動時就初始化?
查閱了相關資料之後,終於找到了問題的答案,那就是類都有一個隱藏屬性beforefieldinit。
beforefieldinit,JIT編譯器可以在首次訪問一個靜態欄位或者一個靜態/例項方法之前,或者建立型別的第一個例項之前,隨便找一個時間生成呼叫。具體呼叫時機由CLR決定,它只保證訪問成員之前會執行(隱式)靜態建構函式,但可能會提前很早就執行。
也就是說靜態成員會在類第一次使用之前的任何時間初始化(由CLR智慧決定),如果是這樣的話,靜態內部類的方式豈不是解決不了延遲載入的問題?
有辦法,那就是在類中實現靜態建構函式,那beforefieldinit屬性就會被precise屬性替換,確保靜態成員會在類第一次使用之前的那一刻進行初始化。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Starting Main");
Test2.EchoAndReturn("Echo!");
Console.WriteLine("After echo");
//Reference a static field in Test
string y = Test2.x;
//Use the value just to avoid compiler cleverness
if (y != null)
{
Console.WriteLine("After field access");
}
Console.ReadKey();
}
}
/// <summary>
/// 只有在第一次使用到Test1的時候,才會初始化Test1.x
/// </summary>
class Test1
{
public static string x = EchoAndReturn("In type initializer");
public static string EchoAndReturn(string s)
{
Console.WriteLine(s);
return s;
}
}
class Test2
{
public static string x = EchoAndReturn("In type initializer");
// Defines a parameterless constructor.
static Test2()
{
}
public static string EchoAndReturn(string s)
{
Console.WriteLine(s);
return s;
}
}
按照這種方式確實解決了靜態成員在類第一次使用之前的那一刻進行初始化的問題。但是網上能夠找到的靜態內部類單例模式普遍都有一個問題,那就是沒有實現靜態內部類的靜態建構函式,結果就是和餓漢模式一樣,沒有解決延遲載入問題,最後貼上正確的靜態內部類單例模式
/// <summary>
/// 靜態內部類單例模式,執行緒安全
/// </summary>
public class StaticSingleton
{
private class InnerInstance
{
/// <summary>
/// 當一個類有靜態建構函式時,它的靜態成員變數不會被beforefieldinit修飾
/// 就會確保在被引用的時候才會例項化,而不是程式啟動的時候例項化
/// </summary>
static InnerInstance() { }
internal static StaticSingleton instance = new StaticSingleton();
}
private StaticSingleton()
{
}
public static StaticSingleton getInstance()
{
return InnerInstance.instance;
}
}