在.NET中System.Linq命名空間中有個擴展方法叫Count<T>()
,現在看下面的代碼示例:
class Program
{
static void Main(string[] args)
{
var userList = new List<int>();
userList.Add(1);
userList.Add(2);
userList.Add(3);
userList.Add(4);
userList.Add(5);
userList.Add(6);
Console.WriteLine(userList.Count);
Console.WriteLine(userList.Count());
var userList2 = userList.Select(i=>i);//返回的是IEnumerable<TResult>類型
Console.WriteLine(userList2.Count());
for (int i = 0; i < userList2.Count(); i++)
{
Console.WriteLine(userList[i]);
}
}
}
上門代碼通過調用了List的自身屬性Count,然後又調用了Linq支持的擴展方法Count(),結果都是輸出的6,可是它們背後有哪些不一樣呢?在上門的for循環中,背後真正走了多少次呢?
我們先開Count本身的屬性,這裏只粘貼了核心源碼,完整的地址自行可以查看http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,aeb6ba6c11713802
[ContractPublicPropertyName("Count")]
private int _size;
public int Count {
get {
Contract.Ensures(Contract.Result<int>() >= 0);
return _size;
}
}
public void Add(T item) {
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
我們通過上面可以看到,內部維護了一個_size字段來表示List中的數量,再向集合中添加元素時,已經進行統計過了。這時再取集合的數量時,時間複雜度就爲O(1)。
然後我們在看下擴展方法Count<T>
源代碼如下:
public static int Count<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
ICollection<TSource> collectionoft = source as ICollection<TSource>;
if (collectionoft != null) return collectionoft.Count;
ICollection collection = source as ICollection;
if (collection != null) return collection.Count;
int count = 0;
using (IEnumerator<TSource> e = source.GetEnumerator()) {
checked {
while (e.MoveNext()) count++;
}
}
return count;
}
我們通過上面可以看到傳入的集合元素如果不能轉換爲ICollection<T>
和ICollection 便會進行逐個元素進行遍歷,然後返回最終的元素集合,這樣的時間複雜度當然爲O(n)。所以能直接調用集合屬性獲取數量,就儘量避免使用擴展方法Count<T>
。
說到這裏,可能有的小夥伴問了,爲何List<T>
能不能轉換爲ICollection<T>
?
我們來
public class List<T> : IList<T>, ICollection<T>, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
public interface ICollection<T> : IEnumerable<T>, IEnumerable
這個ICollection<T>
繼承自IEnumerable<T>
如果傳入的List<T>
這種繼承自ICollection<T>
的集合,當然直接就是調用對應的Count 屬性,如果傳入的是直接繼承自IEnumerable<T>
的集合那麼循環遍歷獲取集合中的數量,因爲讓一個父類轉換爲一個子類,理所當然這裏一直就是null。
那麼在上面的for循環中遍歷,相當於每次遍歷都需要獲取一次集合中的數量,也就等於跑了6*6=36次,可見這是多麼浪費CPU。即使List<T>
能夠轉換成功轉換爲ICollection<T>
但是在CPU裏面也畢竟多了一次轉換操作,轉換後依舊是按照Count屬性獲取集合數量,還不如我們能直接獲取就直接獲取。
當然我們可以把for循環裏面的改成如下遍歷。如下代碼:
for (int i = 0,len=userList2.Count(); i < len; i++)
{
Console.WriteLine(userList2[i]);
}
或者直接使用foreach循環
foreach (var item in userList2)
{
Console.WriteLine(item);
}
亦或者避開Select
延遲查詢的特點(關於延遲查詢,下篇文章會進行講解),改成如下的方式:
var userList2 = userList.Select(i=>i).ToList();//返回的是List<TSource> ToList<TSource>
for (int i = 0; i < userList.Count(); i++)
{
Console.WriteLine(userList[i]);
}
通過上述源代碼我們可以進一步瞭解到,作爲一個公共方法肯定要滿足大多數的需要(可以參考我這篇文章:http://blog.csdn.net/u010533180/article/details/50627771),所以這裏使用了頂級的父類,就有了中間的兩層轉換操作,這也是基於性能方面考慮,比如傳遞過來的就是繼承了ICollection接口,這樣直接取出數量即可,不過大多數情況我們直接使用的是繼承自IEnumerable<T>
接口的類,所以我們作爲代碼的搬運工要積極思考微軟這套東西,持有懷疑性精神前進,這樣我們會瞭解到更多的東西。
最後我想說的是我們要通過提供的源代碼,積極優化我們書寫的代碼,哪怕一個稍微的轉換,對你本身代碼的性能就是一個很大的提升。