研讀 C# in Depth 之餘想到的點子:需要傳入欄位名稱當參數的場合,用 Lambda Expression o => o.PropName 取代名稱字串。

直接用範例展示。

假設我有個 Player 物件陣列:

public class Player 
{
    public string Name { get; set; }
    public DateTime RegDate { get; set; }
    public byte Level { get; set; }
    public int Score { get; set; }
    public Player(string name, DateTime regDate, byte level, int score)
    {
        Name = name;
        RegDate = regDate;
        Level = level;
        Score = score;
    }
}

static Player[] Players = new[] 
{
    new Player("Jeffrey", new DateTime(2000,1,1),  1,  255),
    new Player("darkthread", new DateTime(2012, 12, 21), 2, 32767),
    new Player("GM", new DateTime(1900, 1, 1), 99, 65535)
};

如果我想寫一個將資料轉成三欄 CSV 的函式(方便舉例罷了,實務上不會有這種詭異規格),至於三個欄位要放什麼屬性(Name、RegDate、Level 或 Score),由呼叫端決定。

最直覺且不需技巧的方法是寫個依欄位名稱字回傳不同屬性的小函式,像是這樣:

static void GenCsv(IEnumerable<Player> list, string col1, string col2, string col3) 
{
    Func<Player, string, object> getPropValue = (p, n) => {
        switch (n) 
        {
            case "Name":
                return p.Name;
            case "RegDate":
                return p.RegDate.ToString("yyyy-MM-dd");
            case "Level":
                return p.Level;
            case "Score":
                return p.Score;
            default:
                throw new NotImplementedException();
        }
    };
    foreach (var p in list) 
        Console.WriteLine($"{getPropValue(p, col1)},{getPropValue(p, col2)},{getPropValue(p, col3)}");
    Console.WriteLine();        
}

void Main()
{
    GenCsv(Players, "Name","Score","RegDate");
    GenCsv(Players, "Name","Level","Score");
}

但這是已知物件型別是 Player 的前題下才能用 switch 寫死,如果規格改成 GenCsv<T>(IEnumerable<T> list, ...) 怎麼辦?如果你會寫 Refelection,還是可以過關:

static void GenCsv<T>(IEnumerable<T> list, string col1, string col2, string col3) 
{
    var t = typeof(T);
    var col1Prop = t.GetProperty(col1, BindingFlags.Instance | BindingFlags.Public);
    var col2Prop = t.GetProperty(col2, BindingFlags.Instance | BindingFlags.Public);
    var col3Prop = t.GetProperty(col3, BindingFlags.Instance | BindingFlags.Public);
    foreach (var p in list) 
        Console.WriteLine($"{col1Prop.GetValue(p)},{col2Prop.GetValue(p)},{col3Prop.GetValue(p)}");
    Console.WriteLine();    
}

void Main()
{
    GenCsv<Player>(Players, "Name","Score","RegDate");
    GenCsv<Player>(Players, "Name","Level","Score");
}

但有幾個問題:1) 沒法控制輸出格式,前面 switch 寫法我可以 RegDate.ToString("yyyy-MM-dd") 決定格式,用 PropertyInfo.GetValue() 只能原汁輸出 2) 動用 Reflection,勢必得犧牲一些效能 3) 跟最開始傳欄名稱的寫法一樣,沒有強型別保護,欄位名稱寫錯要執行期間才會被發現。(第三點倒是可以靠 nameof() 補救,C# 技巧:用列舉及 nameof 取代字串常數提高可維護性)

如果能仿效 LINQ .OrderBy(o => o.UnitPrz * o.Qty) 用 Lambda 表示式取代欄位名稱字串當參數,既保有強型別又能加入自訂邏輯,豈不美哉?

想完,程式也寫完了:

static void GenCsv<T>(IEnumerable<T> list, Func<T, object> col1, 
						Func<T, object> col2, Func<T, object> col3) 
{
    foreach (var p in list)
        Console.WriteLine($"{col1(p)},{col2(p)},{col3(p)}");
    Console.WriteLine();
}

void Main()
{   
    GenCsv<Player>(Players, p => p.Name, p => p.Score, p => p.RegDate.ToString("yyyy-MM-dd"));
    // 其實可以不用寫 GenCsv<Player>( ...),
    // Compiler 會自己由 IEnumerable<Player> Players 推斷 T 是 Player
    GenCsv(Players, p => p.Name, p => p.Level, p => p.Score);
}

如此既能兼顧強型別,又可自訂輸出結果,是 Lambda Expression 在 LINQ 之外的又一應用。

For the functions property name parameters, we can use Lambda expressions instead of property name string to keep strong-typed and flexible on result customization.


Comments

# by S.

黑大是否考慮利用anonymous達成不限輸出欄位數量會更實用? 例如: GenCsv<Player>( Players, x => new { x.Name, x.Level } );

# by Jeffrey

to S. ,如果要不限欄位數量,我應該會設成 params Func<T, object>[] cols,用 for 迴圈輸出,比列舉匿名型別各屬性簡單一些。(或是你有好點子不用 Reflection列出匿名型別的所有屬性?)

Post a comment