C# List<T>的使用

C# List<T>的使用

List是在日常开发中使用异常频繁的一个集合类。List是一个线性的容器,也就是存在List中的数据必须是连续的,一个挨着一个。那么就会有一些疑问,为什么List是线性的呢?创建的时候应该要一个多大的容器呢?太大了肯定是浪费空间,太小了装不下怎么办?集合中的单个元素又占有多大的内存空间呢?

①这个容器到底该设置为多大呢?

看一个例子。假设我们需要一个宠物容器。

 

class Program {

        private static List<Pet> pets;

        internal class Pet
        {

        }

        static void Main(string[] args)
        {
            pets = new List<Pet>();
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);
            while (true) ;
        }

    }

}

pic1

因为还没有添加数据,所以此时的容量和树木都是0.现在我们向其中添加一个元素

        static void Main(string[] args)
        {
            pets = new List<Pet>(0);
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);

            Console.WriteLine("添加一个元素后:");
            Pet pet = new Pet();
            pets.Add(pet);
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);


            while (true) ;
        }

这里就说明List容器的容量只有在对其进行操作之后才会被设置,而且有个默认值的规则。实际上List提供了设置容器容量的构造方法,就是你可以设置这个容器的初始大小。之所以设置默认值为4,实在长期的实践过程中统计出设置这个默认值会得到最优解。

②再来看看第二个问题,每个单位元素所占的内存空间是多大?

        internal class Pet
        {

        }

        static void Main(string[] args)
        {
            pets = new List<Pet>(0);
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);

            for (int i = 0; i < 4; i++)
            {
                Pet pet = new Pet();
                pets.Add(pet);
            }

            pets.ForEach(PrintAddress);
            while (true) ;
        }

        /// <summary>
        /// 打印对象在内存中的位置
        /// </summary>
        /// <param name="obj"></param>
        private static void PrintAddress(object obj)
        {
            GCHandle hander = GCHandle.Alloc(obj);
            var pin = GCHandle.ToIntPtr(hander);
            Console.WriteLine(pin);
        }

添加了一个打印内存的方法,并且多添加了几个数据,以便通过元素地址的差值来看出每个元素在内存中所占的内存大小(这里注意Pet是一个空的类型,什么数据都没有)。打印结果如下:

可以看出,每个元素所占的内存大小为8个字节。因为是容器,我们容易臆想我们的数据在容器中是一个叠一个的,就想可比克那样,那么现在单位元素(一片薯片)是因为pet类是空的原因吗,

那我们在Pet上添加一些数据。修改代码如下

        internal class Pet
        {
            public string id;
            public string name;
            public Type type;
            public int age;
        }

然后我们再次执行修改后的代码,发现每个单位元素的所占的地址空间还是8个字节。所以List中的数据并不是和可比克那样叠加保存的。实际上是每个八位字节只保存了每个单位元素在堆内存中的地址,因为通过new出来的对象都是保存在堆内存中的,因为你无法保证每个单位元素实际需要多少内存空间。而在堆内存里的每个对象就像在池塘里的小鱼一样,但是有一根线连着这一条条小鱼,线的另一端就是List中对应的地址。

③现在的容量是4,如果装满了之后我们再向其中添加元素怎么办?

        static void Main(string[] args)
        {
            pets = new List<Pet>(0);
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);
            Console.WriteLine("添加四个元素 ");
            for (int i = 0; i < 4; i++)
            {
                Pet pet = new Pet();
                pet.age = i;
                pets.Add(pet);
            }
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);
            pets.ForEach(PrintAddress);
            Console.WriteLine("再添加四个元素 ");
            for (int i = 0; i < 4; i++)
            {
                Pet pet = new Pet();
                pet.age = i + 4;
                pets.Add(pet);
            }
            Console.WriteLine("Count : " + pets.Count);
            Console.WriteLine("Capacity : " + pets.Capacity);
            pets.ForEach(PrintAddress);

            while (true) ;
        }

我们发现再向其中添加元素的时候,容量被自动拓展了。这是因为在向List中Add元素的时候会先检测是否满了,如果满了就会根据拓展规则来拓展。

List自动拓展规则:当前容量X2(当然<Int最大值),但是我们能够看到在长度由4变为8的时候,整个List的地址其实是发生了变化的,将List之前的数据全部复制到一个容量更大的新的容器中,当然我们的pets的地址会更改为新的地址。

