1、count和any
今天看了鶴沖天的文章:Linq:切勿使用 Count() > 0 來判斷集合非空 有所收獲,寫下文章總結一下:
先看如下代碼:
1 static void Main(string[] args) 2 { 3 Stopwatch watch = new Stopwatch(); 4 watch.Start(); 5 var nums = GetNums(0, 100000000); 6 SomeAction(nums); 7 watch.Stop(); 8 Console.WriteLine(watch.ElapsedMilliseconds); 9 Console.WriteLine("over"); 10 Console.ReadKey(); 11 } 12 public static IEnumerable<int> GetNums(int start, int count) 13 { 14 var end = start + count; 15 for (int i = start; i < end; i++) 16 yield return i; 17 } 18 public static void SomeAction<T>(IEnumerable<T> source) 19 { 20 //if (source.Count() > 0) //2233 毫秒 21 if (source.Any()) //3 毫秒 22 { 23 Console.WriteLine("have data"); 24 } 25 }
自己調試了下,當yield循環低於100000次的話,count()和Any()的執行時間差不多,count()時間略大於any()所耗費的時間,當大於100000次的話,count()執行時間就遠比Any()執行時間多很多!例如上面的程序執行結果,count耗時2233毫秒,any卻只耗時3毫秒!這是為什么呢?我們來看下原因:分別反編譯 Enumerate.Count 的源碼和 Enumerate.Any的源碼,如下:
Enumerate.Any 實現代碼如下:
1 public static bool Any<TSource>(this IEnumerable<TSource> source) 2 { 3 if (source == null) 4 { 5 throw Error.ArgumentNull("source"); 6 } 7 using (IEnumerator<TSource> enumerator = source.GetEnumerator()) 8 { 9 if (enumerator.MoveNext()) 10 { 11 return true; 12 } 13 } 14 return false; 15 }
Enumerate.Count 的源碼如下:
1 public static int Count<TSource>(this IEnumerable<TSource> source) 2 { 3 if (source == null) 4 { 5 throw Error.ArgumentNull("source"); 6 } 7 ICollection<TSource> is2 = source as ICollection<TSource>; 8 if (is2 != null) 9 { 10 return is2.Count; 11 } 12 ICollection is3 = source as ICollection; 13 if (is3 != null) 14 { 15 return is3.Count; 16 } 17 int num = 0; 18 using (IEnumerator<TSource> enumerator = source.GetEnumerator()) 19 { 20 while (enumerator.MoveNext()) 21 { 22 num++; 23 } 24 } 25 return num; 26 }
無論循環多少次,Any方法執行的時間總是最少的,大概10毫秒之內,我想大家從Any的源碼可以知道這個原因,Any方法先判斷集合是否為空,然后判斷集合是否有數據,只進行一次movenext(),並未進行循環查詢集合數量!所以耗費的時間當然一直是最少的了!
我們再看Count方法,同樣是先進行集合判空操作,然后判斷當前的集合source是否能轉成ICollection類型,如果能轉成,就直接返回此集合的Count屬性,不會在進行下面的循環獲取集合個數的操作了,為什么Count()方法比Any()方法執行的時間長呢?答案就在於此:因為GetNums方法中用yield返回純粹的IEnumerable<int>類型,但是ICollection<T>是繼承自IEnumerable<T>類型的,所以必然不好被轉換,也就是Count()源碼中的下面代碼不會執行
ICollection is3 = source as ICollection; if (is3 != null) { return is3.Count; }
上面的代碼不會執行,那么必然會進行下面的代碼操作,也就是循環獲取集合的個數:
int num = 0; using (IEnumerator<TSource> enumerator = source.GetEnumerator()) { while (enumerator.MoveNext()) { num++; } } return num;
這樣必然會導致Count()方法耗時很久!
2、如果我們把GetNums進行如下修改,其他代碼不動:
public static IEnumerable<int> GetNums(int start, int count) { List<int> list = new List<int>(); var end = start + count; for (int i = start; i < end; i++) list.Add(i); return list; }
這時我們在執行程序,可以看到無論循環次數多少,Count和Any兩者執行的時間差不多,大概是1315毫秒左右,其中1312毫秒用於list集合的裝填工作,3毫秒用於Count或Any的判斷時間,為什么會出現這種情況呢,我想通過上面大家都知道了,因為此時GetNums方法返回的是List<T>類型,此類型繼承於ICollection,所以必然可以被轉換,就會返回此類型的Count屬性,Count 方法對 ICollection 進行了優化,直接訪返回它的 Count 屬性,也就是返回一個數,當然很快了,下面的循環獲取集合的個數當然也就不會執行了,也就節約了時間。
3、對某人問題的思考
有人說:”我有個程序里經常要判集合里是否僅有一個元素,又不能用Count,不得已自己寫了個擴展方法 bool Check(this IEnumerable<T> source, int n), 當讀到第n+1個元素就直接返回false“
我想如果此人用的集合是繼承於ICollection<T>的話,例如List<T>,那么是沒必要對IEnumerable<T>進行擴展的,通過反編譯Count源碼可以知道,如果集合source可以轉換成ICollection的話,是可以直接通過count屬性獲取到集合的總數量的,所以耗費的時間要不了多少,也就沒必要對IEnumerable進行擴展了!反之,如果不是繼承ICollection<T>的話,例如IEnumerable,就必須自己擴展IEnumerable方法了,如果還用count的話,就會造成循環了,也就降低了程序的運行效率了!
4、參考
http://www.cnblogs.com/ldp615/archive/2011/12/11/2284154.html#comment_tip 鶴沖天
作者:MrZivChu
2013-12-13 21:30:12
