實體類的動態生成(二)

来源:https://www.cnblogs.com/Zongsoft/archive/2018/07/21/entity-dynamic-generation-2.html
-Advertisement-
Play Games

由於採用字典的方式來保存屬性變更值的底層設計思想,導致了性能問題,雖然.NET的字典實現已經很高效了,但相對於直接讀寫欄位的方式而言依然有巨大的性能差距,那麼這次我們就來徹底解決這個問題…… ...


前言

由於採用字典的方式來保存屬性變更值的底層設計思想,導致了性能問題,雖然.NET的字典實現已經很高效了,但相對於直接讀寫欄位的方式而言依然有巨大的性能差距,同時也會導致對屬性的讀寫過程中產生不必要的裝箱和拆箱。
那麼這次我們就來徹底解決這個問題,同時還要解決“哪些屬性發生過變更”、“獲取變更的屬性集”這些功能特性,所以我們先把介面定義出來,以便後續問題講解。

/* 源碼位於 Zongsoft.CoreLibary 項目的 Zongsoft.Data 命名空間中 */

/// <summary> 表示數據實體的介面。</summary>
public interface IEntity
{
    /// <summary>
    /// 判斷指定的屬性或任意屬性是否被變更過。
    /// </summary>
    /// <param name="names">指定要判斷的屬性名數組,如果為空(null)或空數組則表示判斷任意屬性。</param>
    /// <returns>
    ///     <para>如果指定的<paramref name="names"/>參數有值,當只有參數中指定的屬性發生過更改則返回真(True),否則返回假(False);</para>
    ///     <para>如果指定的<paramref name="names"/>參數為空(null)或空數組,當實體中任意屬性發生過更改則返回真(True),否則返回假(False)。</para>
    /// </returns>
    bool HasChanges(params string[] names);

    /// <summary>
    /// 獲取實體中發生過變更的屬性集。
    /// </summary>
    /// <returns>如果實體沒有屬性發生過變更,則返回空(null),否則返回被變更過的屬性鍵值對。</returns>
    IDictionary<string, object> GetChanges();

    /// <summary>
    /// 嘗試獲取指定名稱的屬性變更後的值。
    /// </summary>
    /// <param name="name">指定要獲取的屬性名。</param>
    /// <param name="value">輸出參數,指定屬性名對應的變更後的值。</param>
    /// <returns>如果指定名稱的屬性是存在的並且發生過變更,則返回真(True),否則返回假(False)。</returns>
    /// <remarks>註意:即使指定名稱的屬性是存在的,但只要其值未被更改過,也會返回假(False)。</remarks>
    bool TryGetValue(string name, out object value);

    /// <summary>
    /// 嘗試設置指定名稱的屬性值。
    /// </summary>
    /// <param name="name">指定要設置的屬性名。</param>
    /// <param name="value">指定要設置的屬性值。</param>
    /// <returns>如果指定名稱的屬性是存在的並且可寫入,則返回真(True),否則返回假(False)。</returns>
    bool TrySetValue(string name, object value);
}

設計思想

根本要點是取消用字典來保存屬性值回歸到欄位方式,只有這樣才能確保性能,關鍵問題是如何在寫入欄位值的時候,標記對應的屬性發生過變更的呢?應用布隆過濾器(Bloom Filter)演算法的思路來處理這個應用場景是一個完美的解決方案,因為布隆過濾器的空間效率和查詢效率極高,而它的缺點在此恰好可以針對性的優化掉。

將每個屬性映射到一個整型數(byte/ushort/uint/ulong)的某個比特位(bit),如果發生過變更則將該 bit 置為 1,只要確保屬性與二進位位順序是確定的即可,演算法複雜度是O(1)常量,並且比特位操作的效率也是極高的。

實現示範

有了演算法,我們寫一個簡單範例來感受下:

