這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
ORM架構在基類中定義各增刪改查方法,子類實現儲存儘可能簡潔,除了各屬性工作表示表結構(也就是POCO Plain Old Csharp Object)至少還得有表名等資訊。
考慮一下程式碼片段:
public abstract class RecordBase<T> where T : RecordBase<T>, new(){ protected static string TableName; public static List<T> GetByID(int id) { using (var conn = OpenConnection()) { var sql = "select * from " + TableName + " where ID = @id"; return conn.Query<T>(sql, new {id = id}).First(); } }}public class User : RecordBase<User>{ public int Id { get; set; } public string Name { get; set; } static User () { TableName = "Users"; }}
調用代碼則類似:
var u = User.GetByID(1);
User子類中的靜態建構函式中對TableName進行賦值;那麼父類中的GetByID
方法執行時,便可以獲得正確的表名。
類型的靜態建構函式一般用於類型的待用資料初識化,它會並且僅會執行一次。
(RecordBase<T>若有多個子類,引用的TableName屬性相互獨立。)
看上去這個情境正式靜態建構函式的典型應用。
遺憾的是,上面代碼是有坑的。
MSDN中對靜態建構函式static constructor定義如下:
A static constructor is used to initialize any static data, or to perform a particular action that needs to be performed once only. It is called automatically before the first instance is created or any static members are referenced.
GetByID
方法是在父類RecordBase<T>
中定義的;即便調用時是寫User.GetByID()
,實際上是被引用的還是父類的靜態成員,而不是子類;如果程式之前沒有建立過任何子類執行個體,那子類的靜態建構函式到這裡仍可能未被執行。
也就是說如果程式的其它地方都沒有引用到User子類,那麼User.GetByID()
會拋異常:TableName是空的。
必須先在調用GetByID之前,先建立一個User執行個體(或者調用User而不是父類的靜態成員),才能正常工作,比方說:
var tmp = new User();var u = User.GetByID(1);
如果ORM這麼實現,這就是一個大坑;如果強製程序初始化的時候,寫顯式代碼去調用一下各個子類,也是相當麻煩。
偶找到的解決方案是:添加父類的靜態建構函式,在其中觸發子類的靜態建構函式。
public abstract class RecordBase<T> where T : RecordBase<T>, new() { protected static string TableName; static RecordBase() { System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle); } }
GetByID
是父類的靜態成員,它被調用的話,父類的靜態建構函式便會先被運行時調用,而函數內通過範型有子類的類型,便可通過CompilerServices調用子類的靜態建構函式。
(RunClassConstructor函數即便被多次調用,類型的靜態建構函式也只會被運行一次。)
c#中的靜態建構函式雖然可以做到精準的控制各類型的初始化,需要的時候才調用,不重複調用。
但若是控製得不精準,或者說實現代碼時不充分注意,便可能在程式中留下坑。若缺乏相關經驗知識,那踩中坑時也會困惑不知如何處理。
實際上,我是當年曾經搞過相關的開發,所以這次才特意去測試了不執行個體化子類,直接調用GetByID
以確定這塊是否存在問題。如果我是新手並且對文檔理解不夠細,那采坑是妥妥的。
c#這門語言在很多方面都相當優秀,但具體到類型、模組初始化這塊,做為Go程式員,我會說Go做得更好。
Go語言不允許無效引用,更不允許模組循環參考,確保了所有的Go語言模組/類型有清晰的依賴關係。
而所有模組被引用前,便自動運行模組init
方法。
Go程式員需要考慮的情境簡單,自然也就不容易采坑。
有些語言或者說工具的“強大”是體現在功能繁多上,功能越多,組合情境就越多,程式員便需要去精心琢磨各個具體情境需要如何處理,這實際上是相當大的心智負擔,並且程式往往也只是做到“沒有明顯bug”。
而像Go這樣的語言,它的“強大”之處是體現於提供儘可能相互不重疊的少數功能,程式員可以輕易的考量所有潛在情境,寫“明顯沒有bug”的程式。
這事用術語來說是Orthogonality,偶當年第一次看到這英文詞是不懂,查中文翻譯:正交性。字都認識,但組成詞,依舊是不懂。
:)