實體類的動態生成(三)

前言

在 .NET 中主要有兩種動態生成並編譯的方式,一種是經過 System.Linq.Expressions 命名空間中的 LambdaExpression 類的 CompileToMethod(...) 方法,可是這種方法只支持動態編譯到靜態方法,由於這個限制咱們只能放棄它而採用 Emitting 生成編譯方案,雖然 Emitting 方案強大可是實現起來麻煩很多,必需要手動處理底層 IL 的各類細節,腦補一些 C# 編譯器的實現機理,同時還要了解一些基本的 IL(Intermediate Language) 和 CLR(JVM) 執行方面的知識。git

基礎知識

由於要採用 Emitting 技術方案,必然須要瞭解 IL,若是你以前沒有怎麼接觸過,也不用灰心,網上有大量關於 IL 的入門文章,「30分鐘入門」仍是沒問題的哈,畢竟 IL 相對 8086/8088 彙編來講,真的平易近人太多了。github

首先你須要一個相似 ILSpy(http://ilspy.net) 這樣的工具來查看生成的IL或反編譯程序集(ILSpy 最新版本還提供了 IL 與對應 C# 的比照解釋,用戶體驗真是體貼得不要不要的)。算法

1、不一樣於 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)一個元素,而後將該元素保存到特定本地變量中(本示例是第一個本地變量)。注:本地變量必須由方法預先聲明。

2、基本上彙編語言或相似 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 之類的高級語言的邏輯運算中有「短路」的內置約定,因此在轉換成跳轉結構時,必須留意處理這個問題,不然會破壞語義並可能致使運行時錯誤。架構

3、由於 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;
            }
        }
    }
}

擴展屬性方法的定義約定:

  1. 必須是一個公共的靜態方法;
  2. 讀取方法名以 Get 打頭,後面接擴展屬性名並區分大小寫;
  3. 讀取方法的第一個參數必須是要擴展實體接口類型,第二個參數可選,若是有的話必須是擴展屬性的類型;返回類型必須是擴展屬性的類型;
  4. 設置方法名以 Set 打頭,後面接擴展屬性名並區分大小寫;
  5. 設置方法的第一個參數必須是要擴展實體接口類型,第二參數是擴展屬性的類型,表示設置的新值;返回類型必須是布爾類型,返回真(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),都會被動態生成器原樣添加到生成的實體類中。所以以前範例中,凡是接口以及接口的屬性聲明的各類自定義特性(包括:DefaultValueAttributeEntity.PropertyAttribute )都會被添加到動態生成的實體類的相應元素中,這對於某些應用是一個必須被支持的特性。

性能測試

《實體類的動態生成(二)》中,咱們已經驗證過設計方案的執行性能了,但結合上面介紹的功能特性細節,還需再提醒的是:由於開啓 DefaultValueAttribute 、擴展屬性方法、單例屬性、屬性變動通知都會致使生成的代碼與最基本字段訪問方式有所功能加強,對應要跑的代碼量增多,所以對跑分是有影響,但這種影響是肯定可知的,它們是 feature 所需並不是實現方案、算法缺陷所致,敬請知曉。

譬如圖二就是增長了屬性變動通知(即實體接口繼承了 INotifyPropertyChanged )致使的性能影響(Dynamic Entity 所在行)。

圖一

圖二

寫在最後的話

該實體類動態生成器簡單易用、運行性能和內存利用率都很是不錯(包括提供 IEntiy 接口的超讚功能),將會成爲從此咱們全部業務系統的基礎結構之一,因此後續的文章中(若是還有的話)應該會常常看到它的應用。

算下來花了整整三天時間(白天晚上都在寫)才完成《實體類的動態生成》系列文章,真心以爲寫文章比寫代碼還累,並且這仍是省略了應該配有的一些流程圖、架構圖的狀況下。計劃接下來我會爲 Zongsoft(https://github.com/Zongsoft) 系列開源項目撰寫該有的全部文檔,照此次這個寫法,心底不禁升起一絲莫名恐懼和淡淡憂傷來。

最後,由於寫這個東西耽擱了很多造 Zongsoft.Data 這個輪子的時間,因此接下來得全力去造輪子了。打算每週至少一篇乾貨滿滿的技術文章在公衆號首發,但願不會讓本身失望吧。

關於 Zongsoft.Data 它必定會是一款性能滿血、易用且足夠靈活的數據引擎,首發即會支持四大關係型數據庫,後續會加入對 Elasticsearch 的支持,總之,它應該是不一樣於市面上任何一款 ORM 數據引擎的開源產品。我會陸續與你們分享有關它的一些設計思考以及實現中遇到的問題,固然,也能夠在 github 上圍觀個人進展。

若是你以爲此次的文章對你有所幫助,又或者你以爲咱們的開源項目作的還不錯,請務必爲咱們點贊並關注咱們的公衆號,這或許是我堅持寫下去的最大動力來源了。

wechat

提示

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


知識共享許可協議

本做品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、從新發布,但必須保留本文的署名 鍾峯(包含連接:http://zongsoft.github.io),不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。若有任何疑問或受權方面的協商,請致信給我 (zongsoft@qq.com)。

相關文章
相關標籤/搜索