public class Person : IEntity
{
    #region 靜態欄位
    private static readonly string[] __NAMES__ = new string[] { "Name", "Gender", "Birthdate" };
    private static readonly Dictionary<string, PropertyToken<Person>> __TOKENS__ = new Dictionary<string, PropertyToken<Person>>()
    {
        { "Name", new PropertyToken<Person>(0, target => target._name, (target, value) => target.Name = (string) value) },
        { "Gender", new PropertyToken<Person>(1, target => target._gender, (target, value) => target.Gender = (Gender?) value) },
        { "Birthdate", new PropertyToken<Person>(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<Person> 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<string, object> GetChanges()
    {
        if(_MASK_ == 0)
            return null;

        var dictionary = new Dictionary<string, object>(__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<T>
{
    public PropertyToken(int ordinal, Func<T, object> getter, Action<T, object> setter)
    {
        this.Ordinal = ordinal;
        this.Getter = getter;
        this.Setter = setter;
    }

    public readonly int Ordinal;
    public readonly Func<T, object> Getter;
    public readonly Action<T, object> Setter;
}

上面實現代碼,主要有以下幾個要點:

  1. 屬性設置器中除了對欄位賦值外,多了一個位或賦值操作(這是一句非常低成本的代碼);
  2. 需要一個額外的整型數的實例欄位 _MASK_ ,來標記對應更改屬性序號;
  3. 分別增加 __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)
    {
        PropertyToken<UserEntity> property;

        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<string, object> GetChanges()
    {
        var dictionary = new Dictionary<string, object>(__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<IPerson>();
var employee = Entity.Build<IEmployee>();

總結

至此,終於得到了一個兼顧性能與功能並易於使用且無需繁瑣的手動實現的最終方案,雖然剛開始看起來是一個多麼平常又簡單的任務。那麼接下來我們該怎麼實現這個動態生成器呢?最終它能性能無損的被實現出來嗎?請關註我們的公眾號(Zongsoft)留言討論。

提示:

本文可能會更新,請閱讀原文:https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-2,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。


敬請期待更精彩的下篇,關註我們的公眾號可以第一時間看到哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、JDBC:JDBA是Java資料庫連接(Java DataBase Connectivity)技術的簡稱,提供連接各種常用資料庫的能力; ●Java是通過JDBC技術實現對各種資料庫訪問的, ●JDBA是Java資料庫連接(Java DataBase Connectivity)技術的簡稱,他充當 ...
  • 1. 什麼是函數 2. 函數的定義及調用 進群:125240963 即可獲取數十套PDF哦! 2.1 定義函數 函數定義規則如下: 2.2 函數中的參數 參數的作用 函數,把具有獨立功能的代碼塊組織成為一個小模塊,在需要的時候調用 函數的參數,增加函數的通用性,針對相同的數據處理邏輯,能夠適應更多的 ...
  • "DotNet菜園" 占個位置 ...
  • 0.使用說明 AliDDNSNet 是基於 .NET Core 開發的動態 DNS 解析工具,藉助於阿裡雲的 DNS API 來實現功能變數名稱與動態 IP 的綁定功能。 使用時請更改同目錄下的 為 文件,同時也可以顯示通過 參數來制定配置文件路徑。例如: 1.配置說明: 通過更改 / 的內容來實現 DDN ...
  • 本文沒啥技術含量,就是測試一下 MSSqlHelper 在 使用反射、不使用反射 的性能對比。 之後,不要問為什麼不用 ORM 這類的東西 —— 會有另外的文章 介紹 自己這些年 自己的ORM 升級歷史。 背景: 我自己有一個 MSSqlHelper, 這個 輔助類 是最基本的一個 資料庫操作類。 ...
  • mysql-connector-net-8.0.11.msi 可以從mysql官網下載 如果使用ado.net鏈接mysql資料庫則只需要引用 MySql.Data.dll即可,並不需要安裝mysql-connector-net驅動程式; 如果使用EF的話需要安裝mysql-connector-ne ...
  • OAuth2.0資料 初衷:一直想整理授權系列demo,讓自己項目高端大尚,列出新手授權系列,幫助小白程式員不用在為授權頭疼 OAuth 允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據。每一個令牌授權一個特定的網站(例如,視頻編輯網站)在特定的時段(例如,接下來的 2 ...
  • 寫在開始 上面一篇寫了一篇使用WebSocket做客戶端,然後服務端是socke代碼實現的。傳送門:webSocket和Socket實現聊天群發 本來我是打算寫到一章上的,畢竟實現的都是一樣的功能,後來想了想就沒寫在一起,主要是兩個方面, 一個原因是這是另一種實現服務方式,放在一起看著有點亂。單獨寫 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...