1

Lambda Expression 應用 - 用強型別動態指定欄位名稱

 1 year ago
source link: https://blog.darkthread.net/blog/lambda-exp-as-prop-param/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Lambda Expression 應用

研讀 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");
}

Fig1_638029506275476559.png

但這是已知物件型別是 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");
}

Fig2_638029506277497267.png

但有幾個問題: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);
}

Fig3_638029506279410071.png

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

  • Posted in
  • C#

and has 2 comments

Comments

Post a comment

Comment
Name Captcha 22 + 0 =

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK