講解.NET 集合中使用Count屬性和擴展方法Count()區別

在.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> 接口的類,所以我們作爲代碼的搬運工要積極思考微軟這套東西,持有懷疑性精神前進,這樣我們會瞭解到更多的東西。

最後我想說的是我們要通過提供的源代碼,積極優化我們書寫的代碼,哪怕一個稍微的轉換,對你本身代碼的性能就是一個很大的提升。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章