1. 程式人生 > >比反射更快:委託 第1部分

比反射更快:委託 第1部分

目錄

為什麼不用反射?

委託一切

靜態屬性

獲取訪問器(get accessors)

設定訪問器(set accessors)

改進

屬性

改進

索引器

改進

Setters

靜態欄位

獲取靜態欄位值

設定靜態欄位值

欄位

獲取欄位值委託

設定欄位值委託

改進

總結


為什麼不用反射?如果你和它一起工作。

GitHubNuget包提供了所有三篇文章的程式碼,包括新功能和錯誤修復。

為什麼不用反射?

如果您使用.NETC#(也可能是其他語言),在某些時候,您將不得不編寫更高階和通用的程式碼,即讀取型別的所有屬性,搜尋屬性等。或者您可能想要呼叫一些私有方法或屬性?使用框架或庫的某種內部機制並不罕見。通常,第一個解決方案是反射。但問題是:反射很慢。當然,如果您需要很少使用

Reflection並且它不是應用程式的主要程式碼,那就足夠了。如果在某些時候更頻繁地使用它,您可能會嘗試對其進行優化。現在是有趣的部分:如何做到這一點?

Reflection的主要問題是它很慢。它比直接呼叫型別成員要慢得多。如果你通過Reflection做到這一點,一切都會變慢。如果需要建立型別的例項,可能會使用Activator.CreateInstance方法。它比反射快一點,但也比直接呼叫建構函式慢。對於屬性,方法和建構函式,可以使用表示式(表示式靜態方法),但表示式和編譯它們甚至比反射慢。

當我在本文中尋找一種快速建立型別例項的方法,我在Stack Overflow上找到了

這個問題。根據它,建立例項的最快方法是使用通過編譯表示式建立的委託。這讓我想到了通過委託獲取例項和型別成員的一般機制。畢竟,它最近是我的興趣點,因為我使用PCL專案處理Xamarin應用程式,這些專案具有可移植的.NET Framework,其功能少於完整版。返回不可用的型別成員的快速機制將非常好,並且會以犧牲平臺程式碼為代價(這是一件好事)使PCL程式碼更大。因為您可以從ReflectionExpressionlambdas建立委託,並且只需要一點開銷(因為您已經呼叫了另一個方法),因此建立用於獲取型別(publicprivate)所有成員的委託的機制是一個好主意。這是本文的主題。 

 下面的程式碼是使用.NET 4.5Visual Studio 2015中編寫的,但是可以將大多數程式碼移植到舊版本。 

委託一切

型別(Type)可以包含以下型別的成員:

  1. 靜態的
    1. 屬性
    2. 方法
    3. 欄位
    4. 事件
    5. 常量欄位
  2. 例項
    1. 屬性
    2. 方法
    3. 欄位
    4. 建構函式
    5. 事件
    6. 索引

我們可以建立委託來檢索所有這些成員。坦率地說,並非所有委託都有意義。例如,委託常量。這些欄位不會改變,因此沒有必要多次檢索它們。您可以使用Reflection輕鬆檢索這些值,並將其儲存在某處以備將來使用。

本文不會涉及的唯一內容是靜態事件。你有沒有用過?說真的,如果絕對需要的話,我想不出一個案例。更常見的是建立普通事件並從單例例項呼叫它。好處是,只需對例項事件進行少量更改,就可以非常輕鬆地為靜態事件建立委託。 

靜態屬性

屬性和方法是最常見的成員。方法要複雜得多,所以最好從屬性開始。

我認為反射最常見的情況是從例項或型別中檢索集合或單個屬性。它可能用於動態表檢視,序列化,反序列化,儲存到檔案等。靜態屬性不那麼複雜,因此我們將從它開始而不是從例項開始。

獲取訪問器(get accessors)

我們應該建立一個新專案。它將被稱為委託——控制檯應用程式,以便於測試。

首先要做的是為測試建立類。我們可以將它命名為TestClass

public class TestClass
{
    public static string StaticPublicProperty { get; set; } = "StaticPublicProperty";

    internal static string StaticInternalProperty { get; set; } = "StaticInternalProperty";

    protected static string StaticProtectedProperty { get; set; } = "StaticProtectedProperty";

    private static string StaticPrivateProperty { get; set; } = "StaticPrivateProperty";
}

第二件事是為委託建立一個類——DelegatesFactory。我們如何檢索靜態屬性?最好的方法是通過Type例項檢索它,它表示有關我們的源型別的資訊。我們可以使它成為Type型別的擴充套件方法。這樣,它會更方便。我們需要知道我們想要的屬性,所以帶有屬性名稱的引數和帶有返回型別的型別引數也是一個好主意。

Type.StaticPropertyGet<string>("StaticPublicProperty");

另一種方法是將源型別和屬性型別設定為型別引數。這樣,我們將使用靜態方法而不是擴充套件方法。此外,由於我們在編譯時通常沒有型別名稱(因為它在執行前不公開或不可用),因此它的可用性較低。

DelegateFactory.StaticPropertyGet<TestClass, string>("StaticPublicProperty");

有了這個假設,我們可以編寫控制檯應用程式作為DelegateFactory測試。

class ConsoleApplication
{
    private static readonly Type Type = typeof(TestClass);

    static void Main(string[] args)
    {
        var sp = DelegateFactory.StaticPropertyGet<TestClass, string>("StaticPublicProperty");
        var sp1 = Type.StaticPropertyGet<string>("StaticPublicProperty");
    }
}

現在,我們可以編寫兩種方法的存根。

