微軟在.NET 3.5中加入了LINQ技術,作為配套改進了C#語言,加入了Lambda表達式,擴展方法,匿名類型等新特性用以支持LINQ。微軟同時提出了要使用聲明式編程,即描述計算規則,而不是描述計算過程。
使用LINQ技術能很好地做到聲明式編程,寫出的代碼表意能力強,可讀性高,避免了以往或其他語言的代碼中充斥大量表意不明的for循環甚至多層循環的問題。不要小看for循環和Where,Select,OrderBy等擴展方法的區別,可以不通過注釋一眼就能看出代碼意圖真的很重要。當看到Java代碼中一大堆的for循環,包括多層循環,又沒有注釋,必須仔細看才能了解代碼作用時,真的很頭大。個人認為LINQ是C#語言區別於其他語言的最顯著的特性,也是最大的優勢之一。
當然現在大多數主流語言都加入了Lambda表達式,從而可以使用類似於LINQ的技術,達到聲明式編程。比如Java語言在Java 8中加入了和C#幾乎一樣的Lambda表達式語法,並加入了Stream API,以達到類似於LINQ的用法。
如此可見,聲明式編程是發展趨勢,既然使用C#,就要多用LINQ,用好LINQ,用對LINQ。不要再寫一堆一堆的for循環了!
要用好LINQ,就要學好LINQ,理解其原理,機制和用法。推薦一個學習和研究LINQ的好工具LINQPad,下面是官網和官網上的截圖。
http://www.linqpad.net/
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LinqResearch { class Program { static void Main(string[] args) { var list = new List<int> { 2, 1, 6, 4, 3, 5, 7, 8, 10, 9 }; Console.WriteLine("list"); var list1 = list.Select(i => { Console.WriteLine("In select {0}", i); return i * i; }); Console.WriteLine("list1"); var list2 = list1.Where(i => { Console.WriteLine("In where>10 {0}", i); return i > 10; }); Console.WriteLine("list2"); var list3 = list2.Where(i => { Console.WriteLine("In where<60 {0}", i); return i < 60; }); Console.WriteLine("list3"); var list4 = list3.OrderBy(i => { Console.WriteLine("In orderby {0}", i); return i; }); Console.WriteLine("list4"); var list5 = list4.ToList(); Console.WriteLine("list5"); foreach (var i in list5) { Console.WriteLine(i); } } } }
先不要看下面的運行結果,想想打印出的是什麼,然後再看結果,看看和想的一樣嗎?
list list1 list2 list3 list4 In select 2 In where>10 4 In select 1 In where>10 1 In select 6 In where>10 36 In where<60 36 In select 4 In where>10 16 In where<60 16 In select 3 In where>10 9 In select 5 In where>10 25 In where<60 25 In select 7 In where>10 49 In where<60 49 In select 8 In where>10 64 In where<60 64 In select 10 In where>10 100 In where<60 100 In select 9 In where>10 81 In where<60 81 In orderby 36 In orderby 16 In orderby 25 In orderby 49 list5 16 25 36 49
為什麼先打印出list 到 list4,而沒有進到Lambda裡面?
這是因為LINQ是延時計算的,即只有foreach或ToList時才去做真正計算,前面的Select,Where等語句只是聲明了計算規則,而不進行計算。
這點很重要,如果不明白這點,就會寫出有BUG的代碼,如下面的程序,打印出的是1和2,而不是1。
var a = 2; var list = new List<int> { 1, 2, 3 }; var list1 = list.Where(i => i < a); a = 3; foreach (var i in list1) { Console.WriteLine(i); }
後面打印出的為什麼先是select和where交錯,然後是orderby,而不是先select再where,最後orderby?
這時因為Select,Where等這些擴展方法,在聲明計算規則時是有優化的(內部可能通過表達式樹等方法實現),它並不是傻傻的按照原始定義的規則,順序執行,而是以一種優化的方法計算並獲得結果。所以使用 LINQ一般會比自己寫的原始的一大堆for循環性能還高,除非花大量時間優化自己的邏輯(一般不會有這個時間)。
可以看到針對元素2和1,並沒有打印出In where<60 的行,這說明針對這兩個元素,第二個Where裡的代碼並沒有執行,因為第一個Where都沒有通過。在進行完投影(Select)和篩選(Where)後,最後進行排序(OrderBy),只針對篩選後留下的元素執行OrderBy裡面的計算邏輯,一點也不浪費。
上面的程序有人可能會寫成這樣。
var list = new List<int> { 2, 1, 6, 4, 3, 5, 7, 8, 10, 9 }; Console.WriteLine("list"); var list1 = list.Select(i => { Console.WriteLine("In select {0}", i); return i * i; }).ToList(); Console.WriteLine("list1"); var list2 = list1.Where(i => { Console.WriteLine("In where>10 {0}", i); return i > 10; }).ToList(); Console.WriteLine("list2"); var list3 = list2.Where(i => { Console.WriteLine("In where<60 {0}", i); return i < 60; }).ToList(); Console.WriteLine("list3"); var list4 = list3.OrderBy(i => { Console.WriteLine("In orderby {0}", i); return i; }).ToList(); Console.WriteLine("list4"); var list5 = list4.ToList(); Console.WriteLine("list5"); foreach (var i in list5) { Console.WriteLine(i); }
這樣寫打印出的結果為,
list In select 2 In select 1 In select 6 In select 4 In select 3 In select 5 In select 7 In select 8 In select 10 In select 9 list1 In where>10 4 In where>10 1 In where>10 36 In where>10 16 In where>10 9 In where>10 25 In where>10 49 In where>10 64 In where>10 100 In where>10 81 list2 In where<60 36 In where<60 16 In where<60 25 In where<60 49 In where<60 64 In where<60 100 In where<60 81 list3 In orderby 36 In orderby 16 In orderby 25 In orderby 49 list4 list5 16 25 36 49
雖然也能得到正確的結果,但是卻是不合理的。因為這樣寫每步都執行計算,並放到集合中,會有很大的性能損耗,失去了使用LINQ的優勢。
何時進行真正計算是個值得思考的問題,多了會增加中間集合的數量,性能不好,少了有可能會有多次重復計算,性能也不好。下文會有說明。
如果使用Resharper插件,會提示出重復迭代(可能會有多次重復計算)的地方,這個功能很好,便於大家分析是否存在問題。
使用Max和Min要小心,Max和Min等聚合運算需要集合中存在值,否則會拋出異常,筆者多次遇到這個問題產生的BUG。
當前面有Where篩選時,後面使用Max或Min不一定是安全的,如下面的代碼會拋出異常。
var a = 0; var list = new List<int> { 1, 2, 3 }; var min = list.Where(i => i < a).Min(); Console.WriteLine(min);
如果a來源於外部值,又有大段的邏輯,這樣的BUG不易發現。
解決方法有多種,我們來分析一下,一種方法是可以先調一下Any,再使用Min,代碼如下,
var a = 0; var list = new List<int> { 1, 2, 3 }; var list2 = list.Where(i => i < a); var min = 0; if (list2.Any()) { min = list2.Min(); } Console.WriteLine(min);
把代碼改為如下,
var a = 3; var list = new List<int> { 1, 2, 3 }; var list2 = list.Where(i => { Console.WriteLine("In where {0}", i); return i < a; }); var min = 0; if (list2.Any(i => { Console.WriteLine("In any {0}", i); return true; })) { min = list2.Min(); } Console.WriteLine(min);
打印結果為,
In where 1 In any 1 In where 1 In where 2 In where 3 1
這樣做有可能對性能影響不大,也有可能較大,取決於where(或前面的其他邏輯)中邏輯的多少和集合中前面不滿足where條件的元素的數量。因為Any確定有就不會繼續執行,但仍有部分重復計算發生。
第二種方法的代碼如下,
var a = 3; var list = new List<int> { 1, 2, 3 }; var list2 = list.Where(i => i < a).ToList(); var min = 0; if (list2.Any()) { min = list2.Min(); } Console.WriteLine(min);
這種方法不會有重復計算的開銷,但會有數據導入集合的開銷,和第一種比較哪種性能更高值得考慮。
第三種方法的代碼如下,
var a = 0; var list = new List<int> { 1, 2, 3 }; var list2 = list.Where(i => i < a); var min = 0; try { min = list2.Min(); } catch (Exception) { } Console.WriteLine(min);
直接吃掉異常,數據量大時,前面過濾條件計算復雜時,可能這種方法性能最高。
總之,C#開發者,學好LINQ,用好LINQ,你會發現真的很爽的!