这里就有一个小的细节值得注意,频繁扩容一定是消耗性能的,毕竟要将数据完全复制到新的内存中,所以如果能够粗略估计数据的规模,在初始化的时候可以给出一个合理的初始容量,以避免频繁扩容。以下是List的扩容的源码,能够看到其规则。

        // Adds the given object to the end of this list. The size of the list is
        // increased by one. If required, the capacity of the list is doubled
        // before adding the new element.
        //
        public void Add(T item) {
            if (_size == _items.Length) EnsureCapacity(_size + 1);
            _items[_size++] = item;
            _version++;
        }



        // Ensures that the capacity of this list is at least the given minimum
        // value. If the currect capacity of the list is less than min, the
        // capacity is increased to twice the current capacity or to min,
        // whichever is larger.
        private void EnsureCapacity(int min) {
            if (_items.Length < min) {
                int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
                // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
                // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
                if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
                if (newCapacity < min) newCapacity = min;
                Capacity = newCapacity;
            }
        }

 

List源码里还有一些值得学习的小技巧:

比如我们想删除掉pets表中age为偶数的元素,我们可能会这样去写。

       private static void RemovePets() {
            for (int i = 0; i <pets.Count; i++)
            {             
                if (pets[i].age % 2 == 0) {
                    pets.RemoveAt(i);
                    Console.WriteLine("------------------------ " + i);
                    pets.ForEach(PrintAddress);
                }
            }
        }

从打印的结果可以看出,再没删除一次,pets中的所有数据的地址都发生了改变,也就是说每次删除一个元素都对所有的元素进行了一次集体搬家,这样不需要删除的元素愣生生搬家了四次。这显然是不合理的,合理的办法应该是先找出所有的需要删除的元素,

再对剩余的元素搬家一次。List提供了RemoveAll方法,就是干这事的。所以代码可以修改为

        private static void RemovePets() {
            pets.RemoveAll((pet)=> { return pet.age % 2 == 0; });
        }

多次搬家显然是耗时的,我们这里测试一下两种方法耗时的时间上到底有多大的差别:

        private static List<Pet> pets;
        public void Use() {
            pets = new List<Pet>(0);
            for (int i = 0; i < 100000; i++)
            {
                Pet pet = new Pet();
                pet.age = i;
                pets.Add(pet);
            }
            Console.WriteLine("pets.count : " + pets.Count);
            DateTime beforeDT = System.DateTime.Now;
            //耗时代码//
            RemovePets1();
            DateTime afterDT = System.DateTime.Now;
            TimeSpan ts = afterDT.Subtract(beforeDT);
            Console.WriteLine("need : " + ts.TotalMilliseconds);
            Console.WriteLine("pets.count : " + pets.Count);
        }

        private void RemovePets2()
        {
            for (int i = pets.Count - 1; i > 0; i--)
            {
                if (pets[i].age % 2 == 0)
                {
                    pets.RemoveAt(i);
                }
            }
        }

        private  void RemovePets1() {
            for (int i = 0; i < pets.Count; i++)
            {
                if (pets[i].age % 2 == 0) {
                    pets.RemoveAt(i);
                }
            }
        }

        private static void RemovePets()
        {
            pets.RemoveAll((pet) => { return pet.age % 2 == 0; });
        }

为了看出差别,我们将数据量扩展为100000,使用RemovePets1删除偶数的耗时如下图:多次测试都在750ms左右,也可以用RemovePets2测试一下,大概在360ms左右

pic10

我们将上述代码中的RemovePets1改用RemovePets() ,结果如下:多次测试都在3ms左右。相差甚远。

pic11

 

当然更重要的是去学会人家怎么实现的,那就去看看源码,这个过程就慢慢嚼吧,是一个耐人寻味的过程,说难吧,也不难,但是真的是很细致。

        // This method removes all items which matches the predicate.
        // The complexity is O(n).   
        public int RemoveAll(Predicate<T> match) {
            if( match == null) {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
            }
            Contract.Ensures(Contract.Result<int>() >= 0);
            Contract.Ensures(Contract.Result<int>() <= Contract.OldValue(Count));
            Contract.EndContractBlock();
    
            int freeIndex = 0;   // the first free slot in items array
 
            // Find the first item which needs to be removed.
            while( freeIndex < _size && !match(_items[freeIndex])) freeIndex++;            
            if( freeIndex >= _size) return 0;
            
            int current = freeIndex + 1;
            while( current < _size) {
                // Find the first item which needs to be kept.
                while( current < _size && match(_items[current])) current++;            
 
                if( current < _size) {
                    // copy item to the free slot.
                    _items[freeIndex++] = _items[current++];
                }
            }                       
            
            Array.Clear(_items, freeIndex, _size - freeIndex);
            int result = _size - freeIndex;
            _size = freeIndex;
            _version++;
            return result;
        }

源码:https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,49c519bce0cdbd82

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值