public static class DelegateFactory
{
    public static Func<TProperty> StaticPropertyGet<TSource, TProperty>(string propertyName)
    {
        return null;
    }

    public static Func<TProperty> StaticPropertyGet<TProperty>(this Type source, string propertyName)
    {
        return null;
    }
}

由於第二種方法(擴充套件的一個方法)更為通用,我們將首先實現它。要建立委託,我們需要PropertyInfo來獲取所請求的屬性。是的,我們需要反射。問題是,許多委託需要反射,但這只是一次,用於建立委託。之後,我們可以使用這個委託只需很小的開銷,因為程式必須執行兩個巢狀方法而不是一個。

通過反射檢索屬性最重要的部分是我們可以通過這種方式訪問PropertyInfo.GetMethod,而GetMethodMethodInfo型別,它將具有CreateDelegate成員。用於建立委託以檢索靜態公共屬性的最簡單程式碼如下所示。

public static Func<TProperty> StaticPropertyGet<TProperty>(this Type source, string propertyName)
{
    var propertyInfo = source.GetProperty(propertyName, BindingFlags.Static);
    return (Func<TProperty>)propertyInfo.GetMethod.CreateDelegate(typeof(Func<TProperty>));
}

看起來很簡單,這就是為什麼這是第一個例子。CreateDelegate方法需要建立委託型別。對於屬性檢索,Func只有一個引數,它是屬性的型別,因此建立的委託將沒有任何引數。只是簡單的無引數方法,返回靜態屬性的值。

接下來是處理非公共(internal, protected private)屬性。我們可以通過在GetProperty方法中更改設定 BindingFlags來實現。對於私人和受保護的成員,我們只需要一個BindingFlags.NonPublic。對於內部,這有點複雜(在我看來,更令人困惑)並且需要標誌PublicNonPublic。理解這一點的一個簡單方法是將內部結構視為一種公共的,但不是完全公共的(程式集內部可訪問的)。

public static Func<TProperty> StaticPropertyGet<TProperty>(this Type source, string propertyName)
{
    var propertyInfo = source.GetProperty(propertyName, BindingFlags.Static);
    if (propertyInfo == null)
    {
        propertyInfo = source.GetProperty(propertyName, BindingFlags.Static | BindingFlags.NonPublic);
    }
    if (propertyInfo == null)
    {
        propertyInfo = source.GetProperty(propertyName,
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
    }
    return (Func<TProperty>)propertyInfo.GetMethod.CreateDelegate(typeof(Func<TProperty>));
}

上面的程式碼很容易理解。如果屬性不是公開的,則會嘗試將其設定為私有或受保護。如果情況不是這樣,它會嘗試內部的。現在,我們可以使用型別引數編寫用於過載的方法體。

public static Func<TProperty> StaticPropertyGet<TSource, TProperty>(string propertyName)
{
    return typeof(TSource).StaticPropertyGet<TProperty>(propertyName);
}

現在我們可以測試兩種方法。我們應該在控制檯應用程式中為TestClass所有靜態屬性建立委託,並使用它們來檢索屬性值,以測試一切是否正常。

class ConsoleApplication
{
    private static readonly Type Type = typeof(TestClass);

    static void Main(string[] args)
    {
        var sp = DelegateFactory.StaticPropertyGet<TestClass, string>("StaticPublicProperty");
        var sp1 = Type.StaticPropertyGet<string>("StaticPublicProperty");
        var sp2 = Type.StaticPropertyGet<string>("StaticInternalProperty");
        var sp3 = Type.StaticPropertyGet<string>("StaticProtectedProperty");
        var sp4 = Type.StaticPropertyGet<string>("StaticPrivateProperty");

        Console.WriteLine("Static public property value: {0}",sp1());
        Console.WriteLine("Static internal property value: {0}", sp2());
        Console.WriteLine("Static protected property value: {0}", sp3());
        Console.WriteLine("Static private property value: {0}", sp4());
    }
}

執行此示例後,我們將在控制檯中看到結果。

Static public property value: StaticPublicProperty
Static internal property value: StaticInternalProperty
Static protected property value: StaticProtectedProperty
Static private property value: StaticPrivateProperty

靜態屬性工作得很好。

我們可以稍微檢查一下這些委託的效能。首先,我們需要為控制檯應用程式型別新增兩個新欄位。

private static Stopwatch _stopWatch;
private static double _delay = 1e8;

其次是為測試編寫迴圈。

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    var test = TestClass.StaticPublicProperty;
}
_stopWatch.Stop();
Console.WriteLine("Static Public property: {0}", _stopWatch.ElapsedMilliseconds);

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    var test = sp1();
}
_stopWatch.Stop();
Console.WriteLine("Static Public property retriever: {0}", _stopWatch.ElapsedMilliseconds);

第一個for迴圈以普通方式檢索靜態屬性。第二個使用委託。執行更改後的程式碼後,我們可以發現差異並不大,真的。

Static Public property: 404
Static Public property retriever: 472

檢索屬性值一億次需要大約400ms,使用委託的速度要慢不到20%。進行更多測試,您可以檢查實際效能開銷在15-35%之間變化。當然,它只是一個簡單的字串。對於首先計算其返回值的更復雜的屬性,差異將更小。僅僅為了實驗,我們可以新增一個帶反射的迴圈。

_stopWatch = new Stopwatch();
var pi0 = Type.GetProperty("StaticPublicProperty", BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public).GetMethod;
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    var test = pi0.Invoke(null, null);
}
_stopWatch.Stop();
Console.WriteLine("Static Public property via reflection: {0}", _stopWatch.ElapsedMilliseconds);

