前陣子跟同事討論到LINQ查詢寫法。
記得在LINQ剛推出時以「可以在C#裡寫SQL語法查資料」為號召,範例裡常看到這種長得有點像SQL語法的LINQ查詢運算式(LINQ Query Expression,我習慣叫它「類SQL查詢語法」):
IEnumerable<int> scoreQuery = //query variable from score in scores //required where score > 80 // optional orderby score descending // optional select score; //must end with select or group |
不過,我在熟悉LINQ後(甚至演變成「少了LINQ幾乎不會寫程式」),卻很少再用這種寫法,大都是用LINQ方法配上Lambda運算式:
IEnumerable<int> scoreQuery = //query variable scores //required .Where(o => o.score > 80) // optional .OrderByDescending(o => o.score) // optional .Select(o => o.score); //must end with select or group |
那一種寫法比較好?好問題,甚至我也想不起來自己選擇第二種寫法的理由。既然聊到了,順手對二者做了粗淺的分析比較。
首先,有一個重要觀念:兩種寫法編譯出來的結果是相同的!
用個範例驗證:
class Run { publicint No; publicstring Name; public TimeSpan Time; publicbyte Road; public Run(int no, string name, TimeSpan time, byte road) { No = no; Name = name; Time = time; Road = road; } } class RoadType { publicbyte Id; publicstring Name; } static Run[] Runs = new Run[] { new Run(1, "櫻花馬", new TimeSpan(5,50,16), 1), new Run(2, "櫻木花道馬", new TimeSpan(5,27,50), 1), new Run(3, "國道馬", new TimeSpan(4,15,38), 2), new Run(4, "鳳梨馬", new TimeSpan(5,26,03), 1), new Run(5, "三重馬", new TimeSpan(4,47,32), 2), new Run(6, "台北星光馬", new TimeSpan(5,30,25), 2), new Run(7, "海山馬", new TimeSpan(5,42,18), 2), new Run(8, "石碇馬", new TimeSpan(6,07,54), 1), new Run(9, "五分山馬", new TimeSpan(5,13,32), 1), new Run(10, "土地公馬", new TimeSpan(5,26,51), 1), new Run(11, "觀音山馬", new TimeSpan(5,47,07), 1) }; static RoadType[] RoadTypes = new RoadType[] { new RoadType() { Id=1, Name="山路" }, new RoadType() { Id=2, Name="平地" } }; staticvoid Main(string[] args) { var queryBySql = from r in Runs where r.Time.Hours < 5 orderby r.Time select new { r.Name, r.Time }; var r1 = queryBySql.Skip(1).Single(); var queryByLambda = Runs .Where(o => o.Time.Hours < 5) .OrderBy(o => o.Time) .Select(o => new { o.Name, o.Time }); var r2 = queryByLambda.Skip(1).Single(); Console.WriteLine("Test {0} {1}", r1.Name, r2.Name); Console.Read(); } |
程式裡我分別用「類SQL查詢」及「方法串接」找出我去年馬拉松成績跑出Sub5的第二筆,使用JustDecompile反組譯可發現兩段程式碼除了變數名稱有別,程式結構一模一樣。(以類SQL查詢呈現是JustDecompile的偏好)
由此可知,不管你使用哪一種寫法,都不會產生效能或結果上的差異,大家可依自己的偏好選擇。
那麼,我為什麼偏好方法串接呢?認真想過,整理出我選擇方法串接的理由:
符合程式運作原理,複雜應用時較易思考
類SQL查詢提供熟悉SQL語法開發者另一種較親切的表達方式,背後運作其實是採方法串接的邏輯,先.Where(),再執行.OrderBy(),最後.Select(),每個方法都傳回IEnumerable<T>故可前後串接。在一些較複雜的動態邏輯串接情境,方法串接寫法就顯得較直覺,容易思考,例如動態依條件加上Where篩選:var q = Runs.ToList();
if (filterSub5)
q = q.Where(o => o.Time.Hours < 5).ToList();
if (filterMountain)
q = q.Where(o => o.Road == 1).ToList();
LINQ查詢未必與DB有關,對陣列、集合下SQL不如呼叫方法來得直覺
平日寫專案,LINQ我用得極多,但超過90%是用在一般的IEnumerable<T>與資料庫沒有任何關係。在這種情況下,不太會有動機刻意寫SQL,套用Where()、Select()得到處理過的IEnumerable<T>再繼續處理比較直覺。有些邏輯只能靠LINQ方法達成
類SQL查詢最終還是得串接如 Count()、Any()、SingleOrDefault() 等方法才能得到結果。使用方法串接可以一氣喝成,用類SQL查詢簡潔度差一些,例如:var countSub5BySql =
(from r in Runs where r.Time.Hours < 5 select r).Count();
var countSub5ByLambda =
Runs.Where(o => o.Time.Hours < 5).Count();
統一做法,減少腦力負擔
承上點,既然寫類SQL查詢免不了動用LINQ方法,乾脆只學只記一種做法,人老堪用記憶體漸少,少記點東西又避免切換做法消耗腦力,省力有效率。 (謎:你的大腦是有多不堪啦?)
但類SQL查詢有一強項可以狂電方法串接-其JOIN語法比.Join()方法簡潔直覺!如下例,差異很明顯吧?
var queryBySql = from r in Runs join t in RoadTypes on r.Road equals t.Id select new { r.Name, RoadType = t.Name }; var queryByLambda = Runs.Join( RoadTypes, r => r.Road, t => t.Id, (r, t) => new { r.Name, RoadType = t.Name }); Console.WriteLine(JsonConvert.SerializeObject(queryBySql.ToArray())); Console.WriteLine(JsonConvert.SerializeObject(queryByLambda.ToArray())); Console.Read(); |
我一直覺得類SQL查詢語法在LINQ技術的定位,有點像VB.NET在.NET技術扮演的角色,都是藉由開發者已經熟悉的語法(SQL、VB、VBScript)增加熟悉感與親和力,引領開發者進入全新世界。而在上手後,開發者依語法簡潔性、市場主流趨勢、自己的偏好,漸漸就會自動流向不同陣營,很有趣。
親愛的.NET開發者們,大家慣用哪一種LINQ寫法呢?