利用ExpandoObject類別,在執行期動態加入和移除成員

在.NET Framework 4.0後出現的System.Dynamic,讓C#漸漸邁向語法鬆散化的不歸路,這次要練習的是ExpandoObject類別,有了ExpandoObject,我們再也不用羨慕JavaScript宣告一個物件的寫法,也不必再受限於匿名型別(Anonymous Types)只能夠承載資料型別(Data Types)或物件,但是沒有辦法承載方法(Methods)。因此,ExpandoObject類別在某些資料結構比較複雜的應用下,會讓你的程式碼變得更精簡!

一段JavaScript的Expando Property會長的像這樣,語法夠鬆散了吧!

var obj = { Prop1: '' };
obj.Prop1 = 'foo';  //Prop1 is NOT an Expando Property
obj.Prop2 = 'bar';  //Prop2 is an Expando Property

不過在這邊也要先幫匿名型別(Anonymous Types)平反一下,當你選擇使用ExpandoObject後,因為所有的驗證(錯誤)都是在執行期運行了,所以VisualStudio引以為豪的IntelliSense瞬間變的毫無用武之地,也就是說你的觀念要變得很好,你才會知道自己正在寫什麼。反觀Anonymous Types,你在定義它完成後,VisualStudio的IntelliSense可是運作的非常良好呢!

開始ExpandoObject的初體驗

ExpandoObject的語法超簡單的,如果你說看不懂我也沒辦法了。要注意的是,一開始的變數前面要宣告dynamic,這表示叫編譯器不要檢查,一切到Runtime時期再說。此外,ExpandoObject是不可以被Instance的。此外,不要試圖去呼叫一個不存在的成員,不然你會在Runtime時期得到Exception。

static void Main(string[] args)
{
  //注意這邊一定要用dynamic跟編譯器描述這是一個動態型別(不要幫我檢查),前面用「var」或是「System.Dynamic.ExpandoObject」都不可以喔!
  dynamic oEmployee = new System.Dynamic.ExpandoObject();
  oEmployee.cName = "John";
  
  //不可以被實例化
  //dynamic oPeople = new oEmployee();

  //不可以亂呼叫不存在的屬性,會觸發Exception
  //Console.WriteLine(oEmployee.cTel);

  Console.WriteLine(oEmployee.cName);
  Console.Read();
}

來個複雜一點的資料型別

接著我們試著把某一些屬性,加載一些較複雜的類別物件List(),觀察看看是否可以正確的運行。其中的Class Motos我用偷懶的使用巢狀類别(Nested Type)。在驗證輸出,我使用Newtonsoft.Json.JsonConvert.SerializeObject來幫輸出成JSON,因為我想要觀察ExpandoObject在複雜的操作情況下,是否可以被正確的序列化成字串?

class Program
{
  //Nested Type
  class Motos
  {
    public string cID { get; set; }
    public string cType { get; set; }
  }
  //Main
  static void Main(string[] args)
  {
    dynamic oEmployee = new System.Dynamic.ExpandoObject();
    oEmployee.cName = "John";
    oEmployee.oMotos = new System.Collections.Generic.List<Motos>();
    oEmployee.oMotos.Add(new Motos() { cID = "AB-1234", cType = "汽車" });
    oEmployee.oMotos.Add(new Motos() { cID = "CD-5678", cType = "機車" });
    //吐出JSON
    Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(oEmployee));
    Console.Read();
  }
}

結果是可以的!真好!

{
  "cName":"John",
  "oMotos":[
    {
      "cID":"AB-1234",
      "cType":"汽車"
    },
    {
      "cID":"CD-5678",
      "cType":"機車"
    }
  ]
}

開始加入方法(Methods)

接下來要做一些匿名型別Anonymous Types辦不到的事情了,開始加入可以被操作的方法成員!當然是採用我們喜愛的System.Action來進行委派啦!

static void Main(string[] args)
{
  dynamic oEmployee = new System.Dynamic.ExpandoObject();
  oEmployee.cName = "John";
  oEmployee.iSalary = 22000;
  oEmployee.AddSalary = (System.Action<int>)((iMoney) => { oEmployee.iSalary += iMoney; });
  //幫John加薪
  oEmployee.AddSalary(1000);
  Console.WriteLine(string.Format("{0}現在的薪資是{1}元。", oEmployee.cName, oEmployee.iSalary));
  Console.Read();
}

很明顯的,John進入巨匠後薪資從22K升級到23K。

John現在的薪資是23000元。

方法成員不可以被序列化

在上面的例子中,我們使用了System.Action來實作方法成員,但如果我們這個物件最終,與前前例子一樣,在AddSalary()被操作後我們想要直接丟成JSON,這可行嗎?答案是否定的,因為方法不可能被序列化成資料啊!這時候我們就要請出System.Dynamic.ExpandoObject的原型,其實它就是一個實作System.Collections.Generic.IDictionary<TKey, TValue>的類別。

static void Main(string[] args)
{
  dynamic oEmployee = new System.Dynamic.ExpandoObject();
  oEmployee.cName = "John";
  oEmployee.iSalary = 22000;
  oEmployee.AddSalary = (System.Action<int>)((iMoney) => { oEmployee.iSalary += iMoney; });
  //幫John加薪
  oEmployee.AddSalary(1000);
  //轉型回IDictionary<TKey, TValue>
  var oTemp = (System.Collections.Generic.IDictionary<string, object>)oEmployee;
  //移除不可能被序列化的成員
  oTemp.Remove("AddSalary");
  Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(oTemp));
  Console.Read();
}

這下John的23K薪資可以被大剌剌的公告在網路上了!

{
  "cName":"John",
  "iSalary":23000
}

相關連結

C# System.Dynamic.ExpandoObject AnonymousTypes JavaScript ExpandoProperty