執行更改後的程式碼後,測試結果應與此類似:

Static Public property: 423
Static Public property retriever: 535
Static Public property via reflection: 13261

如您所見,反射速度要慢很多倍。訪問靜態屬性的時間不到半秒,通過反射執行相同操作大約需要13秒!

即使使用反射,只有一次建立委託比使用它一直訪問該屬性更有效。

要全面瞭解事物,讓我們再做一次測試。如果您曾經使用過Expression型別及其方法,那麼您就知道它可以用於反射等類似的東西。當然,可以通過這種方式為靜態屬性建立委託。

public static Func<TProperty> StaticPropertyGet2<TProperty>(this Type source, string propertyName)
{
    var te = Expression.Lambda(Expression.Property(null, source, propertyName));
    return (Func<TProperty>)te.Compile();
}

這段程式碼比帶反射的程式碼簡單得多。它為TestClass.StaticPublicProperty建立一個lambda函式,看起來像這樣。 

() => TestClass.StaticPublicProperty

現在,我們可以使用反射和表示式來測試委託建立的效能。

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < 10000; i++)
{
    Type.StaticPropertyGet<string>("StaticPublicProperty");
}
_stopWatch.Stop();
Console.WriteLine("Static public field creator via reflection: {0}", _stopWatch.ElapsedMilliseconds);

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < 10000; i++)
{
    Type.StaticPropertyGet2<string>("StaticPublicProperty");
}
_stopWatch.Stop();
Console.WriteLine("Static public field creator via expression: {0}", _stopWatch.ElapsedMilliseconds);

執行此示例後,我們將獲得兩個迴圈的結果。

Static public field creator via reflection: 23
Static public field creator via expression: 543

我們可以看到,表示式比反射慢得多。我沒有進一步調查,但我認為它與編譯表示式委託有關。不幸的是,不可能僅僅通過使用反射為所有成員建立委託,因此有必要為其中一些使用表示式,但我們將盡力不這樣做。

當我們不知道屬性的型別時,仍然存在這種情況。該怎麼辦?由於StaticPropertyGet都需要實際的屬性型別,因此我們無法使用它們。在這種情況下,我們需要一個沒有這種型別的過載,但是要使用什麼替代呢?委託應該返回什麼型別?只有一個選項:object.NET中的所有型別都可以轉換為物件。

Func<object> spg = Type.StaticPropertyGet("StaticPublicProperty");

返回的委託應該是一個不接受引數並返回object型別值的方法。如何實現這個新的StaticPropertyGet?我們需要編碼來檢索PropertyInfo以獲取所需的靜態屬性。最好的方法是使用StaticPropertyGet<TProperty>擴充套件方法中的程式碼在DelegateFactory建立一個新方法。

private static PropertyInfo GetStaticPropertyInfo(Type source, string propertyName)
{
    var propertyInfo = (source.GetProperty(propertyName, BindingFlags.Static) ??
                               source.GetProperty(propertyName, BindingFlags.Static | BindingFlags.NonPublic)) ??
                               source.GetProperty(propertyName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
    return propertyInfo;
}

主要問題是有時我們想要在編譯時檢索或設定未知型別的值的屬性。為此,我們需要一個方法來建立委託,該委託檢索物件型別值而不是屬性型別值。我們可以通過使用表示式來實現,因為我們不能只使用CreateDelegate方法(它需要相容的委託簽名)。此機制的核心是Expression.Lambda方法,它返回LambdaExpression型別物件,以及返回Delegate型別的LambdaExpression.Compile方法。有了這兩個,我們幾乎可以建立任何涉及型別成員訪問的程式碼。

public static Func<object> StaticPropertyGet(this Type source, string propertyName)
{
    var propertyInfo = GetStaticPropertyInfo(source, propertyName);
    return (Func<object>)Expression.Lambda(Expression.Call(propertyInfo.GetMethod)).Compile();
}

Expression.Lambda使用正確的方法簽名建立lambda Expression.Call用於標記lambda的主體,應該呼叫屬性getter——畢竟,屬性getter實際上是get_ {property name}方法。

我們可以使用以下程式碼測試它是否正常工作。

var spg = Type.StaticPropertyGet("StaticPublicProperty");
Console.WriteLine(spg());

執行上面的行將寫入'StaticPublicProperty'的值,這是具有此名稱的property的值。 

設定訪問(set accessors)

現在,當我們有辦法獲取靜態屬性的值時,我們可以編寫用於檢索設定(set)訪問器的委託的方法。該機制是非常類似於獲取訪問器的,但不是使用Func<TProperty>,而是使用Action<TProperty>。當然,PropertyInfo.SetMethod建立委託而不是GetMethod

public static Action<TProperty> StaticPropertySet<TSource, TProperty>(string propertyName)
{
    return typeof(TSource).StaticPropertySet<TProperty>(propertyName);
}
public static Action StaticPropertySet(this Type source, string propertyName)
{
    var propertyInfo = GetStaticPropertyInfo(source, propertyName);
    return (Action)propertyInfo.SetMethod.CreateDelegate(typeof(Action));
}

和以前一樣,對於獲取(get)訪問器,有兩種方法只使用反射:一種是擴充套件方法,另一種是型別引數作為源型別。

對於不需要事先知道屬性型別的過載,我們必須像以前一樣使用StaticPropertyGet表示式。

public static Action<object> StaticPropertySet(this Type source, string propertyName)
{
    var propertyInfo = GetStaticPropertyInfo(source, propertyName);
    var valueParam = Expression.Parameter(typeof(object));
    var convertedValue = Expression.Convert(valueParam, propertyInfo.PropertyType);
    return (Action<object>)Expression.Lambda(Expression.Call(propertyInfo.SetMethod, convertedValue), valueParam).Compile();
}

這個表示式有點複雜。因為它應該返回返回void並且有一個引數的方法,我們需要將此引數新增到Lambda呼叫——因此是valueParam變數。如果您想知道為什麼它不能嵌入到Expression.Lambda,則必須將相同的引數表示式傳遞給ConvertLambda方法——通過引用比較表示式。在傳遞給屬性setter方法之前,必須將變數valueParam轉換為正確的型別。如果沒有,表示式將是有效的並且將無法建立。轉換後,新表示式可以作為引數安全地傳遞給Call方法。valueParam變數中未轉換的表示式作為引數傳遞給Lambda呼叫,要標記,建立的lambda將具有物件型別的單個引數。

完全以相同的方式呼叫新方法。

var sps = DelegateFactory.StaticPropertySet<TestClass, string>("StaticPublicProperty");
var sps1 = Type.StaticPropertySet<string>("StaticPublicProperty");
var sps2 = Type.StaticPropertySet<string>("StaticInternalProperty");
var sps3 = Type.StaticPropertySet<string>("StaticProtectedProperty");
var sps4 = Type.StaticPropertySet<string>("StaticPrivateProperty");<br />var spg5 = Type.StaticPropertyGet("StaticPublicProperty");

在控制檯應用程式中,我們可以建立如下的測試迴圈。

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    TestInstance.PublicProperty = "ordinal way";
}
_stopWatch.Stop();
Console.WriteLine("Public set property: {0}", _stopWatch.ElapsedMilliseconds);

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    sps1("test");
}
_stopWatch.Stop();
Console.WriteLine("Public set property retriever: {0}", _stopWatch.ElapsedMilliseconds);

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    sps2("test");
}
_stopWatch.Stop();
Console.WriteLine("Internal set property retriever: {0}", _stopWatch.ElapsedMilliseconds);

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    sps3("test");
}
_stopWatch.Stop();
Console.WriteLine("Protected set property retriever: {0}", _stopWatch.ElapsedMilliseconds);

