11 年前玩過用 ExpandoObject + dynamic 彈性處理屬性(參考:既然要動態就動個痛快 - ExpandoObject),6 年前見識到 DapperRow 靠實作 IDynamicMetaObjectProvider 憑空捏造物件行為供 dynamic 存取的奇妙手法。最近在讀 C# in Depth,重新認識 dynamic 原理,決定寫個自訂變形蟲物件用 dyanmic 玩一下。

這個變形蟲物件的規格構想如下:傳入一個 Dictionary<string, object> 指定預設屬性,建立彈性物件後指定成 dynamic 形別,透過 flexObject.PropName 可讀取值,未設定過的傳回 null;flexObject.ExisintPropName = XXX 更新屬性,flexObject.NewPropName = XXX 指定新屬性。

方法部分比較酷,可以 Say("Hello, World!") 印出 Hello, World!,SayHello() 印出 "Hello"、SayGoodbye() 印出 "Goodbye",Say 後面可以接任意文字當方法名稱,Say***() 會將 *** 忠實顯示出來,這就有點意思了。另外,為了方便程式列舉所有屬性,再實作了一個 string[] GetProperties 及索引子 object this[string propertyName]。

先看程式測試結果:

using System.Reflection;
using MyDynaObject;

//用 Dictionary<string, object> 傳入預設屬性建立 FlexDynaObject
//指定為動態型別
dynamic flexObject = new FlexDynaObject(new Dictionary<string, object>{
    ["Name"] = "Jeffrey"
});
//讀取Name屬性
Console.WriteLine($"Name={flexObject.Name}");
//讀取不存在的Score屬性
Console.WriteLine($"Score={flexObject.Score}");
//指定Score屬性值
flexObject.Score = 32767;
//讀到剛才設定的Score屬性值
Console.WriteLine($"Score={flexObject.Score}");
//呼叫Say方法,傳入顯示文字
flexObject.Say("Hello World!");
//名稱自訂的Say***方法,Say後方是要顯示的文字
flexObject.SayHello();
//再試一個
flexObject.SayGoodbye();

// 另建一個沒有預設屬性的物件
flexObject = new FlexDynaObject();
//隨便指定三個屬性
flexObject.Prop1 = "Prop1Value";
flexObject.Prop2 = "Prop2Value";
flexObject.Prop3 = "Prop3Value";
//用預設設計的GetProperties方法,取得所有屬性名稱
foreach (var propName in flexObject.GetProperties())
{
    //用屬性名稱當索引子參數,取得屬性值
    Console.WriteLine($"{propName}={flexObject[propName]}");
}
//呼叫沒有實作的方法,會拋出例外
flexObject.NoSuchMethod();

程式要怎麼寫呢?\FlexDynaObject 的原理是繼承 DynamicObject 型別,覆寫實作 TryGetMember()、TrySetMember() 模擬屬性讀寫、實作 TryGetIndex() 模擬索引子讀取 (flexObject["PropName"] 讀屬性值)、TryInvokeMember() 則偵測 InvokeMemberBinder.Name 實作 Say()、Say*()、GetProperties() 三個方法。基底類別 DynamicObject 跟 DapperRow 一樣有實作 IDynamicMetaObjectProvider,故可透過 dynamic 順利存取。程式不複雜,大家也可以玩看看,Have Fun!

using System.Dynamic;

namespace MyDynaObject;
public class FlexDynaObject : DynamicObject
{
    private readonly Dictionary<string, object> _props;

    public FlexDynaObject(Dictionary<string, object> props = null)
    {
        _props = props ?? new Dictionary<string, object>();
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = _props.ContainsKey(binder.Name) ? _props[binder.Name] : null;
        return true;
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        var propName = indexes[0].ToString();
        result = _props.ContainsKey(propName) ? _props[propName] : null;
        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _props[binder.Name] = value.ToString();
        return true;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        result = null!;
        if (binder.Name == "Say")
        {
            Console.WriteLine(args[0]);
            return true;
        }
        else if (binder.Name.StartsWith("Say"))
        {
            Console.WriteLine(binder.Name.Substring(3));
            return true;
        }
        else if (binder.Name == "GetProperties") 
        {
            result = _props.Keys;
            return true;
        }
        throw new NotImplementedException();
    }
}

A example implemented with DynamicObject to provide dyanmic propery access and dyamic name methods.


Comments

# by Ike

_props = props ?? new Dictionary<string, object>(); 寫了兩次,是誤寫嗎?

# by Jeffrey

to lke, 是的,謝謝提醒。

Post a comment