為什么應(yīng)該盡可能避免在靜態(tài)構(gòu)造函數(shù)中初始化靜態(tài)字段?
C#具有一個(gè)默認(rèn)開啟的代碼分析規(guī)則:[CA1810]Initialize reference type static fields inline,推薦我們以內(nèi)聯(lián)的方式初始化靜態(tài)字段,而不是將初始化放在靜態(tài)構(gòu)造函數(shù)中。
一、兩種初始化的性能差異
二、beforefieldinit標(biāo)記
三、靜態(tài)構(gòu)造函數(shù)執(zhí)行的時(shí)機(jī)
四、關(guān)于“All-Zero”結(jié)構(gòu)體
五、RuntimeHelpers.RunClassConstructor方法
一、兩種初始化的性能差異
CA1810這一規(guī)則與性能有關(guān),我們可以利用如下這段簡(jiǎn)單的代碼來演示兩種初始化的性能差異。Foo和Bar這兩個(gè)類的靜態(tài)字段都定義了一個(gè)名為_value的靜態(tài)字段,它們均通過調(diào)用靜態(tài)方法Initialize返回的值進(jìn)行初始化。不同的是Foo以內(nèi)聯(lián)(inline)賦值的方法進(jìn)行初始化,而Bar則將初始化操作定義在靜態(tài)構(gòu)造函數(shù)中。假設(shè)Initialize方法是一個(gè)相對(duì)耗時(shí)的操作,我們利用Program的_initialized字段判斷該方法是否被調(diào)用。
static class Program { private static bool _initialized; static void Main() { Foo.Invoke(); Debug.Assert(_initialized == false); Bar.Invoke(); Debug.Assert(_initialized == true); } private static int Initialize() { _initialized = true; return 123; } public class Foo { private readonly static int _value = Initialize(); public static int Value => _value; public static void Invoke() { } } public class Bar { private readonly static int _value; public static int Value => _value; static Bar() => _value = Initialize(); public static void Invoke() { } } }
從我們給出的調(diào)用斷言可以確定,當(dāng)我們調(diào)用Foo的靜態(tài)方法Invoke時(shí),它的靜態(tài)字段_value并沒有初始化;但是當(dāng)我們調(diào)用Bar的Invoke方法時(shí),Initialize方法會(huì)率先被調(diào)用來初始化靜態(tài)字段。從這個(gè)例子來說,由于整個(gè)應(yīng)用并沒有使用到Foo和Bar的靜態(tài)字段,所以針對(duì)它們的初始化是沒有必要的。所以我們說以內(nèi)聯(lián)方式對(duì)靜態(tài)字段進(jìn)行初始化的Foo具有更好的性能。
二、beforefieldinit標(biāo)記
對(duì)于Foo和Bar這兩個(gè)類型表現(xiàn)出來的不同行為,我們可以試著從IL代碼層面尋找答案。如下所示的兩段IL代碼分別來源于Foo和Bar,我們可以看到雖然Foo類中沒有顯式定義靜態(tài)構(gòu)造函數(shù),但是編譯器會(huì)創(chuàng)建一個(gè)默認(rèn)的靜態(tài)構(gòu)造函數(shù),針對(duì)靜態(tài)字段的初始化就放在這里。我們可以進(jìn)一步看出,自動(dòng)生成的這個(gè)靜態(tài)構(gòu)造函數(shù)和我們自己寫的并沒有本質(zhì)的不同。兩個(gè)類型之間的差異并沒有體現(xiàn)在靜態(tài)構(gòu)造函數(shù)上,而是在于:沒有顯式定義靜態(tài)構(gòu)造函數(shù)的Foo類型上具有一個(gè)beforefieldinit標(biāo)記。
.class public auto ansi beforefieldinit Foo extends [System.Runtime]System.Object { .field private static initonly int32 _value .method private hidebysig specialname rtspecialname static void .cctor () cil managed { .maxstack 8 IL_0000: call int32 Program::Initialize() IL_0005: stsfld int32 Foo::_value IL_000a: ret }
… }
.class public auto ansi Bar extends [System.Runtime]System.Object { .field private static initonly int32 _value .method private hidebysig specialname rtspecialname static void .cctor () cil managed { .maxstack 8 IL_0000: call int32 Program::Initialize() IL_0005: stsfld int32 Bar::_value IL_000a: ret } }
三、靜態(tài)構(gòu)造函數(shù)執(zhí)行的時(shí)機(jī)
從Foo和Bar的IL代碼可以看出,針對(duì)它們靜態(tài)字段的初始化都放在靜態(tài)構(gòu)造函數(shù)中。但是當(dāng)我們調(diào)用一個(gè)并不涉及類型靜態(tài)字段的Invoke方法時(shí),定義在Foo中的靜態(tài)構(gòu)造函數(shù)會(huì)自動(dòng)執(zhí)行,但是定義在Bar中的則不會(huì),由此可以看出一個(gè)類型的靜態(tài)構(gòu)造函數(shù)的執(zhí)行時(shí)機(jī)與類型是否具有beforefieldinit標(biāo)記有關(guān)。具體規(guī)則如下,這一個(gè)規(guī)則直接定義在CLI標(biāo)準(zhǔn)ECMA-335中,靜態(tài)構(gòu)造函數(shù)在此標(biāo)準(zhǔn)中被稱為類型初始化器(Type Initializer)或者.cctor。
- 具有beforefieldinit標(biāo)記:靜態(tài)構(gòu)造函數(shù)會(huì)在第一次讀取任何一個(gè)靜態(tài)字段之前自動(dòng)執(zhí)行,這相當(dāng)于一種Lazy loading的模式;
- 不具有beforefieldinit標(biāo)記:靜態(tài)構(gòu)造函數(shù)會(huì)在如下場(chǎng)景下自動(dòng)執(zhí)行:
- 第一次讀取任何一個(gè)靜態(tài)字段之前;
- 第一個(gè)執(zhí)行任何一個(gè)靜態(tài)方法之前;
- 引用類型:第一次調(diào)用構(gòu)造函數(shù)之前;
- 值類型:第一次調(diào)用實(shí)例方法;
由于beforefieldinit標(biāo)記只有在沒有顯式定義靜態(tài)構(gòu)造函數(shù)的情況下才會(huì)被添加,所以我們自行定義的專門用來初始化靜態(tài)字段的靜態(tài)構(gòu)造函數(shù)是完全沒有必要的。不但沒有必要,還可能帶來性能問題,應(yīng)該改成以內(nèi)聯(lián)的形式對(duì)靜態(tài)字段進(jìn)行初始化。
四、關(guān)于“All-Zero”結(jié)構(gòu)體
如果我們?cè)谝粋€(gè)結(jié)構(gòu)體中顯式定義了一個(gè)靜態(tài)構(gòu)造函數(shù),當(dāng)我們調(diào)用其構(gòu)造函數(shù)之前,靜態(tài)構(gòu)造函數(shù)會(huì)自動(dòng)執(zhí)行。
public class Program { private static bool _initialized= false; static void Main() { var foobar = new Foobar(1, 2); Debug.Assert(_initialized == true); } public struct Foobar { static Foobar() => _initialized = true; public Foobar(int foo, int bar) { Foo = foo; Bar = bar; } public int Foo { get; } public int Bar { get; } } }
倘若按照如下的方式利用default關(guān)鍵字得到一個(gè)所有字段為“零”的默認(rèn)結(jié)構(gòu)體(all-zero structure),我們顯式定義的靜態(tài)構(gòu)造函數(shù)是不會(huì)執(zhí)行的。
public class Program { private static bool _initialized = false; static void Main() { Foobar foobar = default; Debug.Assert(foobar.Foo == 0); Debug.Assert(foobar.Bar == 0); Debug.Assert(_initialized == false); } ... }
五、RuntimeHelpers.RunClassConstructor方法
如果我們要確保某個(gè)類型的靜態(tài)構(gòu)造函數(shù)已經(jīng)被顯式調(diào)用,可以執(zhí)行RuntimeHelpers.RunClassConstructor方法,它的參數(shù)為目標(biāo)類型的TypeHandle。
public class Program { private static bool _initialized = false; static void Main() { RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle); Debug.Assert(_initialized == true); }
… }
由于類型的靜態(tài)構(gòu)造函數(shù)只會(huì)被執(zhí)行一次,所以多次RuntimeHelpers.RunClassConstructor并不會(huì)導(dǎo)致靜態(tài)函數(shù)的重復(fù)執(zhí)行。
public class Program { private static bool _initialized = false; static void Main() { RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle); Debug.Assert(_initialized == true); _typeInitializerInvoked = false;
RuntimeHelpers.RunClassConstructor(typeof(Foobar).TypeHandle); Debug.Assert(_initialized == false); } ... }


C#具有一個(gè)默認(rèn)開啟的代碼分析規(guī)則:[CA1810]Initialize reference type static fields inline,推薦我們以內(nèi)聯(lián)的方式初始化靜態(tài)字段,而不是將初始化放在靜態(tài)構(gòu)造函數(shù)中。
浙公網(wǎng)安備 33010602011771號(hào)