_stopWatch = new Stopwatch();
_stopWatch.Start();
for (var i = 0; i < _delay; i++)
{
    sps4("test");
}
_stopWatch.Stop();
Console.WriteLine("Private set property retriever: {0}", _stopWatch.ElapsedMilliseconds);

Console.WriteLine("Static public property value is {0}", sp1());
Console.WriteLine("Static internal property value is {0}", sp2());
Console.WriteLine("Static protected property value is {0}", sp3());
Console.WriteLine("Static private property value is {0}", sp4());

執行上面的程式碼後,我們將看到結果,其值與下面的值類似。

Public set property: 483
Public set property retriever: 542
Internal set property retriever: 496
Protected set property retriever: 497
Private set property retriever: 542
Static public property value is test
Static internal property value is test
Static protected property value is test
Static private property value is test

正如您所看到的,以這種方式訪問​​setter只是稍慢一些,類似於通過委託訪問getter 

改進

什麼可以做得更好?當然,上面的程式碼不是很安全,因為當它可以丟擲空引用異常時至少有兩個點。首先,當找不到屬性並且propertyInfo變數為null時,第二個是當獲取(get)或設定(set) 訪問器不可用時(並非每個屬性都有,對吧?)。這兩種情況都很容易解決。

我們應該首先編寫屬性來測試第一種情況。

public static string StaticOnlyGetProperty => _staticOnlyGetOrSetPropertyValue;

public static string StaticOnlySetProperty
{
    set { _staticOnlyGetOrSetPropertyValue = value; }
}
private static string _staticOnlyGetOrSetPropertyValue = "StaticOnlyGetOrSetPropertyValue";

現在我們可以修復這兩種方法。

public static Func<TProperty> StaticPropertyGet<TProperty>(this Type source, string propertyName)
{
    var propertyInfo = GetStaticPropertyInfo(source, propertyName);
    return (Func<TProperty>)propertyInfo?.GetMethod?.CreateDelegate(typeof(Func<TProperty>));
}

public static Action<TProperty> StaticPropertySet<TProperty>(this Type source, string propertyName)
{
    var propertyInfo = GetStaticPropertyInfo(source, propertyName);
    return (Action<TProperty>)propertyInfo?.SetMethod?.CreateDelegate(typeof(Action<TProperty>));
}

如您所見,兩種方法中的簡單空檢查就足夠了,但無論如何我們都應該對其進行測試。在控制檯應用程式中跟隨呼叫就足夠了。 

所有呼叫都返回null,這很好。我們想要null而不是錯誤。 

屬性

通過覆蓋靜態屬性,我們現在可以將焦點切換到例項屬性。委託只有一個引數不同。由於例項屬性需要例項來獲取這些屬性的值,因此委託將需要具有這些例項的引數。因此,我們將使用Func<TSource,TProperty>委託代替Func<TProperty>。其餘程式碼幾乎相同。至於靜態屬性,每個訪問器有三種方法。首先,靜態方法帶有源型別引數和屬性型別引數,擴充套件方法帶有型別引數與屬性型別相同,方法沒有型別引數。但首先我們需要一個類似於GetStaticPropertyInfo的方法。

private static PropertyInfo GetPropertyInfo(Type source, string propertyName)
{
    var propertyInfo = source.GetProperty(propertyName) ??
                               source.GetProperty(propertyName, BindingFlags.NonPublic) ??
                               source.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    return propertyInfo;
}

