摘要:源碼位于項目的命名空間中表示數據實體的接口。獲取實體中發生過變更的屬性集。新辦法解決辦法其實很簡單,正是本文的標題動態生成,徹底解放實現者并確保實現的正確性。業務方不再定義具體的實體類,而是定義實體接口即可,實體類將由實體生成器來動態生成。
前言
由于采用字典的方式來保存屬性變更值的底層設計思想,導致了性能問題,雖然.NET的字典實現已經很高效了,但相對于直接讀寫字段的方式而言依然有巨大的性能差距,同時也會導致對屬性的讀寫過程中產生不必要的裝箱和拆箱。
那么這次我們就來徹底解決這個問題,同時還要解決“哪些屬性發生過變更”、“獲取變更的屬性集”這些功能特性,所以我們先把接口定義出來,以便后續問題講解。
/* 源碼位于 Zongsoft.CoreLibary 項目的 Zongsoft.Data 命名空間中 */ ///設計思想表示數據實體的接口。 public interface IEntity { ? ?///? ?/// 判斷指定的屬性或任意屬性是否被變更過。 ? ?/// ? ?/// 指定要判斷的屬性名數組,如果為空(null)或空數組則表示判斷任意屬性。 ? ?///? ?/// ? ?bool HasChanges(params string[] names); ? ?///如果指定的 ? ?///參數有值,當只有參數中指定的屬性發生過更改則返回真(True),否則返回假(False); 如果指定的 ? ?///參數為空(null)或空數組,當實體中任意屬性發生過更改則返回真(True),否則返回假(False)。 ? ?/// 獲取實體中發生過變更的屬性集。 ? ?/// ? ?///如果實體沒有屬性發生過變更,則返回空(null),否則返回被變更過的屬性鍵值對。 ? ?IDictionaryGetChanges(); ? ?/// ? ?/// 嘗試獲取指定名稱的屬性變更后的值。 ? ?/// ? ?/// 指定要獲取的屬性名。 ? ?/// 輸出參數,指定屬性名對應的變更后的值。 ? ?///如果指定名稱的屬性是存在的并且發生過變更,則返回真(True),否則返回假(False)。 ? ?///注意:即使指定名稱的屬性是存在的,但只要其值未被更改過,也會返回假(False)。 ? ?bool TryGetValue(string name, out object value); ? ?///? ?/// 嘗試設置指定名稱的屬性值。 ? ?/// ? ?/// 指定要設置的屬性名。 ? ?/// 指定要設置的屬性值。 ? ?///如果指定名稱的屬性是存在的并且可寫入,則返回真(True),否則返回假(False)。 ? ?bool TrySetValue(string name, object value); }
根本要點是取消用字典來保存屬性值回歸到字段方式,只有這樣才能確保性能,關鍵問題是如何在寫入字段值的時候,標記對應的屬性發生過變更的呢?應用布隆過濾器(Bloom Filter)算法的思路來處理這個應用場景是一個完美的解決方案,因為布隆過濾器的空間效率和查詢效率極高,而它的缺點在此恰好可以針對性的優化掉。
將每個屬性映射到一個整型數(byte/ushort/uint/ulong)的某個比特位(bit),如果發生過變更則將該位置一,只要確保屬性與二進制位順序是確定的即可,算法復雜度是O(1),并且比特位操作的效率也是極高的。
實現示范有了算法,我們寫一個簡單范例來感受下:
public class Person : IEntity { ? ?#region 靜態字段 ? ?private static readonly string[] __NAMES__ = new string[] { "Name", "Gender", "Birthdate" }; ? ?private static readonly Dictionary> __TOKENS__ = new Dictionary >() ? ?{ ? ? ? ?{ "Name", new PropertyToken (0, target => target._name, (target, value) => target.Name = (string) value) }, ? ? ? ?{ "Gender", new PropertyToken (1, target => target._gender, (target, value) => target.Gender = (Gender?) value) }, ? ? ? ?{ "Birthdate", new PropertyToken (2, target => target._birthdate, (target, value) => target.Birthdate = (DateTime) value) }, ? ?}; ? ?#endregion ? ?#region 標記變量 ? ?private byte _MASK_; ? ?#endregion ? ?#region 成員字段 ? ?private string _name; ? ?private bool? _gender; ? ?private DateTime _birthdate; ? ?#endregion ? ?#region 公共屬性 ? ?public string Name ? ?{ ? ? ? ?get => _name; ? ? ? ?set ? ? ? ?{ ? ? ? ? ? ?_name = value; ? ? ? ? ? ?_MASK_ |= 1; ? ? ? ?} ? ?} ? ?public bool? Gender ? ?{ ? ? ? ?get => _gender; ? ? ? ?set ? ? ? ?{ ? ? ? ? ? ?_gender = value; ? ? ? ? ? ?_MASK_ |= 2; ? ? ? ?} ? ?} ? ?public DateTime Birthdate ? ?{ ? ? ? ?get => _birthdate; ? ? ? ?set ? ? ? ?{ ? ? ? ? ? ?_birthdate = value; ? ? ? ? ? ?_MASK_ |= 4; ? ? ? ?} ? ?} ? ?#endregion ? ?#region 接口實現 ? ?public bool HasChanges(string[] names) ? ?{ ? ? ? ?PropertyToken property; ? ? ? ?if(names == null || names.Length == 0) ? ? ? ? ? ?return _MASK_ != 0; ? ? ? ?for(var i = 0; i < names.Length; i++) ? ? ? ?{ ? ? ? ? ? ?if(__TOKENS__.TryGetValue(names[i], out property) && (_MASK_ >> property.Ordinal & 1) == 1) ? ? ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ?public IDictionary GetChanges() ? ?{ ? ? ? ?if(_MASK_ == 0) ? ? ? ? ? ?return null; ? ? ? ?var dictionary = new Dictionary (__NAMES__.Length); ? ? ? ?for(int i = 0; i < __NAMES__.Length; i++) ? ? ? ?{ ? ? ? ? ? ?if((_MASK_ >> i & 1) == 1) ? ? ? ? ? ? ? ?dictionary[__NAMES__[i]] = __TOKENS__[__NAMES__[i]].Getter(this); ? ? ? ?} ? ? ? ?return dictionary; ? ?} ? ?public bool TryGetValue(string name, out object value) ? ?{ ? ? ? ?value = null; ? ? ? ?if(__TOKENS__.TryGetValue(name, out var property) && (_MASK_ >> property.Ordinal & 1) == 1) ? ? ? ?{ ? ? ? ? ? ?value = property.Getter(this); ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ?public bool TrySetValue(string name, object value) ? ?{ ? ? ? ?if(__TOKENS__.TryGetValue(name, out var property)) ? ? ? ?{ ? ? ? ? ? ?property.Setter(this, value); ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ?#endregion } // 輔助結構 public struct PropertyToken { ? ?public PropertyToken(int ordinal, Func getter, Action setter) ? ?{ ? ? ? ?this.Ordinal = ordinal; ? ? ? ?this.Getter = getter; ? ? ? ?this.Setter = setter; ? ?} ? ?public readonly int Ordinal; ? ?public readonly Func Getter; ? ?public readonly Action Setter; }
上面實現代碼,主要有以下幾個要點:
屬性設置器中除了對字段賦值外,多了一個位或賦值操作(這是一句非常低成本的代碼);
需要一個額外的整型數的實例字段 _MASK_ ,來標記對應更改屬性序號;
分別增加 __NAMES__ 和 __TOKENS__ 兩個靜態只讀變量,來保存實體類的元數據,以便更高效的實現 IEntity 接口方法。
根據代碼可分析出其理論執行性能與原生實現基本一致,內存消耗只多了一個字節(如果可寫屬性數量小于9),由于 __NAMES__ 和 __TOKENS__ 是靜態變量,因此不占用實例空間,理論上該方案的整體效率非常高。
性能對比上面我們從代碼角度簡單分析了下整個方案的性能和消耗,那么實際情況到底怎樣呢?跑個分唄(性能對比測試代碼地址:https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/feature-data/samples/Zongsoft.Samples.Entities),具體代碼就不在這里占用版面了,下面給出某次在我的老舊臺式機(CPU:Intel i5-3470@3.2GHz | RAM:8GB | Win10| .NET 4.6)上生成100萬個實例的截圖:
“Native Object: 295”表示原生實現版(即簡單的讀寫字段)的運行時長(單位:毫秒,下同);
“Data Entity: 295”為本案的運行時長,通常本方案比原生方案要慢10毫秒左右,偶爾能跑平(屬于運行環境抖動,可忽略);
“Data Entity(TrySet): 835”為本方案中 TrySet(...) 方法的運行時長,由于 TrySet(...) 方法內部需要進行字典查詢所以有性能損耗亦屬正常,在百萬量級跑到這個時長說明性能也是很不錯的,如果切換到 .NET Core 2.1 的話,得益于基礎類庫的性能改善,還能再享受一波性能紅利。
綜上所述,該方案付出極少的內存成本獲得了與原生簡單屬性訪問基本一致的性能,同時還提供了屬性變更跟蹤等新功能(即高效完成了 Zongsoft.Data.IEntity 接口中定義的那些重要功能特性),為后續業務開發提供了有力的基礎支撐。
實現完善上面的實現范例代碼并沒有實現 INotifyPropertyChanged 接口,下面補充完善下實現該接口后的屬性定義:
public class Person : 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(nameof(Name))); ? ? ? ?} ? ?} }
如上,屬性的設置器中的做了一個新舊值的比對判斷和對 PropertyChanged 事件激發,其他代碼沒有變化。
另外,我們使用的是 byte 類型的 _MASK_ 的標記變量來保存屬性的更改狀態,如果當實體的屬性數量超過 8 個,就需要根據具體數量換成相應的 UInt16,UInt32,UInt64 類型,但如果超過 64 就需要采用 byte[] 了,當然必須要變動下相關代碼,假設以下實體類有 100 個屬性(注意僅例舉了第一個 Property1 和最后一個 Property100 屬性):
public class MyEntity : IEntity { #region 標記變量 private readonly byte[] _MASK_; #endregion public Person() { _MASK_ = new byte[13]; // 13 = Math.Ceiling(100 / 8) } public object Property1 { get => _property1; set { _property1 = value; _MASKS_[0] |= 1; // _MASK_[0 / 8] |= (byte)Math.Pow(2, 0 % 8); } } public object Property100 { get => _property100; set { _property100 = value; _MASKS_[12] |= 8; // _MASK_[99 / 8] |= (byte)Math.Pow(2, 99 % 8); } } }
變化內容為先根據當前屬性的順序號來確定到對應的標記數組的下標,然后再確定對應的掩碼值。當然,也別忘了調整 Zongsoft.Data.IEntity 接口中各方法的實現。
public class MyEntity : IEntity { ? ?public bool HasChanges(params string[] names) ? ?{ ? ? ? ?PropertyTokenproperty; ? ? ? ?if(names == null || names.Length == 0) ? ? ? ?{ ? ? ? ? ? ?for(int i = 0; i < _MASK_.Length; i++) ? ? ? ? ? ?{ ? ? ? ? ? ? ? ?if(_MASK_[i] != 0) ? ? ? ? ? ? ? ? ? ?return true; ? ? ? ? ? ?} ? ? ? ? ? ?return false; ? ? ? ?} ? ? ? ?for(var i = 0; i < names.Length; i++) ? ? ? ?{ ? ? ? ? ? ?if(__TOKENS__.TryGetValue(names[i], out property) && (_MASK_[property.Ordinal / 8] >> (property.Ordinal % 8) & 1) == 1) ? ? ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} ? ?public IDictionary GetChanges() ? ?{ ? ? ? ?var dictionary = new Dictionary (__NAMES__.Length); ? ? ? ?for(int i = 0; i < __NAMES__.Length; i++) ? ? ? ?{ ? ? ? ? ? ?if((_MASK_[i / 8] >> (i % 8) & 1) == 1) ? ? ? ? ? ? ? ?dictionary[__NAMES__[i]] = __TOKENS__[__NAMES__[i]].Getter(this); ? ? ? ?} ? ? ? ?return dictionary.Count == 0 ? null : dictionary; ? ?} ? ?public bool TryGet(string name, out object value) ? ?{ ? ? ? ?value = null; ? ? ? ?if(__TOKENS__.TryGetValue(name, out var property) && (_MASK_[property.Ordinal / 8] >> (property.Ordinal % 8) & 1) == 1) ? ? ? ?{ ? ? ? ? ? ?value = property.Getter(this); ? ? ? ? ? ?return true; ? ? ? ?} ? ? ? ?return false; ? ?} public bool TrySetValue(string name, object value) { /* 相對之前版本沒有變化 */ /* No changes relative to previous versions */ } }
代碼變化部分比較簡單,只有掩碼處理部分需要調整。
新問題有了這些實現范式,定義個實體基類并在基類中完成主要功能即可推廣應用了,但是,這里有個掩碼類型和處理方式無法通用化實現的問題,如果要把這部分代碼交由子類來實現的話,那么代碼復用度會大打折扣甚至完全失去復用的意義。
為展示這個問題的艱難,在 https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/feature-data/tests/Entities.cs 源文件中,寫了屬性數量不等的幾個實體類(Person、Customer、Employee、SpecialEmployee),采用繼承方式進行復用性驗證,可清晰看到實現的非常冗長繁瑣,對實現者的細節把控要求很高、實現上非常容易出錯,更致命的是復用度還極差。并且當實體類需要進行屬性增減,是非常麻煩的,需要仔細調整原有代碼結構中掩碼的映射位置,這對于代碼維護無意是場惡夢。
新辦法解決辦法其實很簡單,正是本文的標題——“動態生成”,徹底解放實現者并確保實現的正確性。業務方不再定義具體的實體類,而是定義實體接口即可,實體類將由實體生成器來動態生成。我們依然“從場景出發”,先來看看業務層的使用。
public interface IPerson : IEntity { string Name { get; set; } bool? Gender { get; set; } DateTime Birthdate { get; set; } } public interface IEmployee : IPerson { byte Status { get; set; } decimal Salary { get; set; } } var person = Entity.Build總結(); var employee = Entity.Build ();
至此,終于得到了一個兼顧性能與功能并易于使用且無需繁瑣的手動實現的最終方案,雖然剛開始看起來是一個多么平常又簡單的任務。那么接下來我們該怎么實現這個動態生成器呢?最終它能性能無損的被實現出來嗎? 請關注我們的公眾號(Zongsoft)留言討論。
提示本文可能會更新,請閱讀原文: https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-2,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。
本作品采用?知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但必須保留本文的署名 鐘峰(包含鏈接:http://zongsoft.github.io),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布。如有任何疑問或授權方面的協商,請致信給我 (zongsoft@qq.com)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76655.html
摘要:前言在應用開發中,通常都會涉及各種實體類的編寫,有時這些實體類還需要實現接口以支持屬性變更通知,一般我們都會手寫這些代碼或者通過工具根據數據庫表定義抑或別的什么模板映射文件之類的來生成它們。 前言 在應用開發中,通常都會涉及各種 POJO/POCO 實體類(DO, DTO, BO, VO)的編寫,有時這些實體類還需要實現 INotifyPropertyChanged 接口以支持屬性變更...
摘要:與靜態代理對比,動態代理是在動態生成代理類,由代理類完成對具體方法的封裝,實現的功能。本文將分析中兩種動態代理的實現方式,和,比較它們的異同。那如何動態編譯呢你可以使用,這是一個封裝了的庫,幫助你方便地實現動態編譯源代碼。 發現Java面試很喜歡問Spring AOP怎么實現的之類的問題,所以寫一篇文章來整理一下。關于AOP和代理模式的概念這里并不做贅述,而是直奔主題,即AOP的實現方...
閱讀 1534·2023-04-26 02:50
閱讀 3535·2023-04-26 00:28
閱讀 1931·2023-04-25 15:18
閱讀 3209·2021-11-24 10:31
閱讀 986·2019-08-30 13:00
閱讀 1000·2019-08-29 15:19
閱讀 1766·2019-08-29 13:09
閱讀 2975·2019-08-29 13:06