实体类的动态生成(三)
前言
在 .NET 中主要有兩種動態生成并編譯的方式,一種是通過?System.Linq.Expressions?命名空間中的?LambdaExpression?類的?CompileToMethod(...)?方法,但是這種方法只支持動態編譯到靜態方法,因為這個限制我們只能放棄它而采用 Emitting 生成編譯方案,雖然 Emitting 方案強大但是實現起來麻煩不少,必須要手動處理底層 IL 的各種細節,腦補一些 C# 編譯器的實現機理,同時還要了解一些基本的?IL(Intermediate?Language) 和?CLR(JVM) 執行方面的知識。
基礎知識
因為要采用 Emitting 技術方案,必然需要了解 IL,如果你之前沒有怎么接觸過,也不用灰心,網上有大量關于 IL 的入門文章,“30分鐘入門”還是沒問題的哈,畢竟 IL 相對 8086/8088 匯編來說,真的平易近人太多了。
首先你需要一個類似 ILSpy(http://ilspy.net) 這樣的工具來查看生成的IL或反編譯程序集,最近版本還會提供 IL 與對應 C# 的比照解釋,用戶體驗真是體貼得不要不要的。
一、不同于 8086/8088 這樣基于寄存器的指令集,IL 和 Java 字節碼一樣都是基于棧的指令集,它們最明顯的區別就是指令的參數指定方式的差異。以“int x = 100+200”操作為例,IL 的指令序列大致是:
ldc.i4 100 ldc.i4 200 add stlocl.0- 前兩行代碼分別將100和200這兩個32位整數加載到運算棧(Evaluation?Stack)中; 
- 第3行的 add 是加法運算指令,它會從運算棧彈出(Pop)兩次以得到它需要的兩個操作數(Operand),計算完成后又會將自己的計算結果壓入(Push)到計算棧中,這時棧頂的元素就是累加的結果(即整數300); 
- 第4行的 stloc.0 是設置本地變量的指令,它會從計算棧彈出(Pop)一個元素,然后將該元素保存到特定本地變量中(本示例是第一個本地變量)。注:本地變量必須由方法預先聲明。 
二、基本上匯編語言或類似 IL 這樣的中間指令集都沒有高級語言中天經地義的 if/else、switch/case、do/while、for/foreach 這樣的基本語言結構,它們只有類似 goto/jump/br 這樣的無條件跳轉和 br.true/br.false/beq/blt/bgt/ceq/clt/cgt 等之類的條件跳轉指令,高級語言中的很多基本語言結構都是由編譯器或解釋器轉換成底層的跳轉結構的,所以在 Emitting 中我們也需要腦補編譯器中這樣的翻譯機制,將那些 if/else、while、for 之類的翻譯成對應的跳轉結構。
需要特別指出的是,因為 C/C++/C#/JAVA 之類的高級語言的邏輯運算中有“短路”的內置約定,所以在轉換成跳轉結構時,必須留意處理這個問題,否則會破壞語義并可能導致運行時錯誤。
三、因為 IL 支持類名、字段、屬性、方法等元素名稱中包含除字母、數字、下劃線之外的其他字符,所有各高級語言編譯器都會利用該特性,主要是為了避免與特定高級語言中用戶代碼發生命名沖突,我們亦會采用該策略。
有了上面的基礎知識,自己稍微花點時間閱讀一些 IL 代碼,再來翻閱 Zongsoft.Data.Entity 類的源碼就簡單了。
另外,在反編譯閱讀 IL 代碼的時候,如果你反編譯的是 Debug 版本,會發現生成的 IL 對本地變量的處理非常啰嗦,重復保存又緊接著加載本地變量的操作,這是因為編譯器沒有做優化導致,不用擔心,換成用 Release 編譯就好很多了,但是依然還是有一些手動優化的空間。
接口說明
實體動態生成器類的源碼位于 Zongsoft.CoreLibrary 項目中(https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/feature-data/src/Data/Entity.cs),這是一個靜態類,其主要公共方法定義如下:
public static Entity{public static T Build<T>();public static T Build<T>(Action<T> map);public static IEnumerable<T> Build<T>(int count, Action<T, int> map = null);public static object Build(Type type);public static object Build(Type type, Action<object> map);public static IEnumerable Build(Type type, int count, Action<object, int> map = null);
}
公共的?Save()?方法是一個供調試之用的方法,它會將動態編譯的程序集保存到文件中,以便使用 ILSpy 這樣的工具反編譯查看,待 feature-data 合并到 master 分支之后會被移除。
關于跑分
在 https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/feature-data/samples/Zongsoft.Samples.Entities/Program.cs?類中的?PerformanceDynamic(int count)?是動態生成的跑分(性能測試)代碼,需要注意的是,如果是首次動態創建某個實體接口,內部會先進行動態編譯。
下面這兩種方式跑分測試方式會有不同的性能表現,大家先琢磨下原因再接著往下閱讀。
private static void PerformanceDynamic(int count){// 獲取構建委托,可能會觸發內部的預先編譯(即預熱)var creator = Data.Entity.GetCreator(typeof(Models.IUserEntity));// 創建跑分計時器var stopwatch = new Stopwatch();stopwatch.Start(); //開始計時/* 第一種跑分 */for(int i = 0; i < count; i++){// 調用構建委托來創建實體類實例var user = (Models.IUserEntity)creator();user.UserId = (uint)i;user.Avatar = ":smile:";user.Name = "Name: " + i.ToString();user.FullName = "FullName";user.Namespace = "Zongsoft";user.Status = (byte)(i % byte.MaxValue);user.StatusTimestamp = (i % 11 == 0) ? DateTime.Now : DateTime.MinValue;user.CreatedTime = DateTime.Now;}stopwatch.Restart(); //重新計時/* 第二種跑分 */int index = 0;// 動態構建指定 count 個實體類實例(懶構建)var entities = Data.Entity.Build<Models.IUserEntity>(count);foreach(var user in entities){user.UserId = (uint)index;user.Avatar = ":smile:";user.Name = "Name: " + index.ToString();user.FullName = "FullName";user.Namespace = "Zongsoft";user.Status = (byte)(index % byte.MaxValue);user.StatusTimestamp = (index++ % 11 == 0) ? DateTime.Now : DateTime.MinValue;user.CreatedTime = DateTime.Now;}stopwatch.Stop(); //停止計時
}
在我的老臺式機上跑一百萬(即count=1,000,000)次,第二種跑分代碼比第一種差不多要慢50~100毫秒左右,兩者區別就在于 for 循環與 Enumerable/Enumerator 模式的區別,我曾嘗試對?Build<T>(int count)?方法內部的?yield return?(由C#編譯器將該語句翻譯成 Enumerable/Enumerator 模式)改為手動實現,優化的思路是:因為在這個場景中,我們已知?count?數量,基于這個必要條件可以剔除 Enumerator 循環中一些不必要的條件判斷代碼。但是手動寫了 Enumerable/Enumerator 后發現,為了代碼安全性還是無法省略一些必要的條件判斷,因為不能確定用戶是否會采用 entities.GetEnumerator() + while 的方式來調用,也就是說即使在確定?count?的條件下也占不到任何性能上的便宜,畢竟基本的代碼安全性還是要優先保障的。
如上述所述,動態生成的代碼并無性能問題,只是在應對一次性創建上百萬個實體實例并遍歷的場景下,為了排除 Enumerable/Enumerator 模式對性能的一點點“干擾”(這是必須的)采取了一點優化手段,在實際業務中通常不需這么處理,特此說明。
使用說明
將原有業務系統中各種實體類改為接口,這些接口可以繼承自?Zongsoft.Data.IEntity?也可以不用,不管實體接口是否從?Zongsoft.Data.IEntity?接口繼承,動態生成的實體類都會實現該接口,因此依然可以將動態創建的實體實例強制轉換為該接口。
注意:實體接口中不能含有事件、方法定義,即只能包含屬性定義。
變更通知
如果實體需要支持屬性變更通知,則實體接口必須增加對?System.ComponentModel.INotifyPropertyChanged?接口的繼承,但這樣的支持需要付出一點點性能成本,以下是動態生成后的部分C#代碼。
public interface IPerson{string Name { get; set; }
}
// 不支持的屬性變更通知版本
public class Person : IPerson, IEntity
{public string Name
? ?{get => _name;set => {_name = value;_MASK_ |= 1;}}
}
/* 增加對屬性變更通知的特性 */
public interface IPerson : INotifyPropertyChanged
{string Name { get; set; }
}
// 支持屬性變更通知版本
public class Person : IPerson, IEntity, INotifyPropertyChanged
{// 事件聲明public event PropertyChangedEventHandler PropertyChanged;public string Name
? ?{get => _name;set => {if(_name == value) ?// 新舊值比對判斷return;_name = value;_MASK_ |= 1;this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));}}
}
所謂一點點性能成本有兩點:①需要對新舊值進行比對,比對方法的實現性能對此處有至關影響;②對?PropertyChanged?事件的有效性判斷并調用事件委托。當然,如果這是必須的 feature 需求,那就無所謂成本了。
提示:關于新舊值比對的說明,如果屬性類型是基元類型,動態生成器會生成 bne/be 這樣的特定 IL 指令;否則如果該類型重寫了 == 操作符則會使用該操作符的實現;否則會調用 Object.Equals(...) 靜態方法來比對。
擴展屬性
在某些場景,需要手動處理屬性的 getter 或 setter 的業務邏輯,那該如何在動態生成中植入這些邏輯代碼呢?在?Zongsoft.Data.Entity?類中有個?PropertyAttribute?自定義特性類,可以利用它來聲明擴展屬性的實現。譬如下面的示例:
public static UserExtension{public static string GetAvatarUrl(IUser user){if(string.IsNullOrEmpty(user.Avatar))return null;return "URL:" + user.Avatar;}
}
public interface IUser
{string Avatar { get; set; }[Entity.Property(Entity.PropertyImplementationMode.Extension, typeof(UserExtension))]string AvatarUrl { get; }
}
/*以下的 User 實體類為動態生成器生成的部分示意代碼。 */
public class User : IUser, IEntity
{private string _avatar;public string Avatar
? ?{get => _avatar;set {_avatar = value;_MASK_ |= xxx;}}public string AvatarUrl
? ?{get {return UserExtension.GetAvatarUrl(this);}}
}
上面的代碼比較好理解,就不多說,如果?IUser?接口中的?AvatarUrl?屬性是可讀寫屬性或者有?System.ComponentModel.DefaultValueAttribute?自定義特性修飾,那么該屬性就會有對應的字段,對應的屬性擴展方法也可以獲取該字段值。
public static class UserExtension{public static string GetAvatarUrl(IUser user, string value){if(string.IsNullOrEmpty(value))return $"http://...{user.Avatar}...";return value;}
}
public interface IUser
{string Avatar { get; set; }[Entity.Property(Entity.PropertyImplementationMode.Extension, typeof(UserExtension))]string AvatarUrl { get; set; }
}
/*以下的 User 實體類為動態生成器生成的部分示意代碼。 */
public class User : IUser, IEntity
{private string _avatar;private string _avatarUrl;public string Avatar
? ?{get => _avatar;set {_avatar = value;_MASK_ |= xxx;}}// 只有讀取獲取擴展方法public string AvatarUrl
? ?{get => Extension.GetAvatarUrl(this, _avatarUrl);set {_avatarUrl = value;_MASK_ |= xxx;}}
}
當然擴展屬性方法支持讀寫兩種,下面是同時實現了兩個版本的擴展方法的樣子:
public static class UserExtension{public static string GetAvatarUrl(IUser user, string value){throw new NotImplementedException();}public static bool SetAvatarUrl(IUser user, string value){throw new NotImplementedException();}
}
/*以下的 User 實體類為動態生成器生成的部分示意代碼。 */
public class User : IUser, IEntity
{public string AvatarUrl
? ?{get => UserExtension.GetAvatarUrl(this, _avatarUrl);set {if(UserExtension.SetAvatarUrl(this, _avatarUrl)){_avatarUrl = value;_MASK_ |= xxx;}}}
}
擴展屬性方法的定義約定:
必須是一個公共的靜態方法;
讀取方法名以 Get 打頭,后面接擴展屬性名并區分大小寫;
讀取方法的第一個參數必須是要擴展實體接口類型,第二個參數可選,如果有的話必須是擴展屬性的類型;返回類型必須是擴展屬性的類型;
設置方法名以 Set 打頭,后面接擴展屬性名并區分大小寫;
設置方法的第一個參數必須是要擴展實體接口類型,第二參數是擴展屬性的類型,表示設置的新值;返回類型必須是布爾類型,返回真(True)表示設置成功否則返回失敗(False),只有返回真對應的成員字段才會被設置更新。
單例模式
某些場景中,屬性需要采用單例模式來實現,譬如一些集合類型的屬性。
public interface IDepartment{[Entity.Property(Entity.PropertyImplementationMode.Singleton)]ICollection<IUser> Users { get; }
}
/*以下的 Department 實體類為動態生成器生成的部分示意代碼。 */
public class Department : IDepartment, IEntity
{private readonly object _users_LOCK;private ICollection<IUser> _users;public Department(){_users_LOCK = new object();}public ICollection<IUser> Users
? ?{get {if(_users == null) {lock(_users_LOCK) {if(_users == null) {_users = new List<IUser>();}}}return _users;}}
}
實現采用的是雙檢鎖模式,必須注意到,每個單例屬性都會額外占用一個用于雙檢鎖的?object?類型變量。
如果屬性類型是集合接口,那么動態生成器會選擇一個合適的實現該接口的集合類;當然,你也可以自定義一個工廠方法來創建對應的實例,在實體屬性中通過?PropertyAttribute?自定特性中聲明工廠方法所在的類型即可。
注意:工廠方法必須是一個公共的靜態方法,有一個可選的參數,參數類型為實體接口類型。
public static class DepartmentExtension{public static ICollection<IUser> GetUsers(IDepartment department){return new MyUserCollection(department);}
}
public interface IDepartment
{[Entity.Property(Entity.PropertyImplementationMode.Singleton, typeof(DepartmentExtension))]ICollection<IUser> Users { get; }
}
/*以下的 Department 實體類為動態生成器生成的部分示意代碼。 */
public class Department : IDepartment, IEntity
{private readonly object _users_LOCK;private ICollection<IUser> _users;public Department(){_users_LOCK = new object();}public ICollection<IUser> Users
? ?{get {if(_users == null) {lock(_users_LOCK) {if(_users == null) {_users = DepartmentExtension.GetUsers(this);}}}return _users;}}
}
默認值和自定義初始化
有時我們需要只讀屬性,但又不需要單例模式這種相對較重的實現機制,可以采用?DefaultValueAttribute?這個自定義特性來處理這種情況。
提示:實體接口或屬性聲明的所有自定義特性都會被生成器添加到實體類的對應元素中,后面的演示代碼可能會省略這些生成的自定義特性,特此說明。
public interface IDepartment{[DefaultValue("Popeye")]string Name { get; set; }[DefaultValue]ICollection<IUser> Users { get; }
}
/*以下的 Department 實體類為動態生成器生成的部分示意代碼。 */
public class Department : IDepartment, IEntity
{private string _name;private ICollection<IUser> _users;public Department(){_name = "Popeye";_users = new List<IUser>();}[DefaultValue("Popeye")]public string Name
? ?{get => _name;set {_name = value;_MASK_ |= xxx;}}[DefaultValue()]public ICollection<IUser> Users
? ?{get => _users;}
}
除了支持固定(Mutable)默認值,還支持動態(Immutable)的,所謂動態值是指它的值不在?DefaultValueAttribute?中被固化,即指定?DefaultValueAttribute?的值為一個靜態類的類型,該靜態類中必須有一個名為 Get 打頭并以屬性名結尾的方法,該方法可以沒有參數,也可以有一個實體接口類型的參數,如下所示。
public static DepartmentExtension{public static DateTime GetCreationDate(){return DateTime.Now;}
}
public interface IDepartment
{[DefaultValue(typeof(DepartmentExtension))]DateTime CreationDate { get; }
}
/*以下的 Department 實體類為動態生成器生成的部分示意代碼。 */
public class Department : IDepartment, IEntity
{private DateTime _creationDate;public Department(){_creationDate = DepartmentExtension.GetCreationDate();}public DateTime CreationDate
? ?{get => _creationDate;}
}
如果?DefaultValueAttribute?默認值自定義特性中指定的是一個類型(即?System.Type),并且該類型不是一個靜態類的類型,并且屬性類型也不是?System.Type?的話,那則表示該類型為屬性的實際類型,這對于某些屬性被聲明為接口或基類的情況下尤為有用,如下所示。
public interface IDepartment{[DefaultValue(typeof(MyManager))]IUser Manager { get; set; }[DefaultValue(typeof(MyUserCollection))]ICollection<IUser> Users { get; }
}
/*以下的 Department 實體類為動態生成器生成的部分示意代碼。 */
public class Department : IDepartment, IEntity
{private IUser _manager;private ICollection<IUser> _users;public Department(){_managert = new MyManager();_users = new MyUserCollection();}public IUser Manager
? ?{get => _manager;set => _manager = value;}public ICollection<IUser> Users
? ?{get => _users;}
}
其他說明
默認生成的實體屬性為公共屬性(即非顯式實現方式),當出現實體接口在繼承中發生了屬性重名,或因為某些特殊需求導致必須對某個實體屬性以顯式方式實現,則可通過?Entity.PropertyAttribute?自定特性中的?IsExplicitImplementation=true?來開啟顯式實現機制。
在實體接口中聲明的各種自定義特性(Attribute),都會被動態生成器原樣添加到生成的實體類中。因此之前范例中,凡是接口以及接口的屬性聲明的各種自定義特性(包括:DefaultValueAttribute?、?Entity.PropertyAttribute?)都會被添加到動態生成的實體類的相應元素中,這對于某些應用是一個必須被支持的特性。
性能測試
在《實體類的動態生成(二)》中,我們已經驗證過設計方案的執行性能了,但結合上面介紹的功能特性細節,還需再提醒的是:因為開啟?DefaultValueAttribute?、擴展屬性方法、單例屬性、屬性變更通知都會導致生成的代碼與最基本字段訪問方式有所功能增強,對應要跑的代碼量增多,因此對跑分是有影響,但這種影響是確定可知的,它們是 feature 所需并非實現方案、算法缺陷所致,敬請知曉。
譬如圖二就是增加了屬性變更通知(即實體接口繼承了?INotifyPropertyChanged)導致的性能影響(Dynamic Entity 所在行)。
圖一
圖二
寫在最后的話
該實體類動態生成器簡單易用、運行性能和內存利用率都非常不錯(包括提供 IEntiy 接口的超贊功能),將會成為今后我們所有業務系統的基礎結構之一,所以后續的文章中(如果還有的話)應該會經常看到它的應用。
算下來花了整整三天時間(白天晚上都在寫)才完成《實體類的動態生成》系列文章,真心覺得寫文章比寫代碼還累,而且這還是省略了應該配有的一些流程圖、架構圖的情況下。計劃接下來我會為?Zongsoft(https://github.com/Zongsoft) 系列開源項目撰寫該有的所有文檔,照這次這個寫法,心底不由升起一絲莫名恐懼和淡淡憂傷來。
如果你覺得這次的文章對你有所幫助,又或者你覺得我們的開源項目做的還不錯,請務必為我們點贊并關注我們的公眾號,這或許是我堅持寫下去的最大動力來源了。
最后,因為寫這個東西耽擱了不少造 Zongsoft.Data 這個輪子的時間,所以接下來得全力去造輪子了。打算每周至少一篇干貨滿滿的技術文章在公眾號首發,希望不會讓自己失望吧。
關于 Zongsoft.Data?它一定會是一款性能滿血、易用且足夠靈活的數據引擎,首發即會支持四大關系型數據庫,后續會加入對?Elasticsearch?的支持,總之,它應該是不同于市面上任何一款 ORM 數據引擎的開源產品。我會陸續與大家分享有關它的一些設計思考以及實現中遇到的問題,當然,也可以在 github 上圍觀我的進展。
相關文章:
- 實體類的動態生成(一) 
- 實體類的動態生成(二) 
原文地址:http://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-3/
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com 
總結
以上是生活随笔為你收集整理的实体类的动态生成(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: MongoDB发布4.0版本,支持ACI
- 下一篇: Quartz.Net分布式任务管理平台(