如您所見,它也非常相似。

我們將從每個訪問器的第一種方法開始。

public static Func<TSource, TProperty> PropertyGet<TSource, TProperty>(string propertyName)
{
    var source = typeof(TSource);
    var propertyInfo = GetPropertyInfo(source, propertyName);
    return (Func<TSource, TProperty>)propertyInfo?.GetMethod?.CreateDelegate(typeof(Func<TSource, TProperty>));
}

public static Action<TSource, TProperty> PropertySet<TSource, TProperty>(string propertyName)
{
    var source = typeof(TSource);
    var propertyInfo = GetPropertyInfo(source, propertyName);
    return (Action<TSource, TProperty>)propertyInfo?.SetMethod?.CreateDelegate(typeof(Action<TSource, TProperty>));
}

如您所見,有一個空傳播運算子(?。)用於屬性資訊和所需的訪問器方法的空檢查。

好。要測試它,我們需要在控制檯應用程式中的TestClass中建立例項屬性。

public class TestClass
{
    public TestClass()
    {
        PublicProperty = "PublicProperty";
        InternalProperty = "InternalProperty";
        ProtectedProperty = "ProtectedProperty";
        PrivateProperty = "PrivateProperty";
    }

    public string PublicProperty { get; set; }

    internal string InternalProperty { get; set; }

    protected string ProtectedProperty { get; set; }

    private string PrivateProperty { get; set; }

}

和控制檯應用程式中的TestClass型別靜態只讀例項,以用於涉及例項的所有測試。

private static readonly TestClass TestInstance = new TestClass();

測試程式碼如下所示。

var ps1 = DelegateFactory.PropertySet<TestClass, string>("PublicProperty");
var ps2 = DelegateFactory.PropertySet<TestClass, string>("InternalProperty");
var ps3 = DelegateFactory.PropertySet<TestClass, string>("ProtectedProperty");
var ps4 = DelegateFactory.PropertySet<TestClass, string>("PrivateProperty");
            
Console.WriteLine("Public property value is {0}", pg1(TestInstance));
Console.WriteLine("Internal property value is {0}", pg2(TestInstance));
Console.WriteLine("Protected property value is {0}", pg3(TestInstance));
Console.WriteLine("Private property value is {0}", pg4(TestInstance));

ps1(TestInstance, "test");
ps2(TestInstance, "test");
ps3(TestInstance, "test");
ps4(TestInstance, "test");

Console.WriteLine("Public property value is {0}", pg1(TestInstance));
Console.WriteLine("Internal property value is {0}", pg2(TestInstance));
Console.WriteLine("Protected property value is {0}", pg3(TestInstance));
Console.WriteLine("Private property value is {0}", pg4(TestInstance));

執行此測試程式碼後,我們將看到這樣的控制檯輸出。

Public property value is PublicProperty
Internal property value is InternalProperty
Protected property value is ProtectedProperty
Private property value is PrivateProperty
Public property value is test
Internal property value is test
Protected property value is test
Private property value is test

前四行是get委託返回的值,與TestClass例項的預設值一致。以下四行是在呼叫設定委託並具有新的測試值之後。好。一切正常!

改進

上述用於檢索和設定例項屬性的方法的主要問題是那些需要在編譯時知道型別,這有時是不可能的,因為某些型別是私有的。但是,如果您沒有型別,則無法建立具有此型別例項作為引數的委託。對此唯一的解決方案是建立一個接受物件的方法,並希望它是正確型別的正確例項。該解決方案的問題是您不能將物件型別的引數作為源例項傳遞給屬性getter。首先必須將其強制轉換為適當的型別。因此,在此委託方法中,您還可以轉換為適當的型別,而不僅僅是一個委託方法,它可以檢索或設定屬性。這是我們必須在靜態屬性中使用表示式的地方。

第二個問題與靜態屬性相同。您不必在編譯時知道屬性型別。它可能是不可用的也可能是私有的。

首先,我們的新過載將如下所示:

Type.PropertyGet<string>("PublicProperty");
Type.PropertyGet("PublicProperty");

很簡單。在第一種情況下,我們知道屬性的型別,在第二種情況下——我們不知道。首先返回字串,第二個返回相同的字串,但作為物件型別。

然後我們需要為PropertyGet方法新增兩個新的過載方法。讓我們從具有已知屬性型別的那個開始,因為它只是一點點,但是稍微簡單一些。

使用GetPropertyInfo方法返回的屬性資訊,我們可以使用Expression類建立適當的委託。用於通過其例項作為物件訪問屬性getter的程式碼如下所示。

var sourceObjectParam = Expression.Parameter(typeof(object));<br />var @delegate = (Func<object, object>)Expression.Lambda(Expression.Call(Expression.Convert(sourceObjectParam, source), propertyInfo.GetMethod), sourceObjectParam).Compile();

機制很簡單。首先,我們需要使用物件型別建立ParameterExpression。這是必要的,因為它被引用兩次:在Expression.Lambda方法中作為目標lambda引數,在Expression.Convert方法中作為轉換的目標。之後,我們使用帶有方法呼叫(Expression.Call方法)的主體建立lambda表示式(Expression.Lambda方法),使用lambda物件引數的轉換結果(Expression.Convert方法)呼叫引數呼叫屬性getter 。它可能看起來很複雜(甚至更多,因為Expression API在我看來有點混亂;注意Lambda方法需要方法的第一個主體,然後引數,但Call方法首先接受一個呼叫的主體然後呼叫方法),但實際上它不是。編譯後,表示式將大致相當於lambda,如下所示。

Func<object,string> @delegate = o => ((TestClass)o).PublicProperty;

看起來更清楚吧? 

有了這些知識,我們就可以建立第一種方法。

public static Func<object, TProperty> PropertyGet<TProperty>(this Type source, string propertyName)
{
    var propertyInfo = GetPropertyInfo(source, propertyName);
    var sourceObjectParam = Expression.Parameter(typeof(object));
    return (Func<object, TProperty>)Expression.Lambda(Expression.Call(Expression.Convert(sourceObjectParam, source), propertyInfo.GetMethod), sourceObjectParam).Compile();
}

以同樣的方式我們可以建立第二個方法,但具有不同的返回型別宣告。標記這一點非常重要,即使用C編寫的帶有返回型別物件的方法不需要在返回語句中強制轉換為物件(因為任何型別都可以隱式轉換為物件,而不需要額外的強制轉換),這在表示式中也不起作用。據我所知,我的測試僅適用於參考型別。對於intDateTime等值型別,我們需要轉換為objectPropertyInfo類具有IsClass屬性,其允許檢查是否需要轉換。

public static Func<object, object> PropertyGet(this Type source, string propertyName)
{
    var propertyInfo = GetPropertyInfo(source, propertyName);
    var sourceObjectParam = Expression.Parameter(typeof(object));
    Expression returnExpression = 
        Expression.Call(Expression.Convert(sourceObjectParam, source), propertyInfo.GetMethod);
    if (!propertyInfo.PropertyType.IsClass)
    {
        returnExpression = Expression.Convert(returnExpression, typeof(object));
    }
    return (Func<object, object>)Expression.Lambda(returnExpression, sourceObjectParam).Compile();
}

如您所見,第二種方法更通用,幾乎與第一種方法相同。因此,我們可以安全地重寫第一種方法,如下所示。

public static Func<object, TProperty> PropertyGet<TProperty>(this Type source, string propertyName)
{
    return source.PropertyGet(propertyName) as Func<object, TProperty>;
}

在向TestClass新增新屬性後使用int型別,PublicPropertyInt以及控制檯應用程式的以下行,我們可以測試這些方法是否有效。

var pgo1 = Type.PropertyGet<string>("PublicProperty");
var pgo2 = Type.PropertyGet("PublicProperty");
var pgo3 = Type.PropertyGet("PublicPropertyInt");
Console.WriteLine("Public property by object and property type {0}", pgo1(TestInstance));
Console.WriteLine("Public property by objects {0}", pgo2(TestInstance));
Console.WriteLine("Public property by objects and with return value type {0}", pgo3(TestInstance));

它將導致控制檯輸出中的以下行。

Public property by object and property type PublicProperty
Public property by objects PublicProperty
Public property by objects and with return value type 0

現在,我們可以為setter編寫類似的方法。核心區別在於正確的委託將是Action而不是Func,因此它將有兩個:例項和值來設定,而不是一個引數。在具有已知引數型別的方法中,我們只有更改委託,但在沒有型別引數的方法中,我們必須轉換例項和值。我們將從第二個開始。首先,我們必須為新屬性值建立ExpressionParameter

var propertyValueParam = Expression.Parameter(propertyInfo.PropertyType);

有了這個,我們可以改變lambda的返回值(如果我們改變類似的PropertyGet方法)。

return (Action<object, object>)Expression.Lambda(Expression.Call(<br />     Expression.Convert(sourceObjectParam, source), <br />     propertyInfo.SetMethod, <br />     Expression.Convert(propertyValueParam, propertyInfo.PropertyType)), sourceObjectParam, propertyValueParam).Compile();

如您所見,它建立了具有兩個引數的Action委託——sourceObjectParampropertyValueParam——兩者都首先轉換為正確的型別。以上行可以轉換為以下lambda

Action<object, object> @delegate = (i, v)=>((TestClass)i).PublicProperty = (string)v;

具有已知屬性型別的PropertySet過載比上述方法更具體,但同時,它將具有兩種型別的知識:值引數和屬性型別。這就是我們可以將其程式碼用於兩個過載的原因。只要屬性型別正確,就不需要轉換。

兩者的最終版本看起來像這樣。

public static Action<object, TProperty> PropertySet<TProperty>(this Type source, string propertyName)
{
    var propertyInfo = GetPropertyInfo(source, propertyName);
    var sourceObjectParam = Expression.Parameter(typeof(object));
    ParameterExpression propertyValueParam;
    Expression valueExpression;
    if (propertyInfo.PropertyType == typeof(TProperty))
    {
        propertyValueParam = Expression.Parameter(propertyInfo.PropertyType);
        valueExpression = propertyValueParam;
    }
    else
    {
        propertyValueParam = Expression.Parameter(typeof(TProperty));
        valueExpression = Expression.Convert(propertyValueParam, propertyInfo.PropertyType);
    }
    return (Action<object, TProperty>)Expression.Lambda(Expression.Call(Expression.Convert(sourceObjectParam, source), propertyInfo.SetMethod, valueExpression), sourceObjectParam, propertyValueParam).Compile();
}

public static Action<object, object> PropertySet(this Type source, string propertyName)
{
    return source.PropertySet<object>(propertyName);
}

應該修復的最後一件事是對GetMethodSetMethod進行空檢查——畢竟它已經在前面的例子中完成了。這在PropertySetPropertyGet方法中需要單個if 

var propertyInfo = GetPropertyInfo(source, propertyName);
if (propertyInfo?.GetMethod == null)
{
    return null;
}

var propertyInfo = GetPropertyInfo(source, propertyName);
if (propertyInfo?.SetMethod == null)
{
    return null;
}

如果任何屬性或訪問器不存在(在所有屬性不總是同時具有setget訪問器之後)方法將返回null而不是委託。 

索引器

索引器只是特殊型別的例項屬性。考慮它們的最佳方式是使用額外的索引引數作為特殊型別的getset訪問器。它們實際上是名為“Item”的特殊屬性。我們應該從在測試類中建立索引器開始。

private readonly List<int> _indexerBackend = new List<int>(10);

public int this[int i]
{
    get
    {
        return _indexerBackend[i];
    }
    set { _indexerBackend[i] = value; }
}

是的,沒有索引檢查,也沒有自動收集大小調整等。它只是測試程式碼,所以它不需要是安全的。

這是非常簡單的情況,因為這樣的委託也將是非常複雜的。我們需要這個索引器有Func<TestInstance,int,int>。使用單個呼叫建立它的最簡單方法如下所示。

var ig1 = DelegateFactory.IndexerGet<TestClass, int, int>();

如何實現IndexerGet方法?最重要的是為索引器獲取PropertyInfo物件。

private const string Item = "Item";<br /><br />private static PropertyInfo GetIndexerPropertyInfo(Type source, Type returnType, Type[] indexesTypes)
{
    var propertyInfo = (source.GetProperty(Item, returnType, indexesTypes) ??
                        source.GetProperty(Item, BindingFlags.NonPublic, null, 
                            returnType, indexesTypes, null)) ?? 
                        source.GetProperty(Item, 
                            BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, 
                            null, returnType, indexesTypes, null);
    return propertyInfo;
}

對於公共索引器,獲取PropertyInfo的方式與屬性幾乎相同——當然,需要兩個新引數來指示所需索引器的返回和索引型別。對於非公共索引器,事情變得有點複雜,因為我們必須使用更一般的過載。當然,對GetProperty方法的所有呼叫都使用常量值“Item”作為屬性的名稱。使用屬性資訊,我們可以為索引器建立委託,其索引引數與普通屬性的委託非常相似。

public static Func<TSource, TIndex, TReturn> IndexerGet<TSource, TReturn, TIndex>()
{
    var propertyInfo = GetIndexerPropertyInfo(typeof(TSource), typeof(TReturn), new[] { typeof(TIndex) });
    return (Func<TSource, TIndex, TReturn>)
            propertyInfo?.GetMethod?.CreateDelegate(typeof(Func<TSource, TIndex, TReturn>));
}

正如您所看到的,主要區別在於,不是隻有一個帶有例項的引數(比如在屬性中),還有一個帶有索引引數的額外引數。除此之外它幾乎是一樣的。

好的,但是如果索引器有多個索引呢?兩三個怎麼樣?它也是可能的,因為IndexerGet方法採用型別引數,我們不能建立一個更通用的方法,返回2,3,4或更多引數的不同委託。對於這些型別的索引器中的每一種,需要具有不同型別引數集的其他方法。由於索引器很少使用,主要用於某種索引操作,因此我們將僅為最多3個索引引數(最多三維陣列)建立方法。

public static Func<TSource, TIndex, TIndex2, TReturn> IndexerGet<TSource, TReturn, TIndex, TIndex2>()
{
    var propertyInfo = GetIndexerPropertyInfo(typeof(TSource), typeof(TReturn), new[] { typeof(TIndex), typeof(TIndex2) });
    return (Func<TSource, TIndex, TIndex2, TReturn>)
            propertyInfo?.GetMethod?.CreateDelegate(typeof(Func<TSource, TIndex, TIndex2, TReturn>));
}

public static Func<TSource, TIndex, TIndex2, TIndex2, TReturn> IndexerGet<TSource, TReturn, TIndex, TIndex2, TIndex3>()
{
    var propertyInfo = GetIndexerPropertyInfo(typeof(TSource), typeof(TReturn), new[] { typeof(TIndex), typeof(TIndex2), typeof(TIndex3) });
    return (Func<TSource, TIndex, TIndex2, TIndex2, TReturn>)
            propertyInfo?.GetMethod?.CreateDelegate(typeof(Func<TSource, TIndex, TIndex2, TIndex2, TReturn>));
}

上面的方法將為兩個和三個索引生成委託,並且被稱為非常類似於一維陣列索引器過載。當然有可能編寫方法來返回更多索引的委託,最多16個——這是Func類的限制。

var ig1 = DelegateFactory.IndexerGet<TestClass, int, int, int>();
var ig2 = DelegateFactory.IndexerGet<TestClass, int, int, int, int>();

您可能已經注意到,索引器getter的所有上述方法只有在我們知道類的型別時才能建立委託。如果我們不這樣做(因為它是私有的或者在編譯時不知道它),我們需要一個採用Type引數的方法。我們可以使用與之前屬性相同的擴充套件方法約定。

var ig1 = Type.IndexerGet<int, int>();
var ig2 = Type.IndexerGet<int, int, int>();
var ig3 = Type.IndexerGet<int, int, int, int>();

索引器與之前的屬性一樣出現同樣的問題。由於我們確實傳遞了例項而不是型別本身,因此我們無法建立具有已知型別引數的委託——它需要是物件。因此,我們再次需要表示式。我們將從具有單個索引引數的索引器開始。

public static Func<object, TIndex, TReturn> IndexerGet<TReturn, TIndex>(this Type source)
{
    var indexType = typeof(TIndex);
    var returnType = typeof(TReturn);
    var propertyInfo = GetIndexerPropertyInfo(source, returnType, new[] { indexType });
    var sourceObjectParam = Expression.Parameter(typeof(object));
    var paramExpression = Expression.Parameter(indexType);
    return (Func<object, TIndex, TReturn>)Expression.Lambda(
        Expression.Call(Expression.Convert(sourceObjectParam, source), propertyInfo.GetMethod, paramExpression), sourceObjectParam, paramExpression).Compile();
}

它與PropertyGet方法非常相似,它建立了將物件作為源例項的委託。差異在於索引引數。因為它是一個物件,所以首先需要轉換。在建立了帶有例項物件和索引引數的lambda表示式之後,它將被編譯並作為委託返回。它可以表示為以下lambda語句。

Func<object, int, int> @delegate = (o, i) => ((TestClass)o)[i];

好。兩三個索引怎麼樣?幸運的是,Expression.LambdaExpression.Call方法具有帶有未指定數量的引數的過載。有了它,我們可以輕鬆地重寫大多數用於建立委託的程式碼,更多的是通用版本。我們應該編寫一個方法,通過從索引器返回型別和索引型別編譯表示式來建立Delegate例項。

public static Delegate DelegateIndexerGet(Type source, Type returnType, params Type[] indexTypes)
{
    var propertyInfo = GetIndexerPropertyInfo(source, returnType, indexTypes);
    var sourceObjectParam = Expression.Parameter(typeof(object));
    var paramsExpression = new ParameterExpression[indexTypes.Length];
    for (var i = 0; i < indexTypes.Length; i++)
    {
        var indexType = indexTypes[i];
        paramsExpression[i] = Expression.Parameter(indexType);
    }
    return Expression.Lambda(
                Expression.Call(Expression.Convert(sourceObjectParam, source), propertyInfo.GetMethod, paramsExpression),
                new[] { sourceObjectParam }.Concat(paramsExpression)).Compile();
}

最重要的變化是此方法採用未指定數量的索引型別。第二個變化是,LambdaCall的引數集合不是已知的引數集合(在lambda中還有一個引數——源例項引數)。除此之外,它與以前的程式碼相同,使用這種新方法,我們可以實現上面的樹擴充套件方法。

public static Func<object, TIndex, TReturn> IndexerGet<TReturn, TIndex>(this Type source)
{
    var indexType = typeof(TIndex);
    return (Func<object, TIndex, TReturn>)DelegateIndexerGet(source, typeof(TReturn), indexType);
}
        
public static Func<object, TIndex, TIndex2, TReturn> IndexerGet<TReturn, TIndex, TIndex2>(this Type source)
{
    var indexType = typeof(TIndex);
    var indexType2 = typeof(TIndex2);
    return (Func<object, TIndex, TIndex2, TReturn>)DelegateIndexerGet(source, typeof(TReturn), indexType, indexType2);
}

public static Func<object, TIndex, TIndex2, TIndex3, TReturn> IndexerGet<TReturn, TIndex, TIndex2, TIndex3>(this Type source)
{
    var indexType = typeof(TIndex);
    var indexType2 = typeof(TIndex2);
    var indexType3 = typeof(TIndex3);
    return (Func<object, TIndex, TIndex2, TIndex3, TReturn>)DelegateIndexerGet(source, typeof(TReturn), indexType, indexType2, indexType3);
}

這是簡單的程式碼。型別引數更改為Type類的例項並傳遞給DelegateIndexerGet方法。返回值將轉換為預期的Func類。但是這些新方法仍然缺乏一些功能。索引器不需要具有原始索引型別——它們可以是任何型別。即使很少出現這種情況,我們也必須有一種方法來建立更多索引引數的委託。但更重要的是索引器的返回型別——它可以是任何型別,它更常見。那我們需要一個更通用的方法。對於初學者,我們可以為只有一個引數的索引器建立委託Func<object,object,object> 的方法。

public static Func<object, object, object> IndexerGet(this Type source, Type returnType, Type indexType)
{
    var propertyInfo = GetIndexerPropertyInfo(source, returnType, new[] { indexType });
    var sourceObjectParam = Expression.Parameter(typeof(object));
    var indexObjectParam = Expression.Parameter(typeof(object));
    Expression returnExpression = Expression.Call(Expression.Convert(sourceObjectParam, source),
        propertyInfo.GetMethod, Expression.Convert(indexObjectParam, indexType));
    if (!propertyInfo.PropertyType.IsClass)
    {
        returnExpression = Expression.Convert(returnExpression, typeof(object));
    }
    return (Func<object, object, object>)
        Expression.Lambda(returnExpression, sourceObjectParam, indexObjectParam).Compile();
}

它與PropertyGet過載幾乎相同,它可以處理物件而不是具體型別——它只需要一個引數。

理想情況下,必須有一種方法以統一的方式獲取所有索引器型別。例如,可以編寫一個帶有兩個引數的委託:例項和索引陣列並返回物件。

var ig = Type.IndexerGet(typeof(int), typeof(int), typeof(int), typeof(int));
var t = ig(TestInstance, new object[] { 0, 0, 0 });

這種方法的問題在於它需要更多的計算,包括讀取索引陣列,將它們轉換為正確的型別,呼叫索引器getter並將其值轉換為object。因為所有這一切,它將比呼叫getter慢得多,但如果我們沒有更嚴格的委託(如Func<TSource, TIndex, TReturn>),那麼這是我們能做的最好的事情。

public